diff --git a/ThunkLibs/Generator/analysis.cpp b/ThunkLibs/Generator/analysis.cpp index 6e395479e2..e8f67e3df5 100644 --- a/ThunkLibs/Generator/analysis.cpp +++ b/ThunkLibs/Generator/analysis.cpp @@ -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 {}; @@ -161,7 +188,14 @@ static ParameterAnnotations GetParameterAnnotations(clang::ASTContext& context, ParameterAnnotations ret; for (const clang::CXXBaseSpecifier& base : decl->bases()) { - throw report_error(base.getSourceRange().getBegin(), "Unknown parameter annotation"); + 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"); + } } return ret; @@ -170,6 +204,8 @@ static ParameterAnnotations GetParameterAnnotations(clang::ASTContext& context, void AnalysisAction::ParseInterface(clang::ASTContext& context) { ErrorReporter report_error { context }; + const std::unordered_map no_param_annotations {}; + // 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()) { @@ -184,9 +220,14 @@ void AnalysisAction::ParseInterface(clang::ASTContext& context) { type = type->getLocallyUnqualifiedSingleStepDesugaredType(); if (type->isFunctionPointerType() || type->isFunctionType()) { - funcptr_types.insert(type.getTypePtr()); + 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); } } @@ -349,7 +390,7 @@ void AnalysisAction::ParseInterface(clang::ASTContext& context) { data.callbacks.emplace(param_idx, callback); if (!callback.is_stub && !callback.is_guest && !data.custom_host_impl) { - funcptr_types.insert(context.getCanonicalType(funcptr)); + thunked_funcptrs[emitted_function->getNameAsString() + "_cb" + std::to_string(param_idx)] = std::pair { context.getCanonicalType(funcptr), no_param_annotations }; } if (data.callbacks.size() != 1) { @@ -358,20 +399,38 @@ void AnalysisAction::ParseInterface(clang::ASTContext& context) { if (funcptr->isVariadic() && !callback.is_stub) { throw report_error(template_arg_loc, "Variadic callbacks are not supported"); } + + // Force treatment as passthrough-pointer + data.param_annotations[param_idx].is_passthrough = true; } 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()) { + } 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) { + if (!data.custom_host_impl) { + 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") .addNote(report_error(emitted_function->getNameInfo().getLoc(), "in function", clang::DiagnosticsEngine::Note)) @@ -417,7 +476,7 @@ void AnalysisAction::ParseInterface(clang::ASTContext& context) { // 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())); + thunked_funcptrs[emitted_function->getNameAsString()] = std::pair { context.getCanonicalType(emitted_function->getFunctionType()), data.param_annotations }; } thunks.push_back(std::move(data)); @@ -439,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()) { @@ -451,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(), diff --git a/ThunkLibs/Generator/analysis.h b/ThunkLibs/Generator/analysis.h index 3255444ce5..3b0d9ebc92 100644 --- a/ThunkLibs/Generator/analysis.h +++ b/ThunkLibs/Generator/analysis.h @@ -23,6 +23,9 @@ struct ThunkedCallback : FunctionParams { }; struct ParameterAnnotations { + bool is_passthrough = false; + bool assume_compatible = false; + bool operator==(const ParameterAnnotations&) const = default; }; @@ -115,6 +118,8 @@ class AnalysisAction : public clang::ASTFrontendAction { std::unique_ptr 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: @@ -132,7 +137,10 @@ class AnalysisAction : public clang::ASTFrontendAction { std::vector thunks; std::vector thunked_api; - std::unordered_set funcptr_types; + // Set of function types for which to generate Guest->Host thunking trampolines. + // The map key is a unique identifier that must be consistent between guest/host processing passes. + // The map value is a pair of the function pointer's clang::Type and the mapping of parameter annotations + std::unordered_map>> thunked_funcptrs; std::unordered_map types; std::optional lib_version; diff --git a/ThunkLibs/Generator/data_layout.cpp b/ThunkLibs/Generator/data_layout.cpp index da05a6ee83..f1e952c5eb 100644 --- a/ThunkLibs/Generator/data_layout.cpp +++ b/ThunkLibs/Generator/data_layout.cpp @@ -26,6 +26,14 @@ std::unordered_map 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 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; } @@ -151,10 +159,34 @@ ABI GetStableLayout(const clang::ASTContext& context, const std::unordered_map GetSha256(const std::string& function_name) { + std::array sha256; + SHA256(reinterpret_cast(function_name.data()), + function_name.size(), + sha256.data()); + return sha256; +}; + void AnalyzeDataLayoutAction::OnAnalysisComplete(clang::ASTContext& context) { if (StrictModeEnabled(context)) { type_abi = GetStableLayout(context, ComputeDataLayout(context, types)); } + + // Register functions that must be guest-callable through host function pointers + for (auto funcptr_type_it = thunked_funcptrs.begin(); funcptr_type_it != thunked_funcptrs.end(); ++funcptr_type_it) { + auto& funcptr_id = funcptr_type_it->first; + auto& [type, param_annotations] = funcptr_type_it->second; + auto func_type = type->getAs(); + std::string mangled_name = clang::QualType { type, 0 }.getAsString(); + auto cb_sha256 = GetSha256("fexcallback_" + mangled_name); + FuncPtrInfo info = { cb_sha256 }; + + info.result = func_type->getReturnType().getAsString(); + for (auto arg : func_type->getParamTypes()) { + info.args.push_back(arg.getAsString()); + } + type_abi.thunked_funcptrs[funcptr_id] = std::move(info); + } } TypeCompatibility DataLayoutCompareAction::GetTypeCompatibility( @@ -178,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)); @@ -233,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(); @@ -297,6 +343,10 @@ TypeCompatibility DataLayoutCompareAction::GetTypeCompatibility( return compat; } +FuncPtrInfo DataLayoutCompareAction::LookupGuestFuncPtrInfo(const char* funcptr_id) { + return guest_abi.thunked_funcptrs.at(funcptr_id); +} + DataLayoutCompareActionFactory::DataLayoutCompareActionFactory(const ABI& abi) : abi(abi) { } diff --git a/ThunkLibs/Generator/data_layout.h b/ThunkLibs/Generator/data_layout.h index 3d58357f4f..da30c3d0ed 100644 --- a/ThunkLibs/Generator/data_layout.h +++ b/ThunkLibs/Generator/data_layout.h @@ -84,6 +84,7 @@ struct FuncPtrInfo { }; struct ABI : std::unordered_map { + std::unordered_map thunked_funcptrs; int pointer_size; // in bytes }; @@ -111,6 +112,8 @@ class DataLayoutCompareAction : public AnalysisAction { const std::unordered_map host_abi, std::unordered_map& type_compat); + FuncPtrInfo LookupGuestFuncPtrInfo(const char* funcptr_id); + private: const ABI& guest_abi; }; diff --git a/ThunkLibs/Generator/gen.cpp b/ThunkLibs/Generator/gen.cpp index 5db5142a18..b18e2b26e1 100644 --- a/ThunkLibs/Generator/gen.cpp +++ b/ThunkLibs/Generator/gen.cpp @@ -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; @@ -101,14 +103,6 @@ void GenerateThunkLibsAction::OnAnalysisComplete(clang::ASTContext& context) { } }; - auto format_struct_members = [](const FunctionParams& params, const char* indent) { - std::string ret; - for (std::size_t idx = 0; idx < params.param_types.size(); ++idx) { - ret += indent + format_decl(params.param_types[idx].getUnqualifiedType(), fmt::format("a_{}", idx)) + ";\n"; - } - return ret; - }; - auto format_function_params = [](const FunctionParams& params) { std::string ret; for (std::size_t idx = 0; idx < params.param_types.size(); ++idx) { @@ -120,8 +114,8 @@ void GenerateThunkLibsAction::OnAnalysisComplete(clang::ASTContext& context) { return ret; }; - auto get_sha256 = [this](const std::string& function_name) { - std::string sha256_message = libname + ":" + function_name; + auto get_sha256 = [this](const std::string& function_name, bool include_libname) { + std::string sha256_message = (include_libname ? libname + ":" : "") + function_name; std::vector sha256(SHA256_DIGEST_LENGTH); SHA256(reinterpret_cast(sha256_message.data()), sha256_message.size(), @@ -141,22 +135,30 @@ void GenerateThunkLibsAction::OnAnalysisComplete(clang::ASTContext& context) { file << "extern \"C\" {\n"; for (auto& thunk : thunks) { const auto& function_name = thunk.function_name; - auto sha256 = get_sha256(function_name); + auto sha256 = get_sha256(function_name, true); fmt::print( file, "MAKE_THUNK({}, {}, \"{:#02x}\")\n", libname, function_name, fmt::join(sha256, ", ")); } file << "}\n"; // Guest->Host transition points for invoking runtime host-function pointers based on their signature - for (auto type_it = funcptr_types.begin(); type_it != funcptr_types.end(); ++type_it) { - auto* type = *type_it; + std::vector> sha256s; + for (auto type_it = thunked_funcptrs.begin(); type_it != thunked_funcptrs.end(); ++type_it) { + auto* type = type_it->second.first; std::string funcptr_signature = clang::QualType { type, 0 }.getAsString(); - auto cb_sha256 = get_sha256("fexcallback_" + funcptr_signature); + auto cb_sha256 = get_sha256("fexcallback_" + funcptr_signature, false); + auto it = std::find(sha256s.begin(), sha256s.end(), cb_sha256); + if (it != sha256s.end()) { + // TODO: Avoid this ugly way of avoiding duplicates + continue; + } else { + sha256s.push_back(cb_sha256); + } // Thunk used for guest-side calls to host function pointers file << " // " << funcptr_signature << "\n"; - auto funcptr_idx = std::distance(funcptr_types.begin(), type_it); + auto funcptr_idx = std::distance(thunked_funcptrs.begin(), type_it); fmt::print( file, " MAKE_CALLBACK_THUNK(callback_{}, {}, \"{:#02x}\");\n", funcptr_idx, funcptr_signature, fmt::join(cb_sha256, ", ")); } @@ -278,8 +280,10 @@ void GenerateThunkLibsAction::OnAnalysisComplete(clang::ASTContext& context) { auto cb = thunk.callbacks.find(idx); if (cb != thunk.callbacks.end() && cb->second.is_guest) { file << "fex_guest_function_ptr a_" << idx; + } else if (thunk.param_annotations[idx].is_passthrough) { + fmt::print(file, "guest_layout<{}> a_{}", type.getAsString(), idx); } else { - file << format_decl(type, fmt::format("a_{}", idx)); + file << format_decl(type, fmt::format("a_{}", idx)); } } // Using trailing return type as it makes handling function pointer returns much easier @@ -295,21 +299,23 @@ 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) { throw report_error(thunk.decl->getLocation(), "Unsupported parameter type %0").AddTaggedVal(param_type); } } } // Packed argument structs used in fexfn_unpack_* - auto GeneratePackedArgs = [&](const auto &function_name, const auto &thunk) -> std::string { + auto GeneratePackedArgs = [&](const auto &function_name, const ThunkedFunction &thunk) -> std::string { std::string struct_name = "fexfn_packed_args_" + libname + "_" + function_name; file << "struct " << struct_name << " {\n"; - file << format_struct_members(thunk, " "); + for (std::size_t idx = 0; idx < thunk.param_types.size(); ++idx) { + fmt::print(file, " guest_layout<{}> a_{};\n", get_type_name(context, thunk.param_types[idx].getTypePtr()), idx); + } if (!thunk.return_type->isVoidType()) { file << " " << format_decl(thunk.return_type, "rv") << ";\n"; } else if (thunk.param_types.size() == 0) { @@ -336,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 pointee_compat; if (param_type->isPointerType()) { @@ -344,7 +353,12 @@ void GenerateThunkLibsAction::OnAnalysisComplete(clang::ASTContext& context) { pointee_compat = type_compat.emplace(context.getCanonicalType(param_type->getPointeeType().getTypePtr()), TypeCompatibility::Full).first->second; } - if (!param_type->isPointerType() || pointee_compat == TypeCompatibility::Full || + if (thunk.param_annotations[param_idx].is_passthrough) { + // args are passed directly to function, no need to use `unpacked` wrappers + continue; + } + + 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) { @@ -356,18 +370,22 @@ void GenerateThunkLibsAction::OnAnalysisComplete(clang::ASTContext& context) { { auto format_param = [&](std::size_t idx) { + std::string raw_arg = fmt::format("args->a_{}.data", idx); + auto cb = thunk.callbacks.find(idx); if (cb != thunk.callbacks.end() && cb->second.is_stub) { return "fexfn_unpack_" + get_callback_name(function_name, cb->first) + "_stub"; } else if (cb != thunk.callbacks.end() && cb->second.is_guest) { - return fmt::format("fex_guest_function_ptr {{ args->a_{} }}", idx); + return fmt::format("fex_guest_function_ptr {{ {} }}", raw_arg); } else if (cb != thunk.callbacks.end()) { - auto arg_name = fmt::format("args->a_{}", idx); + auto arg_name = fmt::format("args->a_{}.data", idx); // Use comma operator to inject a function call before returning the argument return "(FinalizeHostTrampolineForGuestFunction(" + arg_name + "), " + arg_name + ")"; - - } else { + } else if (thunk.param_annotations[idx].is_passthrough) { + // Pass raw guest_layout return fmt::format("args->a_{}", idx); + } else { + return raw_arg; } }; @@ -382,18 +400,36 @@ void GenerateThunkLibsAction::OnAnalysisComplete(clang::ASTContext& context) { file << "static ExportEntry exports[] = {\n"; for (auto& thunk : thunks) { const auto& function_name = thunk.function_name; - auto sha256 = get_sha256(function_name); + auto sha256 = get_sha256(function_name, true); fmt::print( file, " {{(uint8_t*)\"\\x{:02x}\", (void(*)(void *))&fexfn_unpack_{}_{}}}, // {}:{}\n", fmt::join(sha256, "\\x"), libname, function_name, libname, function_name); } // Endpoints for Guest->Host invocation of runtime host-function pointers - for (auto& type : funcptr_types) { + for (auto& host_funcptr_entry : thunked_funcptrs) { + auto& [type, param_annotations] = host_funcptr_entry.second; std::string mangled_name = clang::QualType { type, 0 }.getAsString(); - auto cb_sha256 = get_sha256("fexcallback_" + mangled_name); - fmt::print( file, " {{(uint8_t*)\"\\x{:02x}\", (void(*)(void *))&CallbackUnpack<{}>::ForIndirectCall}},\n", - fmt::join(cb_sha256, "\\x"), mangled_name); + auto info = LookupGuestFuncPtrInfo(host_funcptr_entry.first.c_str()); + + std::string annotations; + for (int param_idx = 0; param_idx < info.args.size(); ++param_idx) { + if (param_idx != 0) { + annotations += ", "; + } + + annotations += "ParameterAnnotations {"; + 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", + fmt::join(info.sha256, "\\x"), info.result, fmt::join(info.args, ", "), annotations, host_funcptr_entry.first); } + file << " { nullptr, nullptr }\n"; file << "};\n"; diff --git a/ThunkLibs/GuestLibs/CMakeLists.txt b/ThunkLibs/GuestLibs/CMakeLists.txt index 223fcd40f2..d32266516c 100644 --- a/ThunkLibs/GuestLibs/CMakeLists.txt +++ b/ThunkLibs/GuestLibs/CMakeLists.txt @@ -17,7 +17,7 @@ endif() if (CMAKE_CURRENT_SOURCE_DIR STREQUAL CMAKE_SOURCE_DIR) # We've been included using ExternalProject_add, so set up the actual thunk libraries to be cross-compiled - set(CMAKE_CXX_STANDARD 17) + set(CMAKE_CXX_STANDARD 20) # This gets passed in from the main cmake project set (DATA_DIRECTORY "${CMAKE_INSTALL_PREFIX}/share/fex-emu" CACHE PATH "global data directory") diff --git a/ThunkLibs/include/common/GeneratorInterface.h b/ThunkLibs/include/common/GeneratorInterface.h index f3cd082db9..05d8e2f0e5 100644 --- a/ThunkLibs/include/common/GeneratorInterface.h +++ b/ThunkLibs/include/common/GeneratorInterface.h @@ -13,4 +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*. +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 diff --git a/ThunkLibs/include/common/Host.h b/ThunkLibs/include/common/Host.h index f4fa32c414..62b35ef565 100644 --- a/ThunkLibs/include/common/Host.h +++ b/ThunkLibs/include/common/Host.h @@ -103,6 +103,17 @@ struct GuestcallInfo { asm volatile("mov %0, x11" : "=r" (target_variable)) #endif +struct ParameterAnnotations { + bool is_passthrough = false; + bool assume_compatible = false; +}; + +// Placeholder type to indicate the given data is in guest-layout +template +struct guest_layout { + T data; +}; + template struct CallbackUnpack; @@ -120,9 +131,28 @@ struct CallbackUnpack { return packed_args.rv; } } +}; + +template +auto Projection(guest_layout& data) { + if constexpr (Annotation.is_passthrough) { + return data; + } else { + return data.data; + } +} + +template +struct GuestWrapperForHostFunction; - static void ForIndirectCall(void* argsv) { - auto args = reinterpret_cast*>(argsv); +template +struct GuestWrapperForHostFunction { + // Host functions called from Guest + template + static void Call(void* argsv) { + static_assert(sizeof...(Annotations) == sizeof...(Args)); + + auto args = reinterpret_cast..., uintptr_t>*>(argsv); constexpr auto CBIndex = sizeof...(Args); uintptr_t cb; static_assert(CBIndex <= 18 || CBIndex == 23); @@ -168,8 +198,15 @@ struct CallbackUnpack { cb = args->a23; } - auto callback = reinterpret_cast(cb); - Invoke(callback, *args); + // This is almost the same type as "Result func(Args..., uintptr_t)", but + // individual parameters annotated as passthrough are replaced by guest_layout + auto callback = reinterpret_cast, Args>..., uintptr_t)>(cb); + + auto f = [&callback](guest_layout... args, uintptr_t target) -> Result { + // Fold over each of Annotations, Args, and args. This will match up the elements in triplets. + return callback(Projection(args)..., target); + }; + Invoke(f, *args); } }; diff --git a/ThunkLibs/include/common/PackedArguments.h b/ThunkLibs/include/common/PackedArguments.h index 14531db2ac..17af2f41c5 100644 --- a/ThunkLibs/include/common/PackedArguments.h +++ b/ThunkLibs/include/common/PackedArguments.h @@ -113,8 +113,8 @@ T&& operator,(T&& t, Regularize) { return std::forward(t); } -template -void Invoke(Result(*func)(Args...), PackedArguments& args) { +template +void Invoke(Func&& func, PackedArguments& args) requires(std::is_invocable_r_v) { constexpr auto NumArgs = sizeof...(Args); static_assert(NumArgs <= 19 || NumArgs == 24); diff --git a/ThunkLibs/libfex_thunk_test/api.h b/ThunkLibs/libfex_thunk_test/api.h index 293ba29451..daacaacecd 100644 --- a/ThunkLibs/libfex_thunk_test/api.h +++ b/ThunkLibs/libfex_thunk_test/api.h @@ -10,4 +10,22 @@ extern "C" { uint32_t GetDoubledValue(uint32_t); + +/// Interfaces used to test opaque_type and assume_compatible_data_layout annotations + +struct OpaqueType; + +OpaqueType* MakeOpaqueType(uint32_t data); +uint32_t ReadOpaqueTypeData(OpaqueType*); +void DestroyOpaqueType(OpaqueType*); + +union UnionType { + uint32_t a; + int32_t b; + uint8_t c[4]; +}; + +UnionType MakeUnionType(uint8_t a, uint8_t b, uint8_t c, uint8_t d); +uint32_t GetUnionTypeA(UnionType*); + } diff --git a/ThunkLibs/libfex_thunk_test/lib.cpp b/ThunkLibs/libfex_thunk_test/lib.cpp index 2e6354b8e1..7502de1a00 100644 --- a/ThunkLibs/libfex_thunk_test/lib.cpp +++ b/ThunkLibs/libfex_thunk_test/lib.cpp @@ -6,4 +6,28 @@ uint32_t GetDoubledValue(uint32_t input) { return 2 * input; } +struct OpaqueType { + uint32_t data; +}; + +OpaqueType* MakeOpaqueType(uint32_t data) { + return new OpaqueType { data }; +} + +uint32_t ReadOpaqueTypeData(OpaqueType* value) { + return value->data; +} + +void DestroyOpaqueType(OpaqueType* value) { + delete value; +} + +UnionType MakeUnionType(uint8_t a, uint8_t b, uint8_t c, uint8_t d) { + return UnionType { .c = { a, b, c, d } }; +} + +uint32_t GetUnionTypeA(UnionType* value) { + return value->a; +} + } // extern "C" diff --git a/ThunkLibs/libfex_thunk_test/libfex_thunk_test_interface.cpp b/ThunkLibs/libfex_thunk_test/libfex_thunk_test_interface.cpp index 7fedaa415d..8e85d7d4b9 100644 --- a/ThunkLibs/libfex_thunk_test/libfex_thunk_test_interface.cpp +++ b/ThunkLibs/libfex_thunk_test/libfex_thunk_test_interface.cpp @@ -12,3 +12,12 @@ template struct fex_gen_param {}; template<> struct fex_gen_config {}; + +template<> struct fex_gen_type : fexgen::opaque_type {}; +template<> struct fex_gen_config {}; +template<> struct fex_gen_config {}; +template<> struct fex_gen_config {}; + +template<> struct fex_gen_type : fexgen::assume_compatible_data_layout {}; +template<> struct fex_gen_config {}; +template<> struct fex_gen_config {}; diff --git a/ThunkLibs/libvulkan/Host.cpp b/ThunkLibs/libvulkan/Host.cpp index deda37d3d4..8e5b333dc8 100644 --- a/ThunkLibs/libvulkan/Host.cpp +++ b/ThunkLibs/libvulkan/Host.cpp @@ -60,7 +60,7 @@ static VkBool32 DummyVkDebugReportCallback(VkDebugReportFlagsEXT, VkDebugReportO return VK_FALSE; } -static VkResult FEXFN_IMPL(vkCreateInstance)(const VkInstanceCreateInfo* a_0, const VkAllocationCallbacks* a_1, VkInstance* a_2) { +static VkResult FEXFN_IMPL(vkCreateInstance)(const VkInstanceCreateInfo* a_0, const VkAllocationCallbacks* a_1, guest_layout a_2) { const VkInstanceCreateInfo* vk_struct_base = a_0; for (const VkBaseInStructure* vk_struct = reinterpret_cast(vk_struct_base); vk_struct->pNext; vk_struct = vk_struct->pNext) { // Override guest callbacks used for VK_EXT_debug_report @@ -74,11 +74,11 @@ static VkResult FEXFN_IMPL(vkCreateInstance)(const VkInstanceCreateInfo* a_0, co } } - return LDR_PTR(vkCreateInstance)(vk_struct_base, nullptr, a_2); + return LDR_PTR(vkCreateInstance)(vk_struct_base, nullptr, a_2.data); } -static VkResult FEXFN_IMPL(vkCreateDevice)(VkPhysicalDevice a_0, const VkDeviceCreateInfo* a_1, const VkAllocationCallbacks* a_2, VkDevice* a_3){ - return LDR_PTR(vkCreateDevice)(a_0, a_1, nullptr, a_3); +static VkResult FEXFN_IMPL(vkCreateDevice)(VkPhysicalDevice a_0, const VkDeviceCreateInfo* a_1, const VkAllocationCallbacks* a_2, guest_layout a_3){ + return LDR_PTR(vkCreateDevice)(a_0, a_1, nullptr, a_3.data); } static VkResult FEXFN_IMPL(vkAllocateMemory)(VkDevice a_0, const VkMemoryAllocateInfo* a_1, const VkAllocationCallbacks* a_2, VkDeviceMemory* a_3){ diff --git a/ThunkLibs/libvulkan/libvulkan_interface.cpp b/ThunkLibs/libvulkan/libvulkan_interface.cpp index 3bf1671264..abc8d8b86b 100644 --- a/ThunkLibs/libvulkan/libvulkan_interface.cpp +++ b/ThunkLibs/libvulkan/libvulkan_interface.cpp @@ -16,11 +16,16 @@ template<> struct fex_gen_config : fexgen::custom_host_im namespace internal { +// Function, parameter index, parameter type [optional] +template +struct fex_gen_param {}; + template struct fex_gen_config : fexgen::generate_guest_symtable, fexgen::indirect_guest_calls { }; template<> struct fex_gen_config : fexgen::custom_host_impl {}; +template<> struct fex_gen_param : fexgen::ptr_passthrough {}; template<> struct fex_gen_config {}; template<> struct fex_gen_config {}; template<> struct fex_gen_config {}; @@ -30,6 +35,7 @@ template<> struct fex_gen_config {}; template<> struct fex_gen_config {}; template<> struct fex_gen_config {}; template<> struct fex_gen_config : fexgen::custom_host_impl {}; +template<> struct fex_gen_param : fexgen::ptr_passthrough {}; template<> struct fex_gen_config {}; template<> struct fex_gen_config {}; template<> struct fex_gen_config {}; diff --git a/ThunkLibs/libwayland-client/Host.cpp b/ThunkLibs/libwayland-client/Host.cpp index 84ba5e5571..8cfcb4693a 100644 --- a/ThunkLibs/libwayland-client/Host.cpp +++ b/ThunkLibs/libwayland-client/Host.cpp @@ -46,7 +46,7 @@ static void WaylandFinalizeHostTrampolineForGuestListener(void (*callback)()) { } extern "C" int fexfn_impl_libwayland_client_wl_proxy_add_listener(struct wl_proxy *proxy, - void (**callback)(void), void *data) { + guest_layout callback_raw, guest_layout data) { auto guest_interface = ((wl_proxy_private*)proxy)->interface; for (int i = 0; i < guest_interface->event_count; ++i) { @@ -60,99 +60,101 @@ extern "C" int fexfn_impl_libwayland_client_wl_proxy_add_listener(struct wl_prox // ? just indicates that the argument may be null, so it doesn't change the signature signature.erase(std::remove(signature.begin(), signature.end(), '?'), signature.end()); + auto callback = callback_raw.data[i]; + if (signature == "") { // E.g. xdg_toplevel::close - WaylandFinalizeHostTrampolineForGuestListener<>(callback[i]); + WaylandFinalizeHostTrampolineForGuestListener<>(callback); } else if (signature == "a") { // E.g. xdg_toplevel::wm_capabilities - WaylandFinalizeHostTrampolineForGuestListener<'a'>(callback[i]); + WaylandFinalizeHostTrampolineForGuestListener<'a'>(callback); } else if (signature == "hu") { // E.g. zwp_linux_dmabuf_feedback_v1::format_table - WaylandFinalizeHostTrampolineForGuestListener<'h', 'u'>(callback[i]); + WaylandFinalizeHostTrampolineForGuestListener<'h', 'u'>(callback); } else if (signature == "i") { // E.g. wl_output_listener::scale - WaylandFinalizeHostTrampolineForGuestListener<'i'>(callback[i]); + WaylandFinalizeHostTrampolineForGuestListener<'i'>(callback); } else if (signature == "if") { // E.g. wl_touch_listener::orientation - WaylandFinalizeHostTrampolineForGuestListener<'i', 'f'>(callback[i]); + WaylandFinalizeHostTrampolineForGuestListener<'i', 'f'>(callback); } else if (signature == "iff") { // E.g. wl_touch_listener::shape - WaylandFinalizeHostTrampolineForGuestListener<'i', 'f', 'f'>(callback[i]); + WaylandFinalizeHostTrampolineForGuestListener<'i', 'f', 'f'>(callback); } else if (signature == "ii") { // E.g. xdg_toplevel::configure_bounds - WaylandFinalizeHostTrampolineForGuestListener<'i', 'i'>(callback[i]); + WaylandFinalizeHostTrampolineForGuestListener<'i', 'i'>(callback); } else if (signature == "iia") { // E.g. xdg_toplevel::configure - WaylandFinalizeHostTrampolineForGuestListener<'i', 'i', 'a'>(callback[i]); + WaylandFinalizeHostTrampolineForGuestListener<'i', 'i', 'a'>(callback); } else if (signature == "iiiiissi") { // E.g. wl_output_listener::geometry - WaylandFinalizeHostTrampolineForGuestListener<'i', 'i', 'i', 'i', 'i', 's', 's', 'i'>(callback[i]); + WaylandFinalizeHostTrampolineForGuestListener<'i', 'i', 'i', 'i', 'i', 's', 's', 'i'>(callback); } else if (signature == "n") { // E.g. wl_data_device_listener::data_offer - WaylandFinalizeHostTrampolineForGuestListener<'n'>(callback[i]); + WaylandFinalizeHostTrampolineForGuestListener<'n'>(callback); } else if (signature == "o") { // E.g. wl_data_device_listener::selection - WaylandFinalizeHostTrampolineForGuestListener<'o'>(callback[i]); + WaylandFinalizeHostTrampolineForGuestListener<'o'>(callback); } else if (signature == "u") { // E.g. wl_registry::global_remove - WaylandFinalizeHostTrampolineForGuestListener<'u'>(callback[i]); + WaylandFinalizeHostTrampolineForGuestListener<'u'>(callback); } else if (signature == "uff") { // E.g. wl_pointer_listener::motion - WaylandFinalizeHostTrampolineForGuestListener<'u', 'f', 'f'>(callback[i]); + WaylandFinalizeHostTrampolineForGuestListener<'u', 'f', 'f'>(callback); } else if (signature == "uhu") { // E.g. wl_keyboard_listener::keymap - WaylandFinalizeHostTrampolineForGuestListener<'u', 'h', 'u'>(callback[i]); + WaylandFinalizeHostTrampolineForGuestListener<'u', 'h', 'u'>(callback); } else if (signature == "ui") { // E.g. wl_pointer_listener::axis_discrete - WaylandFinalizeHostTrampolineForGuestListener<'u', 'i'>(callback[i]); + WaylandFinalizeHostTrampolineForGuestListener<'u', 'i'>(callback); } else if (signature == "uiff") { // E.g. wl_touch_listener::motion - WaylandFinalizeHostTrampolineForGuestListener<'u', 'i', 'f', 'f'>(callback[i]); + WaylandFinalizeHostTrampolineForGuestListener<'u', 'i', 'f', 'f'>(callback); } else if (signature == "uiii") { // E.g. wl_output_listener::mode - WaylandFinalizeHostTrampolineForGuestListener<'u', 'i', 'i', 'i'>(callback[i]); + WaylandFinalizeHostTrampolineForGuestListener<'u', 'i', 'i', 'i'>(callback); } else if (signature == "uo") { // E.g. wl_pointer_listener::leave - WaylandFinalizeHostTrampolineForGuestListener<'u', 'o'>(callback[i]); + WaylandFinalizeHostTrampolineForGuestListener<'u', 'o'>(callback); } else if (signature == "uoa") { // E.g. wl_keyboard_listener::enter - WaylandFinalizeHostTrampolineForGuestListener<'u', 'o', 'a'>(callback[i]); + WaylandFinalizeHostTrampolineForGuestListener<'u', 'o', 'a'>(callback); } else if (signature == "uoff") { // E.g. wl_pointer_listener::enter - WaylandFinalizeHostTrampolineForGuestListener<'u', 'o', 'f', 'f'>(callback[i]); + WaylandFinalizeHostTrampolineForGuestListener<'u', 'o', 'f', 'f'>(callback); } else if (signature == "uoffo") { // E.g. wl_data_device_listener::enter - WaylandFinalizeHostTrampolineForGuestListener<'u', 'o', 'f', 'f', 'o'>(callback[i]); + WaylandFinalizeHostTrampolineForGuestListener<'u', 'o', 'f', 'f', 'o'>(callback); } else if (signature == "usu") { // E.g. wl_registry::global - WaylandFinalizeHostTrampolineForGuestListener<'u', 's', 'u'>(callback[i]); + WaylandFinalizeHostTrampolineForGuestListener<'u', 's', 'u'>(callback); } else if (signature == "uu") { // E.g. wl_pointer_listener::axis_stop - WaylandFinalizeHostTrampolineForGuestListener<'u', 'u'>(callback[i]); + WaylandFinalizeHostTrampolineForGuestListener<'u', 'u'>(callback); } else if (signature == "uuf") { // E.g. wl_pointer_listener::axis - WaylandFinalizeHostTrampolineForGuestListener<'u', 'u', 'f'>(callback[i]); + WaylandFinalizeHostTrampolineForGuestListener<'u', 'u', 'f'>(callback); } else if (signature == "uui") { // E.g. wl_touch_listener::up - WaylandFinalizeHostTrampolineForGuestListener<'u', 'u', 'i'>(callback[i]); + WaylandFinalizeHostTrampolineForGuestListener<'u', 'u', 'i'>(callback); } else if (signature == "uuoiff") { // E.g. wl_touch_listener::down - WaylandFinalizeHostTrampolineForGuestListener<'u', 'u', 'o', 'i', 'f', 'f'>(callback[i]); + WaylandFinalizeHostTrampolineForGuestListener<'u', 'u', 'o', 'i', 'f', 'f'>(callback); } else if (signature == "uuu") { // E.g. zwp_linux_dmabuf_v1::modifier - WaylandFinalizeHostTrampolineForGuestListener<'u', 'u', 'u'>(callback[i]); + WaylandFinalizeHostTrampolineForGuestListener<'u', 'u', 'u'>(callback); } else if (signature == "uuuu") { // E.g. wl_pointer_listener::button - WaylandFinalizeHostTrampolineForGuestListener<'u', 'u', 'u', 'u'>(callback[i]); + WaylandFinalizeHostTrampolineForGuestListener<'u', 'u', 'u', 'u'>(callback); } else if (signature == "uuuuu") { // E.g. wl_keyboard_listener::modifiers - WaylandFinalizeHostTrampolineForGuestListener<'u', 'u', 'u', 'u', 'u'>(callback[i]); + WaylandFinalizeHostTrampolineForGuestListener<'u', 'u', 'u', 'u', 'u'>(callback); } else if (signature == "s") { // E.g. wl_seat::name - WaylandFinalizeHostTrampolineForGuestListener<'s'>(callback[i]); + WaylandFinalizeHostTrampolineForGuestListener<'s'>(callback); } else if (signature == "sii") { // E.g. zwp_text_input_v3::preedit_string - WaylandFinalizeHostTrampolineForGuestListener<'s', 'i', 'i'>(callback[i]); + WaylandFinalizeHostTrampolineForGuestListener<'s', 'i', 'i'>(callback); } else { fprintf(stderr, "TODO: Unknown wayland event signature descriptor %s\n", signature.data()); std::abort(); @@ -160,7 +162,7 @@ extern "C" int fexfn_impl_libwayland_client_wl_proxy_add_listener(struct wl_prox } // Pass the original function pointer table to the host wayland library. This ensures the table is valid until the listener is unregistered. - return fexldr_ptr_libwayland_client_wl_proxy_add_listener(proxy, callback, data); + return fexldr_ptr_libwayland_client_wl_proxy_add_listener(proxy, callback_raw.data, data.data); } wl_interface* fexfn_impl_libwayland_client_fex_wl_exchange_interface_pointer(wl_interface* guest_interface, char const* name) { diff --git a/ThunkLibs/libwayland-client/libwayland-client_interface.cpp b/ThunkLibs/libwayland-client/libwayland-client_interface.cpp index 7371653bc7..32fe6ba374 100644 --- a/ThunkLibs/libwayland-client/libwayland-client_interface.cpp +++ b/ThunkLibs/libwayland-client/libwayland-client_interface.cpp @@ -10,6 +10,11 @@ struct fex_gen_config { template struct fex_gen_type {}; +// Function, parameter index, parameter type [optional] +template +struct fex_gen_param {}; + + template<> struct fex_gen_config : fexgen::custom_guest_entrypoint {}; template<> struct fex_gen_config {}; @@ -31,6 +36,8 @@ template<> struct fex_gen_config {}; template<> struct fex_gen_config {}; template<> struct fex_gen_config : fexgen::custom_host_impl, fexgen::custom_guest_entrypoint {}; +template<> struct fex_gen_param : fexgen::ptr_passthrough {}; +template<> struct fex_gen_param : fexgen::ptr_passthrough {}; template<> struct fex_gen_config {}; template<> struct fex_gen_config {}; template<> struct fex_gen_config {}; diff --git a/unittests/FEXLinuxTests/tests/thunks/thunk_testlib.64.cpp b/unittests/FEXLinuxTests/tests/thunks/thunk_testlib.64.cpp index 4fc742db2f..7c17ad7556 100644 --- a/unittests/FEXLinuxTests/tests/thunks/thunk_testlib.64.cpp +++ b/unittests/FEXLinuxTests/tests/thunks/thunk_testlib.64.cpp @@ -17,8 +17,28 @@ struct Fixture { #define GET_SYMBOL(name) decltype(&::name) name = (decltype(name))dlsym(lib, #name) GET_SYMBOL(GetDoubledValue); + + GET_SYMBOL(MakeOpaqueType); + GET_SYMBOL(ReadOpaqueTypeData); + GET_SYMBOL(DestroyOpaqueType); + + GET_SYMBOL(MakeUnionType); + GET_SYMBOL(GetUnionTypeA); }; TEST_CASE_METHOD(Fixture, "Trivial") { CHECK(GetDoubledValue(10) == 20); } + +TEST_CASE_METHOD(Fixture, "Opaque data types") { + { + auto data = MakeOpaqueType(0x1234); + CHECK(ReadOpaqueTypeData(data) == 0x1234); + DestroyOpaqueType(data); + } + + { + auto data = MakeUnionType(0x1, 0x2, 0x3, 0x4); + CHECK(GetUnionTypeA(&data) == 0x04030201); + } +} diff --git a/unittests/ThunkLibs/abi.cpp b/unittests/ThunkLibs/abi.cpp index ca9b193c5b..0e59101eaf 100644 --- a/unittests/ThunkLibs/abi.cpp +++ b/unittests/ThunkLibs/abi.cpp @@ -464,6 +464,21 @@ TEST_CASE_METHOD(Fixture, "DataLayout") { "template<> struct fex_gen_type {};\n", guest_abi), Catch::Contains("unannotated member") && Catch::Contains("union type")); } + + SECTION("with annotation") { + auto action = compute_data_layout( + "#include \n" + "#include \n", + "union B { int32_t a; uint32_t b; };\n" + "struct A { B a; };\n" + "template<> struct fex_gen_type : fexgen::assume_compatible_data_layout {};\n" + "template<> struct fex_gen_type {};\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 \n" + "#include \n", + "union B { int32_t a; uint32_t b; };\n" + "struct A { B* a; };\n" + "template<> struct fex_gen_type : fexgen::assume_compatible_data_layout {};\n" + "template<> struct fex_gen_type {};\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 \n" + "#include \n", + "struct B;\n" + "struct A { B* a; };\n" + "template<> struct fex_gen_type : fexgen::opaque_type {};\n" + "template<> struct fex_gen_type {};\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 \n" + "#include \n", + "struct A { A* a; };\n" + "template<> struct fex_gen_type : 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 \n" + "#include \n", + "struct B;\n" + "struct A { B* a; };\n" + "struct B { A* a; };\n" + "template<> struct fex_gen_type : 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") { diff --git a/unittests/ThunkLibs/common.h b/unittests/ThunkLibs/common.h index 6fe13022eb..05ae8513d4 100644 --- a/unittests/ThunkLibs/common.h +++ b/unittests/ThunkLibs/common.h @@ -64,7 +64,7 @@ inline std::ostream& operator<<(std::ostream& os, GuestABI abi) { inline void run_tool(clang::tooling::ToolAction& action, std::string_view code, bool silent = false, std::optional guest_abi = std::nullopt) { const char* memory_filename = "gen_input.cpp"; auto adjuster = clang::tooling::getClangStripDependencyFileAdjuster(); - std::vector args = { "clang-tool", "-fsyntax-only", "-std=c++17", "-Werror", "-I.", memory_filename }; + std::vector args = { "clang-tool", "-fsyntax-only", "-std=c++20", "-Werror", "-I.", memory_filename }; if (CLANG_RESOURCE_DIR[0] != 0) { args.push_back("-resource-dir"); args.push_back(CLANG_RESOURCE_DIR); @@ -90,8 +90,16 @@ struct custom_host_impl {}; 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 +template struct fex_gen_param {}; + template struct fex_gen_type; template diff --git a/unittests/ThunkLibs/generator.cpp b/unittests/ThunkLibs/generator.cpp index 7144062ae3..099f71c44a 100644 --- a/unittests/ThunkLibs/generator.cpp +++ b/unittests/ThunkLibs/generator.cpp @@ -248,8 +248,10 @@ SourceWithAST Fixture::run_thunkgen_host(std::string_view prelude, std::string_v run_tool(std::make_unique(libname, output_filenames, data_layout), full_code, silent); std::string result = + "#include \n" "#include \n" "#include \n" + "#include \n" "template\n" "struct function_traits;\n" "template\n" @@ -270,14 +272,19 @@ SourceWithAST Fixture::run_thunkgen_host(std::string_view prelude, std::string_v " uintptr_t GuestUnpacker;\n" " uintptr_t GuestTarget;\n" "};\n" + "struct ParameterAnnotations {};\n" "template\n" - "struct CallbackUnpack {\n" - " static void ForIndirectCall(void* argsv);\n" + "struct GuestWrapperForHostFunction {\n" + " template static void Call(void*);\n" "};\n" - "template\n" - "void FinalizeHostTrampolineForGuestFunction(F*);\n" "struct ExportEntry { uint8_t* sha256; void(*fn)(void *); };\n" - "void *dlsym_default(void* handle, const char* symbol);\n"; + "void *dlsym_default(void* handle, const char* symbol);\n" + "template\n" + "struct guest_layout {\n" + " T data;\n" + "};\n" + "\n" + "template void FinalizeHostTrampolineForGuestFunction(F*);\n"; auto& filename = output_filenames.host; { @@ -377,7 +384,7 @@ TEST_CASE_METHOD(Fixture, "FunctionPointerViaType") { matches(varDecl( hasName("exports"), hasType(constantArrayType(hasElementType(asString("struct ExportEntry")), hasSize(2))), - hasInitializer(hasDescendant(declRefExpr(to(cxxMethodDecl(hasName("ForIndirectCall"), ofClass(hasName("CallbackUnpack"))).bind("funcptr"))))) + hasInitializer(hasDescendant(declRefExpr(to(cxxMethodDecl(hasName("Call"), ofClass(hasName("GuestWrapperForHostFunction"))).bind("funcptr"))))) )).check_binding("funcptr", +[](const clang::CXXMethodDecl* decl) { auto parent = llvm::cast(decl->getParent()); return parent->getTemplateArgs().get(0).getAsType().getAsString() == "int (char, char)"; @@ -415,7 +422,7 @@ TEST_CASE_METHOD(Fixture, "FunctionPointerParameter") { matches(varDecl( hasName("exports"), hasType(constantArrayType(hasElementType(asString("struct ExportEntry")), hasSize(3))), - hasInitializer(hasDescendant(declRefExpr(to(cxxMethodDecl(hasName("ForIndirectCall"), ofClass(hasName("CallbackUnpack"))))))) + hasInitializer(hasDescendant(declRefExpr(to(cxxMethodDecl(hasName("Call"), ofClass(hasName("GuestWrapperForHostFunction"))))))) ))); } @@ -485,10 +492,10 @@ TEST_CASE_METHOD(Fixture, "MultipleParameters") { parameterCountIs(1), hasParameter(0, hasType(pointerType(pointee( recordType(hasDeclaration(decl( - has(fieldDecl(hasType(asString("int")))), - has(fieldDecl(hasType(asString("char")))), - has(fieldDecl(hasType(asString("unsigned long")))), - has(fieldDecl(hasType(asString("struct TestStruct")))) + has(fieldDecl(hasType(asString("guest_layout")))), + has(fieldDecl(hasType(asString("guest_layout")))), + has(fieldDecl(hasType(asString("guest_layout")))), + has(fieldDecl(hasType(asString("guest_layout")))) ))))))) ))); } @@ -563,6 +570,10 @@ TEST_CASE_METHOD(Fixture, "StructRepacking") { SECTION("Parameter unannotated") { CHECK_THROWS(run_thunkgen_host(prelude, code, guest_abi, true)); } + + SECTION("Parameter annotated as ptr_passthrough") { + CHECK_NOTHROW(run_thunkgen_host(prelude, code + "template<> struct fex_gen_param : fexgen::ptr_passthrough {};\n", guest_abi)); + } } SECTION("Pointer to struct with pointer member of opaque type") { @@ -593,6 +604,24 @@ TEST_CASE_METHOD(Fixture, "VoidPointerParameter") { } } + SECTION("Passthrough") { + const char* code = + "#include \n" + "void func(void*);\n" + "template<> struct fex_gen_config : fexgen::custom_host_impl {};\n" + "template<> struct fex_gen_param : fexgen::ptr_passthrough {};\n"; + CHECK_NOTHROW(run_thunkgen_host("", code, guest_abi)); + } + + SECTION("Assumed compatible") { + const char* code = + "#include \n" + "void func(void*);\n" + "template<> struct fex_gen_config {};\n" + "template<> struct fex_gen_param : 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";