Files
FEX-Emu--FEX/unittests/ThunkLibs/generator.cpp
T

514 lines
19 KiB
C++

#include <catch2/catch.hpp>
#include <clang/ASTMatchers/ASTMatchers.h>
#include <clang/ASTMatchers/ASTMatchFinder.h>
#include <clang/Frontend/CompilerInstance.h>
#include <clang/Tooling/Tooling.h>
#include <interface.h>
#include <filesystem>
#include <fstream>
#include <string_view>
#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<clang::ASTUnit> 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<bool(const MatchFinder::MatchResult&)>;
std::vector<CheckFn> binding_checks;
public:
template<typename NodeType>
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<NodeType>(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<typename ClangMatcher>
class HasASTMatching : public Catch::MatcherBase<SourceWithAST> {
ClangMatcher matcher;
MatchCallback callback;
public:
HasASTMatching(const ClangMatcher& matcher_) : matcher(matcher_) {
}
template<typename NodeT>
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<DeclarationMatcher> matches(const DeclarationMatcher& matcher_) {
return HasASTMatching<DeclarationMatcher>(matcher_);
}
HasASTMatching<StatementMatcher> matches(const StatementMatcher& matcher_) {
return HasASTMatching<StatementMatcher>(matcher_);
}
/**
* Catch matcher that checks if a tested C++ source defines a function with the given name
*/
class DefinesPublicFunction : public HasASTMatching<DeclarationMatcher> {
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<clang::ASTUnit>& ast;
ToolAction(std::unique_ptr<clang::ASTUnit>& ast_) : ast(ast_) { }
bool runInvocation(std::shared_ptr<clang::CompilerInvocation> invocation,
clang::FileManager* files,
std::shared_ptr<clang::PCHContainerOperations> 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<GenerateThunkLibsActionFactory>(libname, output_filenames), full_code, silent);
std::string result =
"#include <cstdint>\n"
"#define MAKE_THUNK(lib, name, hash) extern \"C\" int fexthunks_##lib##_##name(void*);\n"
"template<typename>\n"
"struct callback_thunk_defined;\n"
"#define MAKE_CALLBACK_THUNK(name, sig, hash) template<> struct callback_thunk_defined<sig> {};\n"
"#define FEX_PACKFN_LINKAGE\n"
"template<typename Target>\n"
"Target *MakeHostTrampolineForGuestFunction(uint8_t HostPacker[32], void (*)(uintptr_t, void*), Target*);\n"
"template<typename Target>\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<GenerateThunkLibsActionFactory>(libname, output_filenames), full_code, silent);
std::string result =
"#include <cstdint>\n"
"#include <dlfcn.h>\n"
"template<typename Fn>\n"
"struct function_traits;\n"
"template<typename Result, typename Arg>\n"
"struct function_traits<Result(*)(Arg)> {\n"
" using result_t = Result;\n"
" using arg_t = Arg;\n"
"};\n"
"template<auto Fn>\n"
"static typename function_traits<decltype(Fn)>::result_t\n"
"fexfn_type_erased_unpack(void* argsv) {\n"
" using args_t = typename function_traits<decltype(Fn)>::arg_t;\n"
" return Fn(reinterpret_cast<args_t>(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<typename>\n"
"struct CallbackUnpack {\n"
" static void ForIndirectCall(void* argsv);\n"
"};\n"
"template<typename F>\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 <thunks_common.h>\n"
"void func();\n"
"template<auto> struct fex_gen_config {};\n"
"template<> struct fex_gen_config<func> {};\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<auto> struct fex_gen_config {};\n"
"template<> struct fex_gen_config<func> : invalid_annotation {};\n", true));
REQUIRE_THROWS(run_thunkgen("void func();\n",
"template<auto> struct fex_gen_config {};\n"
"template<> struct fex_gen_config<func> { int invalid_field_annotation; };\n", true));
}
TEST_CASE_METHOD(Fixture, "VersionedLibrary") {
const auto output = run_thunkgen_host("",
"template<auto> 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<typename> struct fex_gen_type {};\n"
"template<> struct fex_gen_type<int(char, char)> {};\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<clang::ClassTemplateSpecializationDecl>(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<auto> struct fex_gen_config {};\n"
"template<> struct fex_gen_config<func> {};\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 <thunks_common.h>\n"
"void func(int (*funcptr)(char, char));\n"
"template<auto> struct fex_gen_config {};\n"
"template<> struct fex_gen_config<func> : 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<auto> struct fex_gen_config {};\n"
"template<> struct fex_gen_config<func> {};\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<auto> struct fex_gen_config {};\n"
"template<> struct fex_gen_config<func> {};\n", true));
REQUIRE_NOTHROW(run_thunkgen_guest(prelude,
"#include <thunks_common.h>\n"
"funcptr func(int);\n"
"template<auto> struct fex_gen_config {};\n"
"template<> struct fex_gen_config<func> : 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<auto> struct fex_gen_config {};\n"
"template<> struct fex_gen_config<func> {\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<auto> struct fex_gen_config {};\n"
"template<> struct fex_gen_config<func> {};\n", true));
}