mirror of
https://github.com/FEX-Emu/FEX.git
synced 2025-02-03 13:03:22 +00:00
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:
parent
5ef7537e61
commit
6a6886305e
@ -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(),
|
||||
|
@ -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:
|
||||
|
@ -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();
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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") {
|
||||
|
@ -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
|
||||
|
@ -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";
|
||||
|
Loading…
x
Reference in New Issue
Block a user