Thunks/gen: Add support for generation of host library functions

This commit is contained in:
Tony Wasserka 2021-12-10 11:24:57 +01:00
parent 22466a973c
commit bfb9cabeb8
5 changed files with 232 additions and 8 deletions

View File

@ -141,6 +141,35 @@ void FrontendAction::EndSourceFileAction() {
}
};
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(), "a_" + std::to_string(idx)) + ";\n";
}
return ret;
};
auto format_function_args = [](const FunctionParams& params) {
std::string ret;
for (std::size_t idx = 0; idx < params.param_types.size(); ++idx) {
ret += "args->a_" + std::to_string(idx) + ", ";
}
// drop trailing ", "
ret.resize(ret.size() > 2 ? ret.size() - 2 : 0);
return ret;
};
auto format_function_params = [](const FunctionParams& params) {
std::string ret;
for (std::size_t idx = 0; idx < params.param_types.size(); ++idx) {
auto& type = params.param_types[idx];
ret += format_decl(type, "a_" + std::to_string(idx)) + ", ";
}
// drop trailing ", "
ret.resize(ret.size() > 2 ? ret.size() - 2 : 0);
return ret;
};
auto get_sha256 = [this](const std::string& function_name) {
std::string sha256_message = libname + ":" + function_name;
std::vector<unsigned char> sha256(SHA256_DIGEST_LENGTH);
@ -223,6 +252,71 @@ void FrontendAction::EndSourceFileAction() {
}
file << "}\n";
}
if (!output_filenames.function_unpacks.empty()) {
std::ofstream file(output_filenames.function_unpacks);
file << "extern \"C\" {\n";
for (auto& thunk : thunks) {
const auto& function_name = thunk.function_name;
bool is_void = thunk.return_type->isVoidType();
file << "struct fexfn_packed_args_" << libname << "_" << function_name << " {\n";
file << format_struct_members(thunk, " ");
if (!is_void) {
file << " " << format_decl(thunk.return_type, "rv") << ";\n";
} else if (thunk.param_types.size() == 0) {
// Avoid "empty struct has size 0 in C, size 1 in C++" warning
file << " char force_nonempty;\n";
}
file << "};\n";
file << "static void fexfn_unpack_" << libname << "_" << function_name << "(fexfn_packed_args_" << libname << "_" << function_name << "* args) {\n";
file << (is_void ? " " : " args->rv = ") << "fexldr_ptr_" << libname << "_" << function_name << "(" << format_function_args(thunk) << ");\n";
file << "}\n";
}
file << "}\n";
}
if (!output_filenames.tab_function_unpacks.empty()) {
std::ofstream file(output_filenames.tab_function_unpacks);
for (auto& thunk : thunks) {
const auto& function_name = thunk.function_name;
auto sha256 = get_sha256(function_name);
file << "{(uint8_t*)\"";
for (auto c : sha256) {
file << "\\x" << std::hex << std::setw(2) << std::setfill('0') << +c;
}
file << "\", &fexfn_type_erased_unpack<fexfn_unpack_" << libname << "_" << function_name << ">}, // " << libname << ":" << function_name << "\n";
}
}
if (!output_filenames.ldr.empty()) {
std::ofstream file(output_filenames.ldr);
file << "static void* fexldr_ptr_" << libname << "_so;\n";
file << "extern \"C\" bool fexldr_init_" << libname << "() {\n";
file << " fexldr_ptr_" << libname << "_so = dlopen(\"" << libname << ".so\", RTLD_LOCAL | RTLD_LAZY);\n";
file << " if (!fexldr_ptr_" << libname << "_so) { return false; }\n\n";
for (auto& import : thunked_api) {
file << " (void*&)fexldr_ptr_" << libname << "_" << import.function_name << " = dlsym(fexldr_ptr_" << libname << "_so, \"" << import.function_name << "\");\n";
}
file << " return true;\n";
file << "}\n";
}
if (!output_filenames.ldr_ptrs.empty()) {
std::ofstream file(output_filenames.ldr_ptrs);
for (auto& import : thunked_api) {
const auto& function_name = import.function_name;
file << "using fexldr_type_" << libname << "_" << function_name << " = auto " << "(" << format_function_params(import) << ") -> " << import.return_type.getAsString() << ";\n";
file << "static fexldr_type_" << libname << "_" << function_name << " *fexldr_ptr_" << libname << "_" << function_name << ";\n";
}
}
}
std::unique_ptr<clang::ASTConsumer> FrontendAction::CreateASTConsumer(clang::CompilerInstance&, clang::StringRef) {

View File

@ -4,6 +4,12 @@
#include <string>
struct OutputFilenames {
// Host
std::string function_unpacks;
std::string tab_function_unpacks;
std::string ldr;
std::string ldr_ptrs;
// Guest
std::string thunks;
std::string function_packs;

View File

@ -52,7 +52,15 @@ int main(int argc, char* argv[]) {
while (arg < last_internal_arg) {
auto target = std::string { *arg++ };
auto out_filename = *arg++;
if (target == "-thunks") {
if (target == "-function_unpacks") {
output_filenames.function_unpacks = out_filename;
} else if (target == "-tab_function_unpacks") {
output_filenames.tab_function_unpacks = out_filename;
} else if (target == "-ldr") {
output_filenames.ldr = out_filename;
} else if (target == "-ldr_ptrs") {
output_filenames.ldr_ptrs = out_filename;
} else if (target == "-thunks") {
output_filenames.thunks = out_filename;
} else if (target == "-function_packs") {
output_filenames.function_packs = out_filename;

View File

@ -7,6 +7,21 @@ $end_info$
#pragma once
#include <stdint.h>
template<typename Fn>
struct function_traits;
template<typename Result, typename Arg>
struct function_traits<Result(*)(Arg)> {
using result_t = Result;
using arg_t = Arg;
};
template<auto Fn>
static typename function_traits<decltype(Fn)>::result_t
fexfn_type_erased_unpack(void* argsv) {
using args_t = typename function_traits<decltype(Fn)>::arg_t;
return Fn(reinterpret_cast<args_t>(argsv));
}
struct ExportEntry { uint8_t* sha256; void(*fn)(void *); };
typedef void fex_call_callback_t(uintptr_t callback, void *arg0, void* arg1);

View File

@ -16,6 +16,10 @@ struct Fixture {
tmpdir = std::tmpnam(nullptr);
std::filesystem::create_directory(tmpdir);
output_filenames = {
tmpdir + "/function_unpacks",
tmpdir + "/tab_function_unpacks",
tmpdir + "/ldr",
tmpdir + "/ldr_ptrs",
tmpdir + "/thunks",
tmpdir + "/function_packs",
tmpdir + "/function_packs_public"
@ -26,7 +30,14 @@ struct Fixture {
std::filesystem::remove_all(tmpdir);
}
struct GenOutput {
std::string guest;
std::string host;
};
std::string run_thunkgen_guest(std::string_view code, bool silent = false);
std::string run_thunkgen_host(std::string_view code, bool silent = false);
GenOutput run_thunkgen(std::string_view code, bool silent = false);
const std::string libname = "libtest";
std::string tmpdir;
@ -210,21 +221,85 @@ std::string Fixture::run_thunkgen_guest(std::string_view code, bool silent) {
return result;
}
/**
* Generates host thunk library code from the given input
*/
std::string Fixture::run_thunkgen_host(std::string_view code, bool silent) {
run_tool(std::make_unique<FrontendAction>(libname, output_filenames), 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";
for (auto& filename : {
output_filenames.ldr_ptrs,
output_filenames.function_unpacks,
output_filenames.tab_function_unpacks,
output_filenames.ldr,
}) {
bool tab_function_unpacks = (filename == output_filenames.tab_function_unpacks);
if (tab_function_unpacks) {
result += "struct ExportEntry { uint8_t* sha256; void(*fn)(void *); };\n";
result += "static ExportEntry exports[] = {\n";
}
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());
if (tab_function_unpacks) {
result += " { nullptr, nullptr }\n";
result += "};\n";
}
}
return result;
}
Fixture::GenOutput Fixture::run_thunkgen(std::string_view code, bool silent) {
return { run_thunkgen_guest(code, silent),
run_thunkgen_host(code, silent) };
}
TEST_CASE_METHOD(Fixture, "Trivial") {
const auto output = run_thunkgen_guest(
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");
CHECK_THAT(output, DefinesPublicFunction("func"));
// Guest code
CHECK_THAT(output.guest, DefinesPublicFunction("func"));
CHECK_THAT(output,
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
)));
}
// Function pointer parameters trigger an error
@ -238,17 +313,19 @@ TEST_CASE_METHOD(Fixture, "FunctionPointerParameter") {
TEST_CASE_METHOD(Fixture, "MultipleParameters") {
const std::string prelude = "struct TestStruct { int member; };\n";
auto output = run_thunkgen_guest(
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");
output = prelude + output;
output.guest = prelude + output.guest;
output.host = prelude + output.host;
CHECK_THAT(output, DefinesPublicFunction("func"));
// Guest code
CHECK_THAT(output.guest, DefinesPublicFunction("func"));
CHECK_THAT(output,
CHECK_THAT(output.guest,
matches(functionDecl(
hasName("fexfn_pack_func"),
returns(asString("void")),
@ -258,6 +335,30 @@ TEST_CASE_METHOD(Fixture, "MultipleParameters") {
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