#include #include #include #include #include #include #include #include #include #include "common.h" /** * This class parses its input code and stores it alongside its AST representation. * * Use this with HasASTMatching in Catch2's CHECK_THAT/REQUIRE_THAT macros. */ struct SourceWithAST { std::string code; std::unique_ptr ast; SourceWithAST(std::string_view input); }; std::ostream& operator<<(std::ostream& os, const SourceWithAST& ast) { os << ast.code; // Additionally, change this to true to print the full AST on test failures const bool print_ast = false; if (print_ast) { for (auto it = ast.ast->top_level_begin(); it != ast.ast->top_level_end(); ++it) { // Skip header declarations if (!ast.ast->isInMainFileID((*it)->getBeginLoc())) { continue; } auto llvm_os = llvm::raw_os_ostream { os }; (*it)->dump(llvm_os); } } return os; } struct Fixture { Fixture() { tmpdir = std::string { P_tmpdir } + "/thunkgentestXXXXXX"; if (!mkdtemp(tmpdir.data())) { std::abort(); } std::filesystem::create_directory(tmpdir); output_filenames = { tmpdir + "/thunkgen_guest", tmpdir + "/thunkgen_host", }; } ~Fixture() { std::filesystem::remove_all(tmpdir); } struct GenOutput { SourceWithAST guest; SourceWithAST host; }; /** * Runs the given given code through the thunk generator and verifies the output compiles. * * Input code with common definitions (types, functions, ...) should be specified in "prelude". * It will be prepended to "code" before processing and also to the generator output. */ SourceWithAST run_thunkgen_guest(std::string_view prelude, std::string_view code, bool silent = false); SourceWithAST run_thunkgen_host(std::string_view prelude, std::string_view code, bool silent = false); GenOutput run_thunkgen(std::string_view prelude, std::string_view code, bool silent = false); const std::string libname = "libtest"; std::string tmpdir; OutputFilenames output_filenames; }; using namespace clang::ast_matchers; class MatchCallback : public MatchFinder::MatchCallback { bool success = false; using CheckFn = std::function; std::vector binding_checks; public: template void check_binding(std::string_view binding_name, bool (*check_fn)(const NodeType*)) { // Decorate the given check with node extraction and wrap it in a type-erased interface binding_checks.push_back( [check_fn, binding_name = std::string(binding_name)](const MatchFinder::MatchResult& result) { if (auto node = result.Nodes.getNodeAs(binding_name.c_str())) { return check_fn(node); } return false; }); } void run(const MatchFinder::MatchResult& result) override { success = true; // NOTE: If there are no callbacks, this signals that the match was found at all for (auto& binding_check : binding_checks) { success = success && binding_check(result); } } bool matched() const noexcept { return success; } }; /** * This class connects the libclang AST to Catch2 test matchers, allowing for * code compiled via SourceWithAST objects to be pattern-matched using the * libclang ASTMatcher API. */ template class HasASTMatching : public Catch::MatcherBase { ClangMatcher matcher; MatchCallback callback; public: HasASTMatching(const ClangMatcher& matcher_) : matcher(matcher_) { } template HasASTMatching& check_binding(std::string_view binding_name, bool (*check_fn)(const NodeT*)) { callback.check_binding(binding_name, check_fn); return *this; } bool match(const SourceWithAST& code) const override { MatchCallback result = callback; clang::ast_matchers::MatchFinder finder; finder.addMatcher(matcher, &result); finder.matchAST(code.ast->getASTContext()); return result.matched(); } std::string describe() const override { std::ostringstream ss; ss << "should compile and match the given AST pattern"; return ss.str(); } }; HasASTMatching matches(const DeclarationMatcher& matcher_) { return HasASTMatching(matcher_); } HasASTMatching matches(const StatementMatcher& matcher_) { return HasASTMatching(matcher_); } /** * Catch matcher that checks if a tested C++ source defines a function with the given name */ class DefinesPublicFunction : public HasASTMatching { std::string function_name; public: DefinesPublicFunction(std::string_view name) : HasASTMatching(functionDecl(hasName(name))), function_name(name) { } std::string describe() const override { std::ostringstream ss; ss << "should define and export a function called \"" + function_name + "\""; return ss.str(); } }; SourceWithAST::SourceWithAST(std::string_view input) : code(input) { // Call run_tool with a ToolAction that assigns this->ast struct ToolAction : clang::tooling::ToolAction { std::unique_ptr& ast; ToolAction(std::unique_ptr& ast_) : ast(ast_) { } bool runInvocation(std::shared_ptr invocation, clang::FileManager* files, std::shared_ptr pch, clang::DiagnosticConsumer *diag_consumer) override { auto diagnostics = clang::CompilerInstance::createDiagnostics(&invocation->getDiagnosticOpts(), diag_consumer, false); ast = clang::ASTUnit::LoadFromCompilerInvocation(invocation, std::move(pch), std::move(diagnostics), files); return (ast != nullptr); } } tool_action { ast }; run_tool(tool_action, code); } /** * Generates guest thunk library code from the given input */ SourceWithAST Fixture::run_thunkgen_guest(std::string_view prelude, std::string_view code, bool silent) { const std::string full_code = std::string { prelude } + std::string { code }; run_tool(std::make_unique(libname, output_filenames), full_code, silent); std::string result = "#include \n" "#define MAKE_THUNK(lib, name, hash) extern \"C\" int fexthunks_##lib##_##name(void*);\n" "template\n" "struct callback_thunk_defined;\n" "#define MAKE_CALLBACK_THUNK(name, sig, hash) template<> struct callback_thunk_defined {};\n" "#define FEX_PACKFN_LINKAGE\n" "template\n" "Target *MakeHostTrampolineForGuestFunction(uint8_t HostPacker[32], void (*)(uintptr_t, void*), Target*);\n" "template\n" "Target *AllocateHostTrampolineForGuestFunction(Target*);\n"; const auto& filename = output_filenames.guest; { std::ifstream file(filename); const auto current_size = result.size(); const auto new_data_size = std::filesystem::file_size(filename); result.resize(result.size() + new_data_size); file.read(result.data() + current_size, result.size()); } return SourceWithAST { std::string { prelude } + result }; } /** * Generates host thunk library code from the given input */ SourceWithAST Fixture::run_thunkgen_host(std::string_view prelude, std::string_view code, bool silent) { const std::string full_code = std::string { prelude } + std::string { code }; run_tool(std::make_unique(libname, output_filenames), full_code, silent); std::string result = "#include \n" "#include \n" "template\n" "struct function_traits;\n" "template\n" "struct function_traits {\n" " using result_t = Result;\n" " using arg_t = Arg;\n" "};\n" "template\n" "static typename function_traits::result_t\n" "fexfn_type_erased_unpack(void* argsv) {\n" " using args_t = typename function_traits::arg_t;\n" " return Fn(reinterpret_cast(argsv));\n" "}\n" "#define LOAD_INTERNAL_GUESTPTR_VIA_CUSTOM_ABI(arg)\n" "struct GuestcallInfo {\n" " uintptr_t HostPacker;\n" " void (*CallCallback)(uintptr_t, uintptr_t, void*);\n" " uintptr_t GuestUnpacker;\n" " uintptr_t GuestTarget;\n" "};\n" "template\n" "struct CallbackUnpack {\n" " static void ForIndirectCall(void* argsv);\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"; auto& filename = output_filenames.host; { std::ifstream file(filename); const auto current_size = result.size(); const auto new_data_size = std::filesystem::file_size(filename); result.resize(result.size() + new_data_size); file.read(result.data() + current_size, result.size()); } return SourceWithAST { std::string { prelude } + result }; } Fixture::GenOutput Fixture::run_thunkgen(std::string_view prelude, std::string_view code, bool silent) { return { run_thunkgen_guest(prelude, code, silent), run_thunkgen_host(prelude, code, silent) }; } TEST_CASE_METHOD(Fixture, "Trivial") { const auto output = run_thunkgen("", "#include \n" "void func();\n" "template struct fex_gen_config {};\n" "template<> struct fex_gen_config {};\n"); // Guest code CHECK_THAT(output.guest, DefinesPublicFunction("func")); CHECK_THAT(output.guest, matches(functionDecl( hasName("fexfn_pack_func"), returns(asString("void")), parameterCountIs(0) ))); // Host code CHECK_THAT(output.host, matches(varDecl( hasName("exports"), hasType(constantArrayType(hasElementType(asString("struct ExportEntry")), hasSize(2))), hasInitializer(initListExpr(hasInit(0, expr()), hasInit(1, initListExpr(hasInit(0, implicitCastExpr()), hasInit(1, implicitCastExpr()))))) // TODO: check null termination ))); } // Unknown annotations trigger an error TEST_CASE_METHOD(Fixture, "UnknownAnnotation") { REQUIRE_THROWS(run_thunkgen("void func();\n", "struct invalid_annotation {};\n" "template struct fex_gen_config {};\n" "template<> struct fex_gen_config : invalid_annotation {};\n", true)); REQUIRE_THROWS(run_thunkgen("void func();\n", "template struct fex_gen_config {};\n" "template<> struct fex_gen_config { int invalid_field_annotation; };\n", true)); } TEST_CASE_METHOD(Fixture, "VersionedLibrary") { const auto output = run_thunkgen_host("", "template struct fex_gen_config { int version = 123; };\n"); CHECK_THAT(output, matches(callExpr( callee(functionDecl(hasName("dlopen"))), hasArgument(0, stringLiteral().bind("libname")) )) .check_binding("libname", +[](const clang::StringLiteral* lit) { return lit->getString().endswith(".so.123"); })); } TEST_CASE_METHOD(Fixture, "FunctionPointerViaType") { const auto output = run_thunkgen("", "template struct fex_gen_type {};\n" "template<> struct fex_gen_type {};\n"); // Guest should apply MAKE_CALLBACK_THUNK to this signature CHECK_THAT(output.guest, matches(classTemplateSpecializationDecl( // Should have signature matching input function hasName("callback_thunk_defined"), hasTemplateArgument(0, refersToType(asString("int (char, char)"))) ))); // Host should export the unpacking function for callback arguments CHECK_THAT(output.host, matches(varDecl( hasName("exports"), hasType(constantArrayType(hasElementType(asString("struct ExportEntry")), hasSize(2))), hasInitializer(hasDescendant(declRefExpr(to(cxxMethodDecl(hasName("ForIndirectCall"), ofClass(hasName("CallbackUnpack"))).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)"; })); } // Parameter is a function pointer TEST_CASE_METHOD(Fixture, "FunctionPointerParameter") { const auto output = run_thunkgen("", "void func(int (*funcptr)(char, char));\n" "template struct fex_gen_config {};\n" "template<> struct fex_gen_config {};\n"); CHECK_THAT(output.guest, matches(functionDecl( // Should have signature matching input function hasName("fexfn_pack_func"), returns(asString("void")), parameterCountIs(1), hasParameter(0, hasType(asString("int (*)(char, char)"))) ))); // Host packing function should call FinalizeHostTrampolineForGuestFunction on the argument CHECK_THAT(output.host, matches(functionDecl( hasName("fexfn_unpack_libtest_func"), hasDescendant(callExpr(callee(functionDecl(hasName("FinalizeHostTrampolineForGuestFunction"))), hasArgument(0, expr().bind("funcptr")))) )).check_binding("funcptr", +[](const clang::Expr* funcptr) { // Check that the argument type matches the function pointer return funcptr->getType().getAsString() == "int (*)(char, char)"; })); // Host should export the unpacking function for function pointer arguments CHECK_THAT(output.host, matches(varDecl( hasName("exports"), hasType(constantArrayType(hasElementType(asString("struct ExportEntry")), hasSize(3))), hasInitializer(hasDescendant(declRefExpr(to(cxxMethodDecl(hasName("ForIndirectCall"), ofClass(hasName("CallbackUnpack"))))))) ))); } // Parameter is a guest function pointer TEST_CASE_METHOD(Fixture, "GuestFunctionPointerParameter") { const std::string prelude = "struct fex_guest_function_ptr { int (*x)(char,char); };\n" "static void fexfn_impl_libtest_func(fex_guest_function_ptr) {}\n"; const auto output = run_thunkgen(prelude, "#include \n" "void func(int (*funcptr)(char, char));\n" "template struct fex_gen_config {};\n" "template<> struct fex_gen_config : fexgen::callback_guest, fexgen::custom_host_impl {};\n"); CHECK_THAT(output.guest, matches(functionDecl( hasName("fexfn_pack_func"), returns(asString("void")), parameterCountIs(1), hasParameter(0, hasType(asString("int (*)(char, char)"))) ))); // Host-side implementation only sees an opaque type that it can't call CHECK_THAT(output.host, matches(callExpr(callee(functionDecl(hasName("fexfn_impl_libtest_func"))), hasArgument(0, hasType(asString("struct fex_guest_function_ptr"))) ))); } TEST_CASE_METHOD(Fixture, "MultipleParameters") { const std::string prelude = "struct TestStruct { int member; };\n"; auto output = run_thunkgen(prelude, "void func(int arg, char, unsigned long, TestStruct);\n" "template struct fex_gen_config {};\n" "template<> struct fex_gen_config {};\n"); // Guest code CHECK_THAT(output.guest, DefinesPublicFunction("func")); CHECK_THAT(output.guest, matches(functionDecl( hasName("fexfn_pack_func"), returns(asString("void")), parameterCountIs(4), hasParameter(0, hasType(asString("int"))), hasParameter(1, hasType(asString("char"))), hasParameter(2, hasType(asString("unsigned long"))), hasParameter(3, hasType(asString("struct TestStruct"))) ))); // Host code CHECK_THAT(output.host, matches(varDecl( hasName("exports"), hasType(constantArrayType(hasElementType(asString("struct ExportEntry")), hasSize(2))), hasInitializer(initListExpr(hasInit(0, expr()), hasInit(1, initListExpr(hasInit(0, implicitCastExpr()), hasInit(1, implicitCastExpr()))))) // TODO: check null termination ))); CHECK_THAT(output.host, matches(functionDecl( hasName("fexfn_unpack_libtest_func"), // Packed argument struct should contain all parameters 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")))) ))))))) ))); } // Returning a function pointer should trigger an error unless an annotation is provided TEST_CASE_METHOD(Fixture, "ReturnFunctionPointer") { const std::string prelude = "using funcptr = void (*)(char, char);\n"; REQUIRE_THROWS(run_thunkgen_guest(prelude, "funcptr func(int);\n" "template struct fex_gen_config {};\n" "template<> struct fex_gen_config {};\n", true)); REQUIRE_NOTHROW(run_thunkgen_guest(prelude, "#include \n" "funcptr func(int);\n" "template struct fex_gen_config {};\n" "template<> struct fex_gen_config : fexgen::returns_guest_pointer {};\n")); } TEST_CASE_METHOD(Fixture, "VariadicFunction") { const std::string prelude = "void func(int arg, ...);\n"; const auto output = run_thunkgen_guest(prelude, "template struct fex_gen_config {};\n" "template<> struct fex_gen_config {\n" " using uniform_va_type = char;\n" "};\n"); CHECK_THAT(output, matches(functionDecl( hasName("fexfn_pack_func_internal"), returns(asString("void")), parameterCountIs(3), hasParameter(0, hasType(asString("int"))), hasParameter(1, hasType(asString("unsigned long"))), hasParameter(2, hasType(pointerType(pointee(asString("char"))))) ))); } // Variadic functions without annotation trigger an error TEST_CASE_METHOD(Fixture, "VariadicFunctionsWithoutAnnotation") { REQUIRE_THROWS(run_thunkgen_guest("void func(int arg, ...);\n", "template struct fex_gen_config {};\n" "template<> struct fex_gen_config {};\n", true)); }