#include "clang/AST/RecursiveASTVisitor.h" #include #include #include #include #include #include "interface.h" struct FunctionParams { std::vector param_types; }; struct ThunkedCallback : FunctionParams { clang::QualType return_type; std::size_t callback_index; std::size_t user_arg_index; bool is_stub = false; // Callback will be replaced by a stub that calls std::abort bool is_guest = false; // Callback will never be called on the host bool is_variadic = false; }; /** * Guest<->Host transition point. * * These are normally used to translate the public API of the guest to host * function calls (ThunkedAPIFunction), but a thunk library may also define * internal thunks that don't correspond to any function in the implemented * API. */ struct ThunkedFunction : FunctionParams { std::string function_name; clang::QualType return_type; // If true, param_types contains an extra size_t and the valist for marshalling through an internal function bool is_variadic = false; // If true, the unpacking function will call a custom fexfn_impl function // to be provided manually instead of calling the host library function // directly. // This is implied e.g. for thunks generated for variadic functions bool custom_host_impl = false; FunctionParams GetNonvariadicParams() const { if (!is_variadic) { return *this; } auto ret = param_types; ret.pop_back(); ret.pop_back(); return { std::move(ret) }; } std::string GetOriginalFunctionName() const { const std::string suffix = "_internal"; assert(function_name.length() > suffix.size()); assert((std::string_view { &*function_name.end() - suffix.size(), suffix.size() } == suffix)); return function_name.substr(0, function_name.size() - suffix.size()); } // Maps parameter index to ThunkedCallback std::unordered_map callbacks; clang::FunctionDecl* decl; }; /** * Function that is part of the API of the thunked library. * * For each of these, there is: * - A publicly visible guest entrypoint (usually auto-generated but may be manually defined) * - A pointer to the native host library function loaded through dlsym (or a user-provided function specified via host_loader) * - A ThunkedFunction with the same function_name (possibly suffixed with _internal) */ struct ThunkedAPIFunction : FunctionParams { std::string function_name; clang::QualType return_type; // name of the function to load the native host symbol with std::string host_loader; // If true, no guest-side implementation of this function will be autogenerated bool custom_guest_impl; bool is_variadic; // Index of the symbol table to store this export in (see guest_symtables). // If empty, a library export is created, otherwise the function is entered into a function pointer array std::optional symtable_namespace; }; static std::vector thunks; static std::vector thunked_api; static std::optional lib_version; struct NamespaceInfo { std::string name; // Function to load native host library functions with. // This function must be defined manually with the signature "void* func(void*, const char*)" std::string host_loader; bool generate_guest_symtable; }; // List of namespaces with a non-specialized fex_gen_config definition (including the global namespace, represented with an empty name) static std::vector namespaces; class ASTVisitor : public clang::RecursiveASTVisitor { clang::ASTContext& context; enum class CallbackStrategy { Default, Stub, Guest, }; struct NamespaceAnnotations { std::optional version; std::optional load_host_endpoint_via; bool generate_guest_symtable = false; }; struct Annotations { bool custom_host_impl = false; bool custom_guest_entrypoint = false; bool returns_guest_pointer = false; std::optional uniform_va_type; CallbackStrategy callback_strategy = CallbackStrategy::Default; }; NamespaceAnnotations GetNamespaceAnnotations(clang::CXXRecordDecl* decl) { if (!decl->hasDefinition()) { return {}; } NamespaceAnnotations ret; for (const clang::CXXBaseSpecifier& base : decl->bases()) { auto annotation = base.getType().getAsString(); if (annotation == "fexgen::generate_guest_symtable") { ret.generate_guest_symtable = true; } else { throw Error(base.getSourceRange().getBegin(), "Unknown namespace annotation"); } } for (const clang::FieldDecl* field : decl->fields()) { auto name = field->getNameAsString(); if (name == "load_host_endpoint_via") { auto loader_function_expr = field->getInClassInitializer()->IgnoreCasts(); auto loader_function_str = llvm::dyn_cast_or_null(loader_function_expr); if (loader_function_expr && !loader_function_str) { throw Error(loader_function_expr->getBeginLoc(), "Must initialize load_host_endpoint_via with a string"); } if (loader_function_str) { ret.load_host_endpoint_via = loader_function_str->getString(); } } else if (name == "version") { auto initializer = field->getInClassInitializer()->IgnoreCasts(); auto version_literal = llvm::dyn_cast_or_null(initializer); if (!initializer || !version_literal) { throw Error(field->getBeginLoc(), "No version given (expected integral typed member, e.g. \"int version = 5;\")"); } ret.version = version_literal->getValue().getZExtValue(); } else { throw Error(field->getBeginLoc(), "Unknown namespace annotation"); } } return ret; } Annotations GetAnnotations(clang::CXXRecordDecl* decl) { Annotations ret; for (const auto& base : decl->bases()) { auto annotation = base.getType().getAsString(); if (annotation == "fexgen::returns_guest_pointer") { ret.returns_guest_pointer = true; } else if (annotation == "fexgen::custom_host_impl") { ret.custom_host_impl = true; } else if (annotation == "fexgen::callback_stub") { ret.callback_strategy = CallbackStrategy::Stub; } else if (annotation == "fexgen::callback_guest") { ret.callback_strategy = CallbackStrategy::Guest; } else if (annotation == "fexgen::custom_guest_entrypoint") { ret.custom_guest_entrypoint = true; } else { throw Error(base.getSourceRange().getBegin(), "Unknown annotation"); } } for (const auto& child_decl : decl->getPrimaryContext()->decls()) { if (auto field = llvm::dyn_cast_or_null(child_decl)) { throw Error(field->getBeginLoc(), "Unknown field annotation"); } else if (auto type_alias = llvm::dyn_cast_or_null(child_decl)) { auto name = type_alias->getNameAsString(); if (name == "uniform_va_type") { ret.uniform_va_type = type_alias->getUnderlyingType(); } else { throw Error(type_alias->getBeginLoc(), "Unknown type alias annotation"); } } } return ret; } using ClangDiagnosticAsException = std::pair; template [[nodiscard]] ClangDiagnosticAsException Error(clang::SourceLocation loc, const char (&message)[N]) { auto id = context.getDiagnostics().getCustomDiagID(clang::DiagnosticsEngine::Error, message); return std::pair(loc, id); } public: ASTVisitor(clang::ASTContext& context_) : context(context_) { } /** * Matches "template struct fex_gen_config { ... }" */ bool VisitClassTemplateDecl(clang::ClassTemplateDecl* decl) try { if (decl->getName() != "fex_gen_config") { return true; } auto annotations = GetNamespaceAnnotations(decl->getTemplatedDecl()); auto namespace_decl = llvm::dyn_cast(decl->getDeclContext()); namespaces.push_back({ namespace_decl ? namespace_decl->getNameAsString() : "", annotations.load_host_endpoint_via.value_or(""), annotations.generate_guest_symtable }); if (annotations.version) { if (namespace_decl) { throw Error(decl->getBeginLoc(), "Library version must be defined in the global namespace"); } lib_version = annotations.version; } return true; } catch (ClangDiagnosticAsException& exception) { context.getDiagnostics().Report(exception.first, exception.second); return false; } /** * Matches "template<> struct fex_gen_config { ... }" */ bool VisitClassTemplateSpecializationDecl(clang::ClassTemplateSpecializationDecl* decl) try { if (decl->getName() != "fex_gen_config") { return true; } if (decl->getSpecializationKind() == clang::TSK_ExplicitInstantiationDefinition) { throw Error(decl->getBeginLoc(), "fex_gen_config may not be partially specialized\n"); } std::string namespace_name; if (auto namespace_decl = llvm::dyn_cast(decl->getDeclContext())) { namespace_name = namespace_decl->getNameAsString(); } const auto namespace_idx = std::distance( namespaces.begin(), std::find_if( namespaces.begin(), namespaces.end(), [&](auto& info) { return info.name == namespace_name; })); const NamespaceInfo& namespace_info = namespaces[namespace_idx]; const auto& template_args = decl->getTemplateArgs(); assert(template_args.size() == 1); auto emitted_function = llvm::dyn_cast(template_args[0].getAsDecl()); assert(emitted_function && "Argument is not a function"); auto return_type = emitted_function->getReturnType(); const auto annotations = GetAnnotations(decl); if (return_type->isFunctionPointerType() && !annotations.returns_guest_pointer) { // TODO: Should regular pointers require annotation, too? throw Error(decl->getBeginLoc(), "Function pointer return types require explicit annotation\n"); } // TODO: Use the types as written in the signature instead? ThunkedFunction data; data.function_name = emitted_function->getName().str(); data.return_type = return_type; data.is_variadic = emitted_function->isVariadic(); data.decl = emitted_function; data.custom_host_impl = annotations.custom_host_impl; for (std::size_t param_idx = 0; param_idx < emitted_function->param_size(); ++param_idx) { auto* param = emitted_function->getParamDecl(param_idx); data.param_types.push_back(param->getType()); if (param->getType()->isFunctionPointerType()) { auto funcptr = param->getFunctionType()->getAs(); ThunkedCallback callback; callback.return_type = funcptr->getReturnType(); for (auto& cb_param : funcptr->getParamTypes()) { callback.param_types.push_back(cb_param); } callback.is_stub = annotations.callback_strategy == CallbackStrategy::Stub; callback.is_guest = annotations.callback_strategy == CallbackStrategy::Guest; callback.is_variadic = funcptr->isVariadic(); if (callback.is_guest && !data.custom_host_impl) { throw Error(decl->getBeginLoc(), "callback_guest can only be used with custom_host_impl"); } data.callbacks.emplace(param_idx, callback); // TODO: Support for more than one callback is untested assert(data.callbacks.size() == 1); if (funcptr->isVariadic() && !callback.is_stub) { throw Error(decl->getBeginLoc(), "Variadic callbacks are not supported"); } } } // TODO: Rename to something like "needs_modified_callback" const bool has_nonstub_callbacks = std::any_of(data.callbacks.begin(), data.callbacks.end(), [](auto& cb) { return !cb.second.is_stub && !cb.second.is_guest; }); thunked_api.push_back(ThunkedAPIFunction { (const FunctionParams&)data, data.function_name, data.return_type, namespace_info.host_loader.empty() ? "dlsym" : namespace_info.host_loader, has_nonstub_callbacks || data.is_variadic || annotations.custom_guest_entrypoint, data.is_variadic, std::nullopt }); if (namespace_info.generate_guest_symtable) { thunked_api.back().symtable_namespace = namespace_idx; } if (data.is_variadic) { if (!annotations.uniform_va_type) { throw Error(decl->getBeginLoc(), "Variadic functions must be annotated with parameter type using uniform_va_type"); } // Convert variadic argument list into a count + pointer pair data.param_types.push_back(context.getSizeType()); data.param_types.push_back(context.getPointerType(*annotations.uniform_va_type)); } if (has_nonstub_callbacks || data.is_variadic) { // This function is thunked through an "_internal" symbol since its signature // is different from the one in the native host/guest libraries. data.function_name = data.function_name + "_internal"; assert(!data.custom_host_impl && "Custom host impl requested but this is implied by the function signature already"); data.custom_host_impl = true; } thunks.push_back(std::move(data)); return true; } catch (ClangDiagnosticAsException& exception) { context.getDiagnostics().Report(exception.first, exception.second); return false; } }; class ASTConsumer : public clang::ASTConsumer { public: void HandleTranslationUnit(clang::ASTContext& context) override { ASTVisitor{context}.TraverseDecl(context.getTranslationUnitDecl()); } }; FrontendAction::FrontendAction(const std::string& libname_, const OutputFilenames& output_filenames_) : libfilename(libname_), libname(libname_), output_filenames(output_filenames_) { for (auto& c : libname) { if (c == '-') { c = '_'; } } thunks.clear(); thunked_api.clear(); namespaces.clear(); lib_version = std::nullopt; } template static std::string format_function_args(const FunctionParams& params, Fn&& format_arg) { std::string ret; for (std::size_t idx = 0; idx < params.param_types.size(); ++idx) { ret += std::forward(format_arg)(idx) + ", "; } // drop trailing ", " ret.resize(ret.size() > 2 ? ret.size() - 2 : 0); return ret; }; static std::string format_function_args(const FunctionParams& params) { return format_function_args(params, [](std::size_t idx) -> std::string { return "args->a_" + std::to_string(idx); }); } void FrontendAction::EndSourceFileAction() { static auto format_decl = [](clang::QualType type, const std::string_view& name) { if (type->isFunctionPointerType()) { auto signature = type.getAsString(); const char needle[] = { '(', '*', ')' }; auto it = std::search(signature.begin(), signature.end(), std::begin(needle), std::end(needle)); if (it == signature.end()) { // It's *probably* a typedef, so this should be safe after all return signature + " " + std::string(name); } else { signature.insert(it + 2, name.begin(), name.end()); return signature; } } else { return type.getAsString() + " " + std::string(name); } }; 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_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 sha256(SHA256_DIGEST_LENGTH); SHA256(reinterpret_cast(sha256_message.data()), sha256_message.size(), sha256.data()); return sha256; }; auto get_callback_name = [](std::string_view function_name, unsigned param_index, bool is_first_cb) -> std::string { return std::string { function_name } + "CBFN" + (is_first_cb ? "" : std::to_string(param_index)); }; if (!output_filenames.thunks.empty()) { std::ofstream file(output_filenames.thunks); file << "extern \"C\" {\n"; for (auto& thunk : thunks) { const auto& function_name = thunk.function_name; auto sha256 = get_sha256(function_name); file << "MAKE_THUNK(" << libname << ", " << function_name << ", \""; bool first = true; for (auto c : sha256) { file << (first ? "" : ", ") << "0x" << std::hex << std::setw(2) << std::setfill('0') << +c; first = false; } file << "\")\n"; } file << "}\n"; } if (!output_filenames.function_packs_public.empty()) { std::ofstream file(output_filenames.function_packs_public); file << "extern \"C\" {\n"; for (auto& data : thunked_api) { if (data.custom_guest_impl) { continue; } const auto& function_name = data.function_name; file << "__attribute__((alias(\"fexfn_pack_" << function_name << "\"))) auto " << function_name << "("; for (std::size_t idx = 0; idx < data.param_types.size(); ++idx) { auto& type = data.param_types[idx]; file << (idx == 0 ? "" : ", ") << format_decl(type, "a_" + std::to_string(idx)); } file << ") -> " << data.return_type.getAsString() << ";\n"; } for (std::size_t namespace_idx = 0; namespace_idx < namespaces.size(); ++namespace_idx) { bool empty = true; for (auto& symbol : thunked_api) { if (symbol.symtable_namespace == namespace_idx) { if (empty) { file << "static struct { const char* name; void (*fn)(); } " << namespaces[namespace_idx].name << "_symtable[] = {\n"; empty = false; } file << " { \"" << symbol.function_name << "\", (void(*)())&" << symbol.function_name << " },\n"; } } if (!empty) { file << " { nullptr, nullptr }\n"; file << "};\n"; } } file << "}\n"; } if (!output_filenames.function_packs.empty()) { std::ofstream file(output_filenames.function_packs); file << "extern \"C\" {\n"; for (auto& data : thunks) { const auto& function_name = data.function_name; bool is_void = data.return_type->isVoidType(); file << "static auto fexfn_pack_" << function_name << "("; for (std::size_t idx = 0; idx < data.param_types.size(); ++idx) { auto& type = data.param_types[idx]; file << (idx == 0 ? "" : ", ") << format_decl(type, "a_" + std::to_string(idx)); } // Using trailing return type as it makes handling function pointer returns much easier file << ") -> " << data.return_type.getAsString() << " {\n"; file << " struct {\n"; for (std::size_t idx = 0; idx < data.param_types.size(); ++idx) { auto& type = data.param_types[idx]; file << " " << format_decl(type.getUnqualifiedType(), "a_" + std::to_string(idx)) << ";\n"; } if (!is_void) { file << " " << format_decl(data.return_type, "rv") << ";\n"; } else if (data.param_types.size() == 0) { // Avoid "empty struct has size 0 in C, size 1 in C++" warning file << " char force_nonempty;\n"; } file << " } args;\n"; for (std::size_t idx = 0; idx < data.param_types.size(); ++idx) { file << " args.a_" << idx << " = a_" << idx << ";\n"; } file << " fexthunks_" << libname << "_" << function_name << "(&args);\n"; if (!is_void) { file << " return args.rv;\n"; } file << "}\n"; } 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"; /* Generate stub callbacks */ for (auto& [cb_idx, cb] : thunk.callbacks) { if (cb.is_stub) { bool is_first_cb = (cb_idx == thunk.callbacks.begin()->first); const char* variadic_ellipsis = cb.is_variadic ? ", ..." : ""; auto cb_function_name = "fexfn_unpack_" + get_callback_name(function_name, cb_idx, is_first_cb) + "_stub"; file << "[[noreturn]] static " << cb.return_type.getAsString() << " " << cb_function_name << "(" << format_function_params(cb) << variadic_ellipsis << ") {\n"; file << " fprintf(stderr, \"FATAL: Attempted to invoke callback stub for " << function_name << "\\n\");\n"; file << " std::abort();\n"; file << "}\n"; } } file << "static void fexfn_unpack_" << libname << "_" << function_name << "(fexfn_packed_args_" << libname << "_" << function_name << "* args) {\n"; file << (is_void ? " " : " args->rv = ") << (thunk.custom_host_impl ? "fexfn_impl_" : "fexldr_ptr_") << libname << "_" << function_name << "("; { auto format_param = [&](std::size_t idx) { auto cb = thunk.callbacks.find(idx); if (cb != thunk.callbacks.end() && cb->second.is_stub) { bool is_first_cb = (cb->first == thunk.callbacks.begin()->first); return "fexfn_unpack_" + get_callback_name(function_name, cb->first, is_first_cb) + "_stub"; } else if (cb != thunk.callbacks.end() && cb->second.is_guest) { return "fex_guest_function_ptr { args->a_" + std::to_string(idx) + " }"; } else { return "args->a_" + std::to_string(idx); } }; file << format_function_args(thunk, format_param); } file << ");\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}, // " << 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"; std::string version_suffix; if (lib_version) { version_suffix = '.' + std::to_string(*lib_version); } const std::string library_filename = libfilename + ".so" + version_suffix; file << " fexldr_ptr_" << libname << "_so = dlopen(\"" << library_filename << "\", 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 << " = " << import.host_loader << "(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; const char* variadic_ellipsis = import.is_variadic ? ", ..." : ""; file << "using fexldr_type_" << libname << "_" << function_name << " = auto " << "(" << format_function_params(import) << variadic_ellipsis << ") -> " << import.return_type.getAsString() << ";\n"; file << "static fexldr_type_" << libname << "_" << function_name << " *fexldr_ptr_" << libname << "_" << function_name << ";\n"; } } if (!output_filenames.callback_structs.empty()) { std::ofstream file(output_filenames.callback_structs); for (auto& thunk : thunks) { for (const auto& [cb_idx, cb] : thunk.callbacks) { if (cb.is_stub || cb.is_guest) { continue; } file << "struct " << thunk.GetOriginalFunctionName() << "CB_Args {\n"; file << format_struct_members(cb, " "); if (!cb.return_type->isVoidType()) { file << " " << format_decl(cb.return_type, "rv") << ";\n"; } file << "};\n"; } } } if (!output_filenames.callback_typedefs.empty()) { std::ofstream file(output_filenames.callback_typedefs); for (auto& thunk : thunks) { for (const auto& [cb_idx, cb] : thunk.callbacks) { if (cb.is_stub || cb.is_guest) { continue; } bool is_first_cb = (cb_idx == thunk.callbacks.begin()->first); auto cb_function_name = get_callback_name(thunk.GetOriginalFunctionName(), cb_idx, is_first_cb); file << "typedef " << cb.return_type.getAsString() << " " << cb_function_name << "(" << format_function_params(cb) << ");\n"; } } } if (!output_filenames.callback_unpacks.empty()) { std::ofstream file(output_filenames.callback_unpacks); for (auto& thunk : thunks) { for (const auto& [cb_idx, cb] : thunk.callbacks) { if (cb.is_stub || cb.is_guest) { continue; } bool is_void = cb.return_type->isVoidType(); bool is_first_cb = (cb_idx == thunk.callbacks.begin()->first); auto cb_function_name = thunk.function_name + "CB" + (is_first_cb ? "" : std::to_string(cb_idx)); file << "static void fexfn_unpack_" << libname << "_" << cb_function_name << "(uintptr_t cb, void* argsv) {\n"; file << " typedef " << cb.return_type.getAsString() << " fn_t (" << format_function_params(cb) << ");\n"; file << " auto callback = reinterpret_cast(cb);\n"; file << " struct arg_t {\n"; file << format_struct_members(cb, " "); if (!is_void) { file << " " << format_decl(cb.return_type, "rv") << ";\n"; } file << " };\n"; file << " auto args = (arg_t*)argsv;\n"; file << (is_void ? " " : " args->rv = ") << "callback(" << format_function_args(cb) << ");\n"; file << "}\n"; } } } if (!output_filenames.callback_unpacks_header.empty()) { std::ofstream file(output_filenames.callback_unpacks_header); for (auto& thunk : thunks) { for (const auto& [cb_idx, cb] : thunk.callbacks) { if (cb.is_stub || cb.is_guest) { continue; } bool is_first_cb = (cb_idx == thunk.callbacks.begin()->first); auto cb_function_name = thunk.GetOriginalFunctionName() + "CB" + (is_first_cb ? "" : std::to_string(cb_idx)); file << "uintptr_t " << libname << "_" << cb_function_name << ";\n"; } } } if (!output_filenames.callback_unpacks_header_init.empty()) { std::ofstream file(output_filenames.callback_unpacks_header_init); for (auto& thunk : thunks) { for (const auto& [cb_idx, cb] : thunk.callbacks) { if (cb.is_stub || cb.is_guest) { continue; } bool is_first_cb = (cb_idx == thunk.callbacks.begin()->first); auto cb_function_name = thunk.function_name + "CB" + (is_first_cb ? "" : std::to_string(cb_idx)); file << "(uintptr_t)&fexfn_unpack_" << libname << "_" << cb_function_name << ",\n"; } } } if (!output_filenames.symbol_list.empty()) { std::ofstream file(output_filenames.symbol_list); for (std::size_t namespace_idx = 0; namespace_idx < namespaces.size(); ++namespace_idx) { const auto& ns = namespaces[namespace_idx]; file << "#define FOREACH_" << ns.name << (ns.name.empty() ? "" : "_") << "SYMBOL(EXPAND) \\\n"; for (auto& symbol : thunked_api) { if (symbol.symtable_namespace.value_or(0) == namespace_idx) { file << " EXPAND(" << symbol.function_name << ", \"TODO\") \\\n"; } } file << "\n"; } } } std::unique_ptr FrontendAction::CreateASTConsumer(clang::CompilerInstance&, clang::StringRef) { return std::make_unique(); }