Thunks/gen: Track data types passed across architecture boundaries

The set of these types is tracked in AnalysisAction, to which extensive
verification logic is added to detect potential incompatibilities and to
enforce use of annotatations where needed.
This commit is contained in:
Tony Wasserka committed 2023-10-02 22:18:22 +02:00
1 parent d65d29903b
commit 371bf50c76
2 files changed
+269 -81

No files matched your search

+240 -81
View File
@@ -128,6 +128,9 @@ void AnalysisAction::ExecuteAction() {
try {
ParseInterface(context);
if (StrictModeEnabled(context)) {
CoverReferencedTypes(context);
}
OnAnalysisComplete(context);
} catch (ClangDiagnosticAsException& exception) {
exception.Report(context.getDiagnostics());
@@ -149,9 +152,25 @@ FindClassTemplateDeclByName(clang::DeclContext& decl_context, std::string_view s
}
}
static ParameterAnnotations GetParameterAnnotations(clang::ASTContext& context, clang::CXXRecordDecl* decl) {
if (!decl->hasDefinition()) {
return {};
}
ErrorReporter report_error { context };
ParameterAnnotations ret;
for (const clang::CXXBaseSpecifier& base : decl->bases()) {
throw report_error(base.getSourceRange().getBegin(), "Unknown parameter annotation");
}
return ret;
}
void AnalysisAction::ParseInterface(clang::ASTContext& context) {
ErrorReporter report_error { context };
// TODO: Assert fex_gen_type is not declared at non-global namespaces
if (auto template_decl = FindClassTemplateDeclByName(*context.getTranslationUnitDecl(), "fex_gen_type")) {
for (auto* decl : template_decl->specializations()) {
const auto& template_args = decl->getTemplateArgs();
@@ -161,8 +180,42 @@ void AnalysisAction::ParseInterface(clang::ASTContext& context) {
// named types (e.g. GLuint/GLenum) are represented by
// different Type instances. The canonical type they refer
// to is unique, however.
auto type = context.getCanonicalType(template_args[0].getAsType()).getTypePtr();
funcptr_types.insert(type);
clang::QualType type = context.getCanonicalType(template_args[0].getAsType());
type = type->getLocallyUnqualifiedSingleStepDesugaredType();
if (type->isFunctionPointerType() || type->isFunctionType()) {
funcptr_types.insert(type.getTypePtr());
} else {
[[maybe_unused]] auto [it, inserted] = types.emplace(context.getCanonicalType(type.getTypePtr()), RepackedType { });
assert(inserted);
}
}
}
// Process function parameter annotations
std::unordered_map<const clang::FunctionDecl*, std::unordered_map<unsigned, ParameterAnnotations>> param_annotations;
for (auto& decl_context : decl_contexts) {
if (auto template_decl = FindClassTemplateDeclByName(*decl_context, "fex_gen_param")) {
for (auto* decl : template_decl->specializations()) {
const auto& template_args = decl->getTemplateArgs();
assert(template_args.size() == 3);
auto function = llvm::dyn_cast<clang::FunctionDecl>(template_args[0].getAsDecl());
auto param_idx = template_args[1].getAsIntegral().getZExtValue();
clang::QualType type = context.getCanonicalType(template_args[2].getAsType());
type = type->getLocallyUnqualifiedSingleStepDesugaredType();
if (param_idx >= function->getNumParams()) {
throw report_error(decl->getTypeAsWritten()->getTypeLoc().getAs<clang::TemplateSpecializationTypeLoc>().getArgLoc(1).getLocation(), "Out-of-bounds parameter index passed to fex_gen_param");
}
if (!type->isVoidType() && !context.hasSameType(type, function->getParamDecl(param_idx)->getType())) {
throw report_error(decl->getTypeAsWritten()->getTypeLoc().getAs<clang::TemplateSpecializationTypeLoc>().getArgLoc(2).getLocation(), "Type passed to fex_gen_param doesn't match the function signature")
.addNote(report_error(function->getParamDecl(param_idx)->getTypeSourceInfo()->getTypeLoc().getBeginLoc(), "Expected this type instead"));
}
param_annotations[function][param_idx] = GetParameterAnnotations(context, decl);
}
}
}
@@ -220,94 +273,200 @@ void AnalysisAction::ParseInterface(clang::ASTContext& context) {
const auto template_arg_loc = decl->getTypeAsWritten()->getTypeLoc().castAs<clang::TemplateSpecializationTypeLoc>().getArgLoc(0).getLocation();
auto emitted_function = llvm::dyn_cast<clang::FunctionDecl>(template_args[0].getAsDecl());
assert(emitted_function && "Argument is not a function");
auto return_type = emitted_function->getReturnType();
if (auto emitted_function = llvm::dyn_cast<clang::FunctionDecl>(template_args[0].getAsDecl())) {
auto return_type = emitted_function->getReturnType();
const auto annotations = GetAnnotations(context, decl);
if (return_type->isFunctionPointerType() && !annotations.returns_guest_pointer) {
throw report_error( template_arg_loc,
"Function pointer return types require explicit annotation\n");
}
// TODO: Use the types as written in the signature instead?
ThunkedFunction data;
data.function_name = emitted_function->getName().str();
data.return_type = return_type;
data.is_variadic = emitted_function->isVariadic();
data.decl = emitted_function;
data.custom_host_impl = annotations.custom_host_impl;
for (std::size_t param_idx = 0; param_idx < emitted_function->param_size(); ++param_idx) {
auto* param = emitted_function->getParamDecl(param_idx);
data.param_types.push_back(param->getType());
if (param->getType()->isFunctionPointerType()) {
auto funcptr = param->getFunctionType()->getAs<clang::FunctionProtoType>();
ThunkedCallback callback;
callback.return_type = funcptr->getReturnType();
for (auto& cb_param : funcptr->getParamTypes()) {
callback.param_types.push_back(cb_param);
}
callback.is_stub = annotations.callback_strategy == CallbackStrategy::Stub;
callback.is_guest = annotations.callback_strategy == CallbackStrategy::Guest;
callback.is_variadic = funcptr->isVariadic();
if (callback.is_guest && !data.custom_host_impl) {
throw report_error(template_arg_loc, "callback_guest can only be used with custom_host_impl");
}
data.callbacks.emplace(param_idx, callback);
if (!callback.is_stub && !callback.is_guest) {
funcptr_types.insert(context.getCanonicalType(funcptr));
}
if (data.callbacks.size() != 1) {
throw report_error(template_arg_loc, "Support for more than one callback is untested");
}
if (funcptr->isVariadic() && !callback.is_stub) {
throw report_error(template_arg_loc, "Variadic callbacks are not supported");
}
}
}
thunked_api.push_back(ThunkedAPIFunction { (const FunctionParams&)data, data.function_name, data.return_type,
namespace_info.host_loader.empty() ? "dlsym_default" : namespace_info.host_loader,
data.is_variadic || annotations.custom_guest_entrypoint,
data.is_variadic,
std::nullopt });
if (namespace_info.generate_guest_symtable) {
thunked_api.back().symtable_namespace = namespace_idx;
}
if (data.is_variadic) {
if (!annotations.uniform_va_type) {
throw report_error(decl->getBeginLoc(), "Variadic functions must be annotated with parameter type using uniform_va_type");
const auto annotations = GetAnnotations(context, decl);
if (return_type->isFunctionPointerType() && !annotations.returns_guest_pointer) {
throw report_error( template_arg_loc,
"Function pointer return types require explicit annotation\n");
}
// Convert variadic argument list into a count + pointer pair
data.param_types.push_back(context.getSizeType());
data.param_types.push_back(context.getPointerType(*annotations.uniform_va_type));
}
// TODO: Use the types as written in the signature instead?
ThunkedFunction data;
data.function_name = emitted_function->getName().str();
data.return_type = return_type;
data.is_variadic = emitted_function->isVariadic();
if (data.is_variadic) {
// This function is thunked through an "_internal" symbol since its signature
// is different from the one in the native host/guest libraries.
data.function_name = data.function_name + "_internal";
if (data.custom_host_impl) {
throw report_error(decl->getBeginLoc(), "Custom host impl requested but this is implied by the function signature already");
data.decl = emitted_function;
data.custom_host_impl = annotations.custom_host_impl;
data.param_annotations = param_annotations[emitted_function];
const int retval_index = -1;
for (int param_idx = retval_index; param_idx < (int)emitted_function->param_size(); ++param_idx) {
auto param_type = param_idx == retval_index ? emitted_function->getReturnType() : emitted_function->getParamDecl(param_idx)->getType();
auto param_loc = param_idx == retval_index ? emitted_function->getReturnTypeSourceRange().getBegin() : emitted_function->getParamDecl(param_idx)->getBeginLoc();
if (param_idx != retval_index) {
data.param_types.push_back(param_type);
} else if (param_type->isVoidType()) {
continue;
}
auto check_struct_type = [&](const clang::Type* type) {
if (type->isIncompleteType()) {
if (!StrictModeEnabled(context)) {
return;
}
throw report_error(type->getAsTagDecl()->getBeginLoc(), "Unannotated pointer with incomplete struct type; consider using an opaque_type annotation")
.addNote(report_error(emitted_function->getNameInfo().getLoc(), "in function", clang::DiagnosticsEngine::Note))
.addNote(report_error(template_arg_loc, "used in annotation here", clang::DiagnosticsEngine::Note));
}
for (auto* member : type->getAsStructureType()->getDecl()->fields()) {
/*if (!member->getType()->isPointerType())*/ {
// TODO: Perform more elaborate validation for non-pointers to ensure ABI compatibility
continue;
}
throw report_error(member->getBeginLoc(), "Unannotated pointer member")
.addNote(report_error(param_loc, "in struct type", clang::DiagnosticsEngine::Note))
.addNote(report_error(template_arg_loc, "used in annotation here", clang::DiagnosticsEngine::Note));
}
};
if (param_type->isFunctionPointerType()) {
if (param_idx == retval_index) {
// TODO: We already rely on this in a few places...
// throw report_error(template_arg_loc, "Support for returning function pointers is not implemented");
continue;
}
auto funcptr = emitted_function->getParamDecl(param_idx)->getFunctionType()->getAs<clang::FunctionProtoType>();
ThunkedCallback callback;
callback.return_type = funcptr->getReturnType();
for (auto& cb_param : funcptr->getParamTypes()) {
callback.param_types.push_back(cb_param);
}
callback.is_stub = annotations.callback_strategy == CallbackStrategy::Stub;
callback.is_guest = annotations.callback_strategy == CallbackStrategy::Guest;
callback.is_variadic = funcptr->isVariadic();
if (callback.is_guest && !data.custom_host_impl) {
throw report_error(template_arg_loc, "callback_guest can only be used with custom_host_impl");
}
data.callbacks.emplace(param_idx, callback);
if (!callback.is_stub && !callback.is_guest && !data.custom_host_impl) {
funcptr_types.insert(context.getCanonicalType(funcptr));
}
if (data.callbacks.size() != 1) {
throw report_error(template_arg_loc, "Support for more than one callback is untested");
}
if (funcptr->isVariadic() && !callback.is_stub) {
throw report_error(template_arg_loc, "Variadic callbacks are not supported");
}
} else if (param_type->isBuiltinType()) {
// NOTE: Intentionally not using getCanonicalType here since that would turn e.g. size_t into platform-specific types
// TODO: Still, we may want to de-duplicate some of these...
types.emplace(param_type.getTypePtr(), RepackedType { });
} else if (param_type->isEnumeralType()) {
types.emplace(context.getCanonicalType(param_type.getTypePtr()), RepackedType { });
} else if ( param_type->isStructureType()) {
check_struct_type(param_type.getTypePtr());
types.emplace(context.getCanonicalType(param_type.getTypePtr()), RepackedType { });
} else if (param_type->isPointerType()) {
auto pointee_type = param_type->getPointeeType()->getLocallyUnqualifiedSingleStepDesugaredType();
if ( pointee_type->isStructureType()) {
check_struct_type(pointee_type.getTypePtr());
types.emplace(context.getCanonicalType(pointee_type.getTypePtr()), RepackedType { });
} else if (false /* TODO: Can't check if this is unsupported until data layout analysis is complete */) {
throw report_error(param_loc, "Unsupported parameter type")
.addNote(report_error(emitted_function->getNameInfo().getLoc(), "in function", clang::DiagnosticsEngine::Note))
.addNote(report_error(template_arg_loc, "used in definition here", clang::DiagnosticsEngine::Note));
}
} else {
// TODO: For non-pointer parameters, perform more elaborate validation to ensure ABI compatibility
}
}
data.custom_host_impl = true;
thunked_api.push_back(ThunkedAPIFunction { (const FunctionParams&)data, data.function_name, data.return_type,
namespace_info.host_loader.empty() ? "dlsym_default" : namespace_info.host_loader,
data.is_variadic || annotations.custom_guest_entrypoint,
data.is_variadic,
std::nullopt });
if (namespace_info.generate_guest_symtable) {
thunked_api.back().symtable_namespace = namespace_idx;
}
if (data.is_variadic) {
if (!annotations.uniform_va_type) {
throw report_error(decl->getBeginLoc(), "Variadic functions must be annotated with parameter type using uniform_va_type");
}
// Convert variadic argument list into a count + pointer pair
data.param_types.push_back(context.getSizeType());
data.param_types.push_back(context.getPointerType(*annotations.uniform_va_type));
types.emplace(context.getSizeType()->getTypePtr(), RepackedType { });
if (!annotations.uniform_va_type.value()->isVoidPointerType()) {
types.emplace(annotations.uniform_va_type->getTypePtr(), RepackedType { });
}
}
if (data.is_variadic) {
// This function is thunked through an "_internal" symbol since its signature
// is different from the one in the native host/guest libraries.
data.function_name = data.function_name + "_internal";
if (data.custom_host_impl) {
throw report_error(decl->getBeginLoc(), "Custom host impl requested but this is implied by the function signature already");
}
data.custom_host_impl = true;
}
// For indirect calls, register the function signature as a function pointer type
if (namespace_info.indirect_guest_calls) {
funcptr_types.insert(context.getCanonicalType(emitted_function->getFunctionType()));
}
thunks.push_back(std::move(data));
}
}
}
}
}
void AnalysisAction::CoverReferencedTypes(clang::ASTContext& context) {
// Repeat until no more children are appended
for (bool changed = true; std::exchange(changed, false);) {
for ( auto next_type_it = types.begin(), type_it = next_type_it;
type_it != types.end();
type_it = next_type_it) {
++next_type_it;
const auto& [type, type_repack_info] = *type_it;
if (!type->isStructureType()) {
continue;
}
for (auto* member : type->getAsStructureType()->getDecl()->fields()) {
auto member_type = member->getType().getTypePtr();
while (member_type->isArrayType()) {
member_type = member_type->getArrayElementTypeNoTypeQual();
}
while (member_type->isPointerType()) {
member_type = member_type->getPointeeType().getTypePtr();
}
// For indirect calls, register the function signature as a function pointer type
if (namespace_info.indirect_guest_calls) {
funcptr_types.insert(context.getCanonicalType(emitted_function->getFunctionType()));
if (!member_type->isBuiltinType()) {
member_type = context.getCanonicalType(member_type);
}
if (member_type->isUnionType() && !types.contains(member_type)) {
throw std::runtime_error(fmt::format("\"{}\" has unannotated member \"{}\" of union type \"{}\"",
clang::QualType { type, 0 }.getAsString(),
member->getNameAsString(),
clang::QualType { member_type, 0 }.getAsString()));
}
thunks.push_back(std::move(data));
if (!member_type->isStructureType() && !(member_type->isBuiltinType() && !member_type->isVoidType()) && !member_type->isEnumeralType()) {
continue;
}
auto [new_type_it, inserted] = types.emplace(member_type, RepackedType { });
if (inserted) {
changed = true;
next_type_it = new_type_it;
}
}
}
}
+29
View File
@@ -1,5 +1,6 @@
#pragma once
#include <clang/Basic/FileEntry.h>
#include <clang/Frontend/FrontendAction.h>
#include <memory>
@@ -21,6 +22,10 @@ struct ThunkedCallback : FunctionParams {
bool is_variadic = false;
};
struct ParameterAnnotations {
bool operator==(const ParameterAnnotations&) const = default;
};
/**
* Guest<->Host transition point.
*
@@ -52,6 +57,10 @@ struct ThunkedFunction : FunctionParams {
// Maps parameter index to ThunkedCallback
std::unordered_map<unsigned, ThunkedCallback> callbacks;
// Maps parameter index to ParameterAnnotations
// TODO: Use index -1 for the return value?
std::unordered_map<unsigned, ParameterAnnotations> param_annotations;
clang::FunctionDecl* decl;
};
@@ -105,10 +114,16 @@ public:
std::unique_ptr<clang::ASTConsumer> CreateASTConsumer(clang::CompilerInstance&, clang::StringRef /*file*/) override;
struct RepackedType {
};
protected:
// Build the internal API representation by processing fex_gen_config and other annotated entities
void ParseInterface(clang::ASTContext&);
// Recursively extend the type set to include types of struct members
void CoverReferencedTypes(clang::ASTContext&);
// Called from ExecuteAction() after parsing is complete
virtual void OnAnalysisComplete(clang::ASTContext&) {};
@@ -116,8 +131,22 @@ protected:
std::vector<ThunkedFunction> thunks;
std::vector<ThunkedAPIFunction> thunked_api;
std::unordered_set<const clang::Type*> funcptr_types;
std::unordered_map<const clang::Type*, RepackedType> types;
std::optional<unsigned> lib_version;
std::vector<NamespaceInfo> namespaces;
RepackedType& LookupType(clang::ASTContext& context, const clang::Type* type) {
return types.at(context.getCanonicalType(type));
}
};
// Analysis can't process interfaces of real libraries, yet. This function
// defines a "strict mode" to use for tests, only. Real libraries will switch
// to strict mode once analysis is more feature-complete.
inline bool StrictModeEnabled(clang::ASTContext& context) {
auto filename = context.getSourceManager().getFileEntryForID(context.getSourceManager().getMainFileID())->getName();
return filename.endswith("libfex_thunk_test_interface.cpp") || filename.endswith("gen_input.cpp");
}