
При создании компилятора для собственного языка программирования я сделал его как транспайлер в исходный код на С++, вот только реализация сильно подкачала. Сначала приходится генерировать динамическую библиотеку с помощью вызова gcc, который и сам по себе не очень быстрый, так еще его может и не быть на целевой машине, особенно на другой платформе (например Windows). Конечно, для первых экспериментов и такой реализации было достаточно, но сейчас, когда я начал готовить код компилятора к публикации, стало понятно, что текущий вариант с фоновым запуском gcc никуда не годится.
Из-за этого, я решил не откладывать перевод компилятора на использование LLVM, который планировался когда нибудь в будущем, а решил сделать это уже сейчас. И для этого нужно было научиться запускать компиляцию C++ кода с помощью библиотек Clang, но тут вылезло сразу несколько проблем.
Оказывается, интерфейс Clang меняется от версии к версии и все найденные мной примеры были старыми и не запускались в актуальной версии (Сlang 12), а стабильный C-style интерфейс предназначен для парсинга и анализа исходников и с помощью которого сгенерировать исполняемые файлы не получится*.
Дополнительная проблемой оказалось, что Clang не может анализировать файл из памяти, даже если для этого есть соответствующие классы. Из объяснений выходило, что в экземпляре компилятора проверяется, является ли ввод файлом**.
А теперь публикую результат своих изысканий в виде рабочего примера динамической компиляции С++ кода с последующей его загрузкой и выполнением скомпилированных функций. Исходники адаптированны под актуальную версию Clang 12. Пояснения к коду я перевел и дополнил перед публикацией, а ссылки на исходные материалы приведены в конце статьи.
- *) Кажется в 14 версии планируется реализовать C интерфейс для генерации исполняемых файлов.
- **) На самом деле, Clang может (или теперь может) компилировать файлы из оперативной памяти, поэтому в исходники я добавил и эту возможность.
Не простой LLVM
Как было написано в самом начале, интерфейс Clang меняется от версии к версии и работающий код, например для LLVM 7, может уже не работать для LLVM 8 или 6 (текущая актуальная версия 12.1 и на подходе уже 13 версия LLVM).
А стабильный C-style интерфейс libtooling предназначен для парсинга и создания AST, а не для генерации исполняемых файлов с помощью LLVM.
Поэтому, последовательность этапов получается следующая:
- Распарсить исходный код С/С++ с правильными опциями и получить AST (Abstract Syntax Tree)
- Преобразовать AST во внутреннее представление (Intermediate Representation).
- Выполнить различные оптимизации и скомпилировать IR в исполняемый код (JIT LLVM).
- Далее требуется создать экземпляр LLVM модуля, который хранит всю информацию о текущей среде выполнения.
- И только затем можно будет загрузить скомпилированный код и переходить к непосредственному вызову функции, которую мы скомпилировали.
Необходимые пояснения для примера кода
Заголовочных файлов используется очень много, поэтому большинство из них вынесено в файл #include «llvm_precomp.h». Далее в отдельную функцию InitializeLLVM() вынесена инициализация LLVM.
Первоначальный пример был сделан для Clang 6 или 7 версии, где такая возможность действительно отсутствовала, но сейчас это предостережение уже не актуально.
Во-вторых, это опции компиляции. Их нужно устанавливать таким же образом, как и в командной строке со всеми соответствующими флагами и включенными путями. Это можно сделать, позволив Сlang установить все автоматически, используя список аргументов по умолчанию.
Но самое главное, это диагностика проблем! Нужно начинать с настройки объектов, с помощью которых будут выводиться все предупреждения и ошибки в работе парсера Clang и всех последующих инструментов, необходимых для работы JIT компилятора для C/C++ кода.
Автор второй статьи (ссылки на исходные публикации приведены в конце) немного «причесал» исходный пример, т.к. ему пришлось заменить несколько unique_ptrs на контейнеры IntrusiveRefCntPtr, предоставленные LLVM (это было необходимо, поскольку исходный код не компилировался). Еще он добавил несколько дополнительных отладочных сообщений. Сейчас в примере динамически собираются две функции nv_add и nv_sub.
У меня тоже сразу не получилось использовать найденные примеры кода, т.к. интерфейс Clang опять поменялся и у некоторых функций, где раньше использовались обычные ссылки на объекты, они были заменены на IntrusiveRefCntPtr. Хотя в основном все осталось как в изначальных исходниках.
clang::IntrusiveRefCntPtr<clang::DiagnosticOptions> DiagOpts = new clang::DiagnosticOptions; clang::TextDiagnosticPrinter *textDiagPrinter = new clang::TextDiagnosticPrinter(llvm::outs(), &*DiagOpts); clang::IntrusiveRefCntPtr<clang::DiagnosticIDs> pDiagIDs; clang::DiagnosticsEngine *pDiagnosticsEngine = new clang::DiagnosticsEngine(pDiagIDs, &*DiagOpts, textDiagPrinter);
Возможно тут остался какой-то косяк, т.к. при завершении работы исходного примера, приложение падало с ошибками Segmentation fault или Double free or corrupt, но в конечном итоге методом «научного тыка» исходный код был приведен в состояние, когда пример завершается корректно.
Далее идет настройка Triple, комбинация из трех значений, которая определяет архитектуру процессора и целевую платформу. В моем случае это x86_64-pc-linux-gnu. После чего идет создание самого компилятора с опциями как в командной строке.
Сейчас Clang уже умеет парсить файлы из памяти, точнее из входного потока, и для этого во входных параметрах вместо имен файлов нужно передать минус, а сами данные записать в pipe:
// Send code through a pipe to stdin int codeInPipe[2]; pipe2(codeInPipe, O_NONBLOCK); write(codeInPipe[1], (void *) func_text, strlen(func_text)); close(codeInPipe[1]); // We need to close the pipe to send an EOF dup2(codeInPipe[0], STDIN_FILENO); ... itemcstrs.push_back("-"); // Read code from stdin
Далее в коде идет настройка опций компилятора и непосредственный вызов компилятора для создания AST.
if(!compilerInstance.ExecuteAction(*action)) { }
Генерация исполняемого кода
Внимание, будьте аккуратны с контекстом выполнения!
Во-первых, контекст LLVM, который мы создали, должен оставаться актуальным до тех пор, пока мы используем что-либо из этого модуля компиляции. Это очень важно, потому что все, что сгенерировано с помощью JIT, должно оставаться в памяти после генерации кода и находится в его контексте до тех пор, пока не будет удалено явно.
Вторая проблема заключается в том, что по умолчанию не выполняется оптимизация IR. И это приходится выполнять вручную.
Первым делом получается модуль LLVM из предыдущего действия.
std::unique_ptr<llvm::Module> module = action->takeModule(); if(!module) { ... }
После чего можно выполнять разные проходы оптимизации. Код для оптимизации довольно сложен, но это LLVM… и одна из причин, по которой API продолжает видоизменяться от версии к версии.
llvm::PassBuilder passBuilder; llvm::LoopAnalysisManager loopAnalysisManager(codeGenOptions.DebugPassManager); llvm::FunctionAnalysisManager functionAnalysisManager(codeGenOptions.DebugPassManager); llvm::CGSCCAnalysisManager cGSCCAnalysisManager(codeGenOptions.DebugPassManager); llvm::ModuleAnalysisManager moduleAnalysisManager(codeGenOptions.DebugPassManager); passBuilder.registerModuleAnalyses(moduleAnalysisManager); passBuilder.registerCGSCCAnalyses(cGSCCAnalysisManager); passBuilder.registerFunctionAnalyses(functionAnalysisManager); passBuilder.registerLoopAnalyses(loopAnalysisManager); passBuilder.crossRegisterProxies(loopAnalysisManager, functionAnalysisManager, cGSCCAnalysisManager, moduleAnalysisManager); llvm::ModulePassManager modulePassManager = passBuilder.buildPerModuleDefaultPipeline(llvm::PassBuilder::OptimizationLevel::O3); modulePassManager.run(*module, moduleAnalysisManager);
И только после этого можно использовать JIT-компилятор и искать в контексте нужную нам функцию. Имейте в виду, что модуль LLVM должен оставаться актуальным до тех пор, пока вы собираетесь используете скомпилированные данные!
llvm::EngineBuilder builder(std::move(module)); builder.setMCJITMemoryManager(std::make_unique<llvm::SectionMemoryManager>()); builder.setOptLevel(llvm::CodeGenOpt::Level::Aggressive); auto executionEngine = builder.create(); if(!executionEngine) { ... } reinterpret_cast<Function> (executionEngine->getFunctionAddress(function));
Исходники
Исходники проекта опубликованы на bitbucket (т.к. github.com хочет либо идентификацию с помощью внешних сервисов или настроить двухфакторную аутентификацию с помощью сертификатов). С первым не хочу заморачаться, а второе лень настраивать.
remote: Support for password authentication was removed on August 13, 2021. Please use a personal access token instead.
remote: Please see github.blog/2020-12-15-token-authentication-requirements-for-git-operations for more information.
fatal: недоступно: The requested URL returned error: 403
Сборка примера
Сборку исходников я проверял только под linux с установленным Clang 12. Система сборки используется от древней версии NetBeans, но все собирается стандартно с помощью команды make.
В статье использованы следующие материалы
Compiling C++ code in memory with clang и её переработка с небольшими исправлениями с учетом версии Clang. Еще добавил в пример компиляцию кода из оперативной памяти, который нашел тут.
З.Ы.
Собственно на этом все.
Пишите, если будут комментарии или замечания.
З.З.Ы.
Более актуальный материал про динамическую JIT компиляцию C++ с использованием интерфейса ORCv2.
А вообще, настоящая JIT компиляция С++ кода, это очень круто!
Исходники примера JIT компилятора С/С++
#include <sstream> #include <iostream> #include <fstream> #include <unistd.h> #include <fcntl.h> #include "llvm_precomp.h" //#define NV_LLVM_VERBOSE 1 bool LLVMinit = false; #define ERROR_MSG(msg) std::cout << "[ERROR]: "<<msg<< std::endl; #define DEBUG_MSG(msg) std::cout << "[DEBUG]: "<<msg<< std::endl; void InitializeLLVM() { if(LLVMinit) { return; } // We have not initialized any pass managers for any device yet. // Run the global LLVM pass initialization functions. llvm::InitializeNativeTarget(); llvm::InitializeNativeTargetAsmPrinter(); llvm::InitializeNativeTargetAsmParser(); auto& Registry = *llvm::PassRegistry::getPassRegistry(); llvm::initializeCore(Registry); llvm::initializeScalarOpts(Registry); llvm::initializeVectorization(Registry); llvm::initializeIPO(Registry); llvm::initializeAnalysis(Registry); llvm::initializeTransformUtils(Registry); llvm::initializeInstCombine(Registry); llvm::initializeInstrumentation(Registry); llvm::initializeTarget(Registry); LLVMinit = true; } int main(int argc, char *argv[]) { InitializeLLVM(); const char * func_text = \ "int nv_add(int a, int b) {\n\ printf(\"call nv_add(%d, %d)\\n\", a, b);\n\ return a + b;\n\ }\n\ \n\ int nv_sub(int a, int b) {\n\ printf(\"call nv_sub(%d, %d)\\n\", a, b);\n\ return a - b;\n\ }\n\ "; DEBUG_MSG("Running clang compilation..."); clang::CompilerInstance compilerInstance; auto& compilerInvocation = compilerInstance.getInvocation(); // Диагностика работы Clang clang::IntrusiveRefCntPtr<clang::DiagnosticOptions> DiagOpts = new clang::DiagnosticOptions; clang::TextDiagnosticPrinter *textDiagPrinter = new clang::TextDiagnosticPrinter(llvm::outs(), &*DiagOpts); clang::IntrusiveRefCntPtr<clang::DiagnosticIDs> pDiagIDs; clang::DiagnosticsEngine *pDiagnosticsEngine = new clang::DiagnosticsEngine(pDiagIDs, &*DiagOpts, textDiagPrinter); // Целевая платформа std::stringstream ss; ss << "-triple=" << llvm::sys::getDefaultTargetTriple(); std::cout << llvm::sys::getDefaultTargetTriple(); std::istream_iterator<std::string> begin(ss); std::istream_iterator<std::string> end; std::istream_iterator<std::string> i = begin; std::vector<const char*> itemcstrs; std::vector<std::string> itemstrs; while(i != end) { itemstrs.push_back(*i); ++i; } for (unsigned idx = 0; idx < itemstrs.size(); idx++) { // note: if itemstrs is modified after this, itemcstrs will be full // of invalid pointers! Could make copies, but would have to clean up then... itemcstrs.push_back(itemstrs[idx].c_str()); } // Компиляция из памяти // Send code through a pipe to stdin int codeInPipe[2]; pipe2(codeInPipe, O_NONBLOCK); write(codeInPipe[1], (void *) func_text, strlen(func_text)); close(codeInPipe[1]); // We need to close the pipe to send an EOF dup2(codeInPipe[0], STDIN_FILENO); itemcstrs.push_back("-"); // Read code from stdin clang::CompilerInvocation::CreateFromArgs(compilerInvocation, llvm::ArrayRef<const char *>(itemcstrs.data(), itemcstrs.size()), *pDiagnosticsEngine); auto* languageOptions = compilerInvocation.getLangOpts(); auto& preprocessorOptions = compilerInvocation.getPreprocessorOpts(); auto& targetOptions = compilerInvocation.getTargetOpts(); auto& frontEndOptions = compilerInvocation.getFrontendOpts(); #ifdef NV_LLVM_VERBOSE frontEndOptions.ShowStats = true; #endif auto& headerSearchOptions = compilerInvocation.getHeaderSearchOpts(); #ifdef NV_LLVM_VERBOSE headerSearchOptions.Verbose = true; #endif auto& codeGenOptions = compilerInvocation.getCodeGenOpts(); targetOptions.Triple = llvm::sys::getDefaultTargetTriple(); compilerInstance.createDiagnostics(textDiagPrinter, false); llvm::LLVMContext context; std::unique_ptr<clang::CodeGenAction> action = std::make_unique<clang::EmitLLVMOnlyAction>(&context); if(!compilerInstance.ExecuteAction(*action)) { ERROR_MSG("Cannot execute action with compiler instance."); } // Runtime LLVM Module std::unique_ptr<llvm::Module> module = action->takeModule(); if(!module) { ERROR_MSG("Cannot retrieve IR module."); } // Оптимизация IR llvm::PassBuilder passBuilder; llvm::LoopAnalysisManager loopAnalysisManager(codeGenOptions.DebugPassManager); llvm::FunctionAnalysisManager functionAnalysisManager(codeGenOptions.DebugPassManager); llvm::CGSCCAnalysisManager cGSCCAnalysisManager(codeGenOptions.DebugPassManager); llvm::ModuleAnalysisManager moduleAnalysisManager(codeGenOptions.DebugPassManager); passBuilder.registerModuleAnalyses(moduleAnalysisManager); passBuilder.registerCGSCCAnalyses(cGSCCAnalysisManager); passBuilder.registerFunctionAnalyses(functionAnalysisManager); passBuilder.registerLoopAnalyses(loopAnalysisManager); passBuilder.crossRegisterProxies(loopAnalysisManager, functionAnalysisManager, cGSCCAnalysisManager, moduleAnalysisManager); llvm::ModulePassManager modulePassManager = passBuilder.buildPerModuleDefaultPipeline(llvm::PassBuilder::OptimizationLevel::O3); modulePassManager.run(*module, moduleAnalysisManager); llvm::EngineBuilder builder(std::move(module)); builder.setMCJITMemoryManager(std::make_unique<llvm::SectionMemoryManager>()); builder.setOptLevel(llvm::CodeGenOpt::Level::Aggressive); std::string createErrorMsg; builder.setEngineKind(llvm::EngineKind::JIT); builder.setVerifyModules(true); builder.setErrorStr(&createErrorMsg); std::string triple = llvm::sys::getDefaultTargetTriple(); DEBUG_MSG("Using target triple: " << triple); auto executionEngine = builder.create(); if(!executionEngine) { ERROR_MSG("Cannot create execution engine.'" << createErrorMsg << "'"); } DEBUG_MSG("Retrieving nv_add/nv_sub functions..."); typedef int(*AddFunc)(int, int); typedef int(*SubFunc)(int, int); AddFunc add = reinterpret_cast<AddFunc> (executionEngine->getFunctionAddress("nv_add")); if(!add) { ERROR_MSG("Cannot retrieve Add function."); } else { int res = add(40, 2); DEBUG_MSG("The meaning of life is: " << res << "!"); } SubFunc sub = reinterpret_cast<SubFunc> (executionEngine->getFunctionAddress("nv_sub")); if(!sub) { ERROR_MSG("Cannot retrieve Sub function."); } else { int res = sub(50, 8); DEBUG_MSG("The meaning of life is really: " << res << "!"); } DEBUG_MSG("Done running clang compilation."); return 0; }

