Thunks/gen: Add assume_compatible/is_opaque annotations

These annotations allow for a given type or parameter to be treated as
"compatible" even if data layout analysis can't infer this automatically.

assume_compatible_data_layout is more powerful than is_opaque, since it
allows for structs containing members of a certain type to be automatically
inferred as "compatible".

Conversely however, is_opaque enforces that the underlying data is never
accessed directly, since non-pointer uses of the type would still be
detected as "incompatible".
This commit is contained in:
Tony Wasserka 2023-10-19 12:49:00 +02:00
parent 5ef7537e61
commit 6a6886305e
9 changed files with 197 additions and 8 deletions

View File

@ -152,6 +152,33 @@ FindClassTemplateDeclByName(clang::DeclContext& decl_context, std::string_view s
}
}
struct TypeAnnotations {
bool is_opaque = false;
bool assumed_compatible = false;
};
static TypeAnnotations GetTypeAnnotations(clang::ASTContext& context, clang::CXXRecordDecl* decl) {
if (!decl->hasDefinition()) {
return {};
}
ErrorReporter report_error { context };
TypeAnnotations ret;
for (const clang::CXXBaseSpecifier& base : decl->bases()) {
auto annotation = base.getType().getAsString();
if (annotation == "fexgen::opaque_type") {
ret.is_opaque = true;
} else if (annotation == "fexgen::assume_compatible_data_layout") {
ret.assumed_compatible = true;
} else {
throw report_error(base.getSourceRange().getBegin(), "Unknown type annotation");
}
}
return ret;
}
static ParameterAnnotations GetParameterAnnotations(clang::ASTContext& context, clang::CXXRecordDecl* decl) {
if (!decl->hasDefinition()) {
return {};
@ -164,6 +191,8 @@ static ParameterAnnotations GetParameterAnnotations(clang::ASTContext& context,
auto annotation = base.getType().getAsString();
if (annotation == "fexgen::ptr_passthrough") {
ret.is_passthrough = true;
} else if (annotation == "fexgen::assume_compatible_data_layout") {
ret.assume_compatible = true;
} else {
throw report_error(base.getSourceRange().getBegin(), "Unknown parameter annotation");
}
@ -193,7 +222,12 @@ void AnalysisAction::ParseInterface(clang::ASTContext& context) {
if (type->isFunctionPointerType() || type->isFunctionType()) {
thunked_funcptrs[type.getAsString()] = std::pair { type.getTypePtr(), no_param_annotations };
} else {
[[maybe_unused]] auto [it, inserted] = types.emplace(context.getCanonicalType(type.getTypePtr()), RepackedType { });
const auto annotations = GetTypeAnnotations(context, decl);
RepackedType repack_info = {
.assumed_compatible = annotations.is_opaque || annotations.assumed_compatible,
.pointers_only = annotations.is_opaque && !annotations.assumed_compatible,
};
[[maybe_unused]] auto [it, inserted] = types.emplace(context.getCanonicalType(type.getTypePtr()), repack_info);
assert(inserted);
}
}
@ -374,12 +408,19 @@ void AnalysisAction::ParseInterface(clang::ASTContext& context) {
types.emplace(param_type.getTypePtr(), RepackedType { });
} else if (param_type->isEnumeralType()) {
types.emplace(context.getCanonicalType(param_type.getTypePtr()), RepackedType { });
} else if ( param_type->isStructureType()) {
} else if ( param_type->isStructureType() &&
!(types.contains(context.getCanonicalType(param_type.getTypePtr())) &&
LookupType(context, param_type.getTypePtr()).assumed_compatible)) {
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()) {
if ((types.contains(context.getCanonicalType(pointee_type.getTypePtr())) && LookupType(context, pointee_type.getTypePtr()).assumed_compatible)) {
// Nothing to do
data.param_annotations[param_idx].assume_compatible = true;
} else if ( pointee_type->isStructureType() &&
!(types.contains(context.getCanonicalType(pointee_type.getTypePtr())) &&
LookupType(context, pointee_type.getTypePtr()).assumed_compatible)) {
check_struct_type(pointee_type.getTypePtr());
types.emplace(context.getCanonicalType(pointee_type.getTypePtr()), RepackedType { });
} else if (data.param_annotations[param_idx].is_passthrough) {
@ -387,6 +428,8 @@ void AnalysisAction::ParseInterface(clang::ASTContext& context) {
throw report_error(param_loc, "Passthrough annotation requires custom host implementation");
}
// Nothing to do
} else if (data.param_annotations[param_idx].assume_compatible) {
// Nothing to do
} 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")
@ -455,6 +498,11 @@ void AnalysisAction::CoverReferencedTypes(clang::ASTContext& context) {
continue;
}
if (type_repack_info.assumed_compatible) {
// If assumed compatible, we don't need the member definitions
continue;
}
for (auto* member : type->getAsStructureType()->getDecl()->fields()) {
auto member_type = member->getType().getTypePtr();
while (member_type->isArrayType()) {
@ -467,6 +515,15 @@ void AnalysisAction::CoverReferencedTypes(clang::ASTContext& context) {
if (!member_type->isBuiltinType()) {
member_type = context.getCanonicalType(member_type);
}
if (types.contains(member_type) && types.at(member_type).pointers_only) {
if (member_type == context.getCanonicalType(member->getType().getTypePtr())) {
throw std::runtime_error(fmt::format("\"{}\" references opaque type \"{}\" via non-pointer member \"{}\"",
clang::QualType { type, 0 }.getAsString(),
clang::QualType { member_type, 0 }.getAsString(),
member->getNameAsString()));
}
continue;
}
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(),

View File

@ -24,6 +24,7 @@ struct ThunkedCallback : FunctionParams {
struct ParameterAnnotations {
bool is_passthrough = false;
bool assume_compatible = false;
bool operator==(const ParameterAnnotations&) const = default;
};
@ -117,6 +118,8 @@ public:
std::unique_ptr<clang::ASTConsumer> CreateASTConsumer(clang::CompilerInstance&, clang::StringRef /*file*/) override;
struct RepackedType {
bool assumed_compatible = false; // opaque_type or assume_compatible_data_layout
bool pointers_only = assumed_compatible; // if true, only pointers to this type may be used
};
protected:

View File

@ -26,6 +26,14 @@ std::unordered_map<const clang::Type*, TypeInfo> ComputeDataLayout(const clang::
// First, add all types directly used in function signatures of the library API to the meta set
for (const auto& [type, type_repack_info] : types) {
if (type_repack_info.assumed_compatible) {
auto [_, inserted] = layout.insert(std::pair { context.getCanonicalType(type), TypeInfo {} });
if (!inserted) {
throw std::runtime_error("Failed to gather type metadata: Opaque type \"" + clang::QualType { type, 0 }.getAsString() + "\" already registered");
}
continue;
}
if (type->isIncompleteType()) {
throw std::runtime_error("Cannot compute data layout of incomplete type \"" + clang::QualType { type, 0 }.getAsString() + "\". Did you forget any annotations?");
}
@ -54,7 +62,7 @@ std::unordered_map<const clang::Type*, TypeInfo> ComputeDataLayout(const clang::
// Then, add information about members
for (const auto& [type, type_repack_info] : types) {
if (!type->isStructureType()) {
if (!type->isStructureType() || type_repack_info.assumed_compatible) {
continue;
}
@ -202,6 +210,14 @@ TypeCompatibility DataLayoutCompareAction::GetTypeCompatibility(
}
}
if (types.contains(type) && types.at(type).assumed_compatible) {
if (types.at(type).pointers_only && !type->isPointerType()) {
throw std::runtime_error("Tried to dereference opaque type \"" + clang::QualType { type, 0 }.getAsString() + "\" when querying data layout compatibility");
}
type_compat.at(type) = TypeCompatibility::Full;
return TypeCompatibility::Full;
}
auto type_name = get_type_name(context, type);
auto& guest_info = guest_abi.at(type_name);
auto& host_info = host_abi.at(type->isBuiltinType() ? type : context.getCanonicalType(type));
@ -257,12 +273,18 @@ TypeCompatibility DataLayoutCompareAction::GetTypeCompatibility(
// * Pointer member is annotated
// TODO: Don't restrict this to structure types. it applies to pointers to builtin types too!
auto host_member_pointee_type = context.getCanonicalType(host_member_type->getPointeeType().getTypePtr());
if (host_member_pointee_type->isPointerType()) {
if (types.contains(host_member_pointee_type) && types.at(host_member_pointee_type).assumed_compatible) {
// Pointee doesn't need repacking, but pointer needs extending on 32-bit
member_compat.push_back(is_32bit ? TypeCompatibility::Repackable : TypeCompatibility::Full);
} else if (host_member_pointee_type->isPointerType()) {
// This is a nested pointer, e.g. void**
if (is_32bit) {
// Nested pointers can't be repacked on 32-bit
member_compat.push_back(TypeCompatibility::None);
} else if (types.contains(host_member_pointee_type->getPointeeType().getTypePtr()) && types.at(host_member_pointee_type->getPointeeType().getTypePtr()).assumed_compatible) {
// Pointers to opaque types are fine
member_compat.push_back(TypeCompatibility::Full);
} else {
// Check the innermost type's compatibility on 64-bit
auto pointee_pointee_type = host_member_pointee_type->getPointeeType().getTypePtr();

View File

@ -58,7 +58,9 @@ void GenerateThunkLibsAction::OnAnalysisComplete(clang::ASTContext& context) {
if (StrictModeEnabled(context)) {
const auto host_abi = ComputeDataLayout(context, types);
for (const auto& [type, type_repack_info] : types) {
GetTypeCompatibility(context, type, host_abi, ret);
if (!type_repack_info.pointers_only) {
GetTypeCompatibility(context, type, host_abi, ret);
}
}
}
return ret;
@ -297,7 +299,7 @@ void GenerateThunkLibsAction::OnAnalysisComplete(clang::ASTContext& context) {
continue;
}
auto type = param_type->getPointeeType();
if (type_compat.at(context.getCanonicalType(type.getTypePtr())) == TypeCompatibility::None) {
if (!types.at(context.getCanonicalType(type.getTypePtr())).assumed_compatible && type_compat.at(context.getCanonicalType(type.getTypePtr())) == TypeCompatibility::None) {
// TODO: Factor in "assume_compatible_layout" annotations here
// That annotation should cause the type to be treated as TypeCompatibility::Full
if (!thunk.param_annotations[param_idx].is_passthrough) {
@ -340,6 +342,9 @@ void GenerateThunkLibsAction::OnAnalysisComplete(clang::ASTContext& context) {
}
auto& param_type = thunk.param_types[param_idx];
const bool is_assumed_compatible = param_type->isPointerType() &&
(thunk.param_annotations[param_idx].assume_compatible || ((param_type->getPointeeType()->isStructureType() || (param_type->getPointeeType()->isPointerType() && param_type->getPointeeType()->getPointeeType()->isStructureType())) &&
(types.contains(context.getCanonicalType(param_type->getPointeeType()->getLocallyUnqualifiedSingleStepDesugaredType().getTypePtr())) && LookupType(context, context.getCanonicalType(param_type->getPointeeType()->getLocallyUnqualifiedSingleStepDesugaredType().getTypePtr())).assumed_compatible)));
std::optional<TypeCompatibility> pointee_compat;
if (param_type->isPointerType()) {
@ -353,7 +358,7 @@ void GenerateThunkLibsAction::OnAnalysisComplete(clang::ASTContext& context) {
continue;
}
if (!param_type->isPointerType() || pointee_compat == TypeCompatibility::Full ||
if (!param_type->isPointerType() || (is_assumed_compatible || pointee_compat == TypeCompatibility::Full) ||
param_type->getPointeeType()->isBuiltinType() /* TODO: handle size_t. Actually, properly check for data layout compatibility */) {
// Fully compatible
} else if (pointee_compat == TypeCompatibility::Repackable) {
@ -416,6 +421,9 @@ void GenerateThunkLibsAction::OnAnalysisComplete(clang::ASTContext& context) {
if (param_annotations.contains(param_idx) && param_annotations.at(param_idx).is_passthrough) {
annotations += ".is_passthrough=true,";
}
if (param_annotations.contains(param_idx) && param_annotations.at(param_idx).assume_compatible) {
annotations += ".assume_compatible=true,";
}
annotations += "}";
}
fmt::print( file, " {{(uint8_t*)\"\\x{:02x}\", (void(*)(void *))&GuestWrapperForHostFunction<{}({})>::Call<{}>}}, // {}\n",

View File

@ -13,9 +13,20 @@ struct callback_annotation_base {
struct callback_stub : callback_annotation_base {};
struct callback_guest : callback_annotation_base {};
struct type_annotation_base { bool prevent_multiple; };
// Pointers to types annotated with this will be passed through without change
struct opaque_type : type_annotation_base {};
// Function parameter annotation.
// Pointers are passed through to host (extending to 64-bit if needed) without modifying the pointee.
// The type passed to Host will be guest_layout<pointee_type>*.
struct ptr_passthrough {};
// Type / Function parameter annotation.
// Assume objects of the given type are compatible across architectures,
// even if the generator can't automatically prove this. For pointers, this refers to the pointee type.
// NOTE: In contrast to opaque_type, this allows for non-pointer members with the annotated type to be repacked automatically.
struct assume_compatible_data_layout : type_annotation_base {};
} // namespace fexgen

View File

@ -105,6 +105,7 @@ struct GuestcallInfo {
struct ParameterAnnotations {
bool is_passthrough = false;
bool assume_compatible = false;
};
// Placeholder type to indicate the given data is in guest-layout

View File

@ -464,6 +464,21 @@ TEST_CASE_METHOD(Fixture, "DataLayout") {
"template<> struct fex_gen_type<A> {};\n", guest_abi),
Catch::Contains("unannotated member") && Catch::Contains("union type"));
}
SECTION("with annotation") {
auto action = compute_data_layout(
"#include <thunks_common.h>\n"
"#include <cstdint>\n",
"union B { int32_t a; uint32_t b; };\n"
"struct A { B a; };\n"
"template<> struct fex_gen_type<B> : fexgen::assume_compatible_data_layout {};\n"
"template<> struct fex_gen_type<A> {};\n", guest_abi);
INFO(FormatDataLayout(action->host_layout));
REQUIRE(action->guest_layout->contains("A"));
CHECK(action->GetTypeCompatibility("struct A") == TypeCompatibility::Full);
}
}
}
@ -593,6 +608,36 @@ TEST_CASE_METHOD(Fixture, "DataLayoutPointers") {
Catch::Contains("unannotated member") && Catch::Contains("union type"));
}
SECTION("Pointer to union type with assume_compatible_data_layout annotation") {
auto action = compute_data_layout(
"#include <thunks_common.h>\n"
"#include <cstdint>\n",
"union B { int32_t a; uint32_t b; };\n"
"struct A { B* a; };\n"
"template<> struct fex_gen_type<B> : fexgen::assume_compatible_data_layout {};\n"
"template<> struct fex_gen_type<A> {};\n", guest_abi);
INFO(FormatDataLayout(action->host_layout));
REQUIRE(action->guest_layout->contains("A"));
CHECK(action->GetTypeCompatibility("struct A") == compat_full64_repackable32);
}
SECTION("Pointer to opaque type") {
auto action = compute_data_layout(
"#include <thunks_common.h>\n"
"#include <cstdint>\n",
"struct B;\n"
"struct A { B* a; };\n"
"template<> struct fex_gen_type<B> : fexgen::opaque_type {};\n"
"template<> struct fex_gen_type<A> {};\n", guest_abi);
INFO(FormatDataLayout(action->host_layout));
REQUIRE(action->guest_layout->contains("A"));
CHECK(action->GetTypeCompatibility("struct A") == compat_full64_repackable32);
}
SECTION("Self-referencing struct (like VkBaseOutStructure)") {
// Without annotation
auto action = compute_data_layout(
@ -605,6 +650,20 @@ TEST_CASE_METHOD(Fixture, "DataLayoutPointers") {
REQUIRE(action->guest_layout->contains("A"));
CHECK_THROWS_WITH(action->GetTypeCompatibility("struct A"), Catch::Contains("recursive reference"));
// With annotation
if (guest_abi == GuestABI::X86_64) {
auto action = compute_data_layout(
"#include <thunks_common.h>\n"
"#include <cstdint>\n",
"struct A { A* a; };\n"
"template<> struct fex_gen_type<A> : fexgen::assume_compatible_data_layout {};\n", guest_abi);
INFO(FormatDataLayout(action->host_layout));
REQUIRE(action->guest_layout->contains("A"));
CHECK(action->GetTypeCompatibility("struct A") == TypeCompatibility::Full);
}
}
SECTION("Circularly referencing structs") {
@ -623,6 +682,22 @@ TEST_CASE_METHOD(Fixture, "DataLayoutPointers") {
REQUIRE(action->guest_layout->contains("B"));
CHECK_THROWS_WITH(action->GetTypeCompatibility("struct A"), Catch::Contains("recursive reference"));
CHECK_THROWS_WITH(action->GetTypeCompatibility("struct B"), Catch::Contains("recursive reference"));
// With annotation
if (guest_abi == GuestABI::X86_64) {
auto action = compute_data_layout(
"#include <thunks_common.h>\n"
"#include <cstdint>\n",
"struct B;\n"
"struct A { B* a; };\n"
"struct B { A* a; };\n"
"template<> struct fex_gen_type<B> : fexgen::assume_compatible_data_layout {};\n", guest_abi);
INFO(FormatDataLayout(action->host_layout));
REQUIRE(action->guest_layout->contains("B"));
CHECK(action->GetTypeCompatibility("struct B") == TypeCompatibility::Full);
}
}
SECTION("Pointers to void") {

View File

@ -91,6 +91,9 @@ struct callback_annotation_base { bool prevent_multiple; };
struct callback_stub : callback_annotation_base {};
struct callback_guest : callback_annotation_base {};
struct opaque_type {};
struct assume_compatible_data_layout {};
struct ptr_passthrough {};
} // namespace fexgen

View File

@ -613,6 +613,15 @@ TEST_CASE_METHOD(Fixture, "VoidPointerParameter") {
CHECK_NOTHROW(run_thunkgen_host("", code, guest_abi));
}
SECTION("Assumed compatible") {
const char* code =
"#include <thunks_common.h>\n"
"void func(void*);\n"
"template<> struct fex_gen_config<func> {};\n"
"template<> struct fex_gen_param<func, 0, void*> : fexgen::assume_compatible_data_layout {};\n";
CHECK_NOTHROW(run_thunkgen_host("", code, guest_abi));
}
SECTION("Unannotated in struct") {
const char* prelude =
"struct A { void* a; };\n";