Динамическая JIT компиляция C++

от автора

Когда-то давно я писал статью-наблюдение про использование Хабра в качестве площадки для архитектурных ревью технических решений практически любой сложности Хабр — ума палата, и продолжаю пользоваться этим способом на постоянной основе.

А теперь, похоже, я нашел еще один очень полезный Хаброхак, который заключается в использования Хабра как записной книжки, но не как в том анекдоте я думал, что это блокнот для заметок, а всем видно оказывается что я здесь написал, а в самом хорошем смысле — для хранения результатов различных экспериментов, подведения итогов поиска в решения проблем и публикации итоговых выводов.

Так, на днях искал материалы про JIT компиляцию C++ в рантайме с помощью clang/llvm и поиск мне выдал мою же собственную статью трехгодичной давности: Динамическая JIT компиляция С/С++ в LLVM с помощью Clang / Хабр. В результате появилась уже эта новая статья про JIT компиляцию C/С++.

Предыстория

Мне для языка NewLang потребовался JIT компилятор С++ и сперва я попробовал воспользоваться примерами из первоисточника. У проекта clang есть исходники с примерами использования синтаксического анализатора кода, но все они ограничиваются анализом или обработкой синтаксического дерева, тогда как мне нужно получить из исходников С++ LLVM IR (низкоуровневое промежуточное представление).

В примерах llvm тоже есть файлы с различными примерами JIT компиляции и с динамическим созданием LLVM IR HowToUseLLJIT или HowToUseJIT, но это примеры для исполнения низкоуровневого кода и там нет преобразователя C++ -> IR.

Попробовал взять cling. Но он использует собственный пропатченный форк llvm, который, естественно отстает от текущей актуальной версии. К тому же у меня так и не получилось его собрать. И после исправления пятой или шестой ошибки сборки я плюнул, и решил поискать что-то нибудь другое.

Другие найденные примеры были либо для старых версий LLVM и не собираются (эта известная проблема clang и LLVM), либо не то, что мне требуется.

В результате, как я написал выше, поисковик предложил мне мою же собственную статью на Хабре с примером генерации кода из C++ в IR с последующей компиляцией в JIT, которую я и переписал под актуальную версию LLVM 18 на основе примеров OrcV2. Но прежде, чем перейти непосредственно к коду, нужно рассказать про сам LLVM ORC.

LLVM ORC

LLVM ORC, это модульный API для создания JIT-компиляторов. Как написано в документации, существует несколько вариантов его использования:

  1. В учебных пособиях по LLVM используется простой JIT-класс на основе ORC для выполнения выражений, скомпилированных из игрушечного языка: калейдоскопа.
  2. Отладчик LLVM, LLDB, использует JIT кросс-компиляции для оценки выражений. В этом случае кросс-компиляция позволяет выполнять выражения, скомпилированные в процессе отладчика, в целевом процессе отладки, который может находиться на другом устройстве/архитектуре.
  3. В высокопроизводительных JIT (например, JVM, Julia), которые хотят использовать оптимизации LLVM в существующей JIT-инфраструктуре.
  4. В интерпретаторах и REPL, например. Cling (C++) и интерпретатор Swift.

Это вторая версия данного интерфейса и его основная «фишка» по сравнению с предыдущей — реализованная модель правил связывания и разрешения символов, которая применяется статическими и динамическими компоновщиками. Это позволяет ORC JIT выполнять произвольный LLVM IR, включая созданный обычным статическими компиляторами (например, clang), используя такие методы, как позднее связывание, видимость символов, слабые ссылки и т.д. Подробнее с примерами тут.

Переработанный код JIT компилятора C++ под LLVM 18 на ORCv2

Исходники кода можно взять тут либо развернуть спойлер.

Портянка кода

#include <iostream> #include <fcntl.h>  #include <llvm-c/Core.h> #include <llvm-c/Support.h> #include <llvm-c/TargetMachine.h>  #include <clang/Frontend/CompilerInstance.h> #include <clang/Frontend/TextDiagnosticPrinter.h> #include <clang/CodeGen/CodeGenAction.h>  #include <llvm/Support/InitLLVM.h> #include <llvm/Support/TargetSelect.h> #include <llvm/InitializePasses.h> #include <llvm/Passes/PassBuilder.h> #include <llvm/ExecutionEngine/Orc/LLJIT.h>  using namespace llvm; using namespace llvm::orc;  /*   * Функции и классы для вызова изнутри JIT  */ namespace ns_stub {      int func_stub(int arg1, short arg2) {         return arg1*arg2;     };      int func_extern_stub() {         return 4242;     };      class class_stub {     public:         int field_1;         static int static_field_2;          static class_stub * create(int a1, int a2) {             return new class_stub(a1, a2);         }          class_stub() {             printf("Call constructor class_stub()\n");             field_1 = 0;         }          class_stub(int arg1, int arg2) {             printf("Call constructor class_stub(%d, %d)\n", arg1, arg2);             field_1 = arg1;             static_field_2 = arg2;         }          virtual ~class_stub() {             printf("Call virtual ~class_stub()\n");         }          int method_sum() {              return field_1 + static_field_2;         }          int method_field1(int arg) {              return field_1;         }          virtual double method_virt2() {              return 999999999;         }          virtual float method_virt() {              return 3.14 + field_1;         }          static float method_static() {             return 3.1415;         }     };      int class_stub::static_field_2 = 0;      class class_full {     public:          class_full() {         }          int method() {             return 42;         }     }; };  /*  * Строка прототип для компиляции в JIT  */ const char * func_text = ""         "extern \"C\" int printf(const char *, ...);\n"         "extern \"C\" int nv_add(int a, int b) {"         "   printf(\"call nv_add(%d, %d)\\n\", a, b);"         "   return a + b;"         "};\n"         ""         "extern \"C\" int nv_sub(int a, int b) {"         "   printf(\"call nv_sub(%d, %d)\\n\", a, b);"         "   return a - b;"         "};\n"         "extern \"C\" int run(){"         "   nv_add(100, 123);"         "   nv_sub(100, 123);"         "   return 42;"         "};\n"         ""         "namespace ns_stub {"         "   class run_internal {"         "       public:\n"         "       run_internal(){};"         "       int method(){"         "           return 43;"         "       };"         "   };"         "   class class_full {"         "       public:\n"         "       class_full();"         "       int method();"         "   };"         ""         "   class class_stub {"         "       public:\n"         "       static class_stub * create(int, int);"         "       class_stub();"         "       class_stub(int arg1, int arg2);"         "       int method_sum();"         "       int method_field1(int);"         "       virtual float method_virt();"         "   };"         ""         "};"         "extern \"C\" int run_internal(){"         "   ns_stub::run_internal cl_int;"         "   printf(\"run_internal.method %d\\n\", cl_int.method());"         "   return 44;"         "};\n"         ""         "extern \"C\" int run_stub(){"         "   ns_stub::class_stub *cl = ns_stub::class_stub::create(123, 123);"         "   printf(\"class_stub.method_sum %d\\n\", cl->method_sum());"         "   delete cl;"         "   return 42;"         "};\n"         ""         "extern \"C\" int run_extern();"         "extern \"C\" int run_extern_stub(){"         "   return run_extern();"         "};\n"         "extern \"C\" int run_virt(){"         "   ns_stub::class_stub *cl = ns_stub::class_stub::create(124, 125);"         "   printf(\"class_stub.method_virt %f\\n\", cl->method_virt());"         "   delete cl;"         "   return 0;"         "};\n"         "";  #define DEBUG_MSG(msg) std::cout << "[DEBUG]: "<<msg<< std::endl;  void InitializeLLVM() {      // 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::initializeTarget(Registry); }  std::unique_ptr<llvm::Module> CompileCpp(std::string source) {     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::string triple = LLVMGetDefaultTargetTriple();      std::vector<std::string> itemstrs;     itemstrs.push_back(triple.insert(0, "-triple="));     itemstrs.push_back("-xc++");     itemstrs.push_back("-std=c++20");     //    itemstrs.push_back("-fno-exceptions");     //    itemstrs.push_back("-funwind-tables");      std::vector<const char*> itemcstrs;     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());         std::cout << itemcstrs.back() << "\n";     }      // Компиляция из памяти     // Send code through a pipe to stdin     int codeInPipe[2];     pipe2(codeInPipe, O_NONBLOCK);     write(codeInPipe[1], source.c_str(), source.size());     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();     //    frontEndOptions.ShowStats = true;      auto& headerSearchOptions = compilerInvocation.getHeaderSearchOpts();     //    headerSearchOptions.Verbose = true;      auto& codeGenOptions = compilerInvocation.getCodeGenOpts();      targetOptions.Triple = LLVMGetDefaultTargetTriple();     compilerInstance.createDiagnostics(textDiagPrinter, false);      DEBUG_MSG("Using target triple: " << triple);      LLVMContextRef ctx = LLVMContextCreate();     std::unique_ptr<clang::CodeGenAction> action = std::make_unique<clang::EmitLLVMOnlyAction>((llvm::LLVMContext *)ctx);      assert(compilerInstance.ExecuteAction(*action));      // Runtime LLVM Module     std::unique_ptr<llvm::Module> module = action->takeModule();      assert(module);      // Оптимизация IR     llvm::PassBuilder passBuilder;     llvm::LoopAnalysisManager loopAnalysisManager;     llvm::FunctionAnalysisManager functionAnalysisManager;     llvm::CGSCCAnalysisManager cGSCCAnalysisManager;     llvm::ModuleAnalysisManager moduleAnalysisManager;      passBuilder.registerModuleAnalyses(moduleAnalysisManager);     passBuilder.registerCGSCCAnalyses(cGSCCAnalysisManager);     passBuilder.registerFunctionAnalyses(functionAnalysisManager);     passBuilder.registerLoopAnalyses(loopAnalysisManager);     passBuilder.crossRegisterProxies(loopAnalysisManager, functionAnalysisManager, cGSCCAnalysisManager, moduleAnalysisManager);      llvm::ModulePassManager modulePassManager = passBuilder.buildPerModuleDefaultPipeline(llvm::OptimizationLevel::O3);     modulePassManager.run(*module, moduleAnalysisManager);      return module; }  ExitOnError ExitOnErr;  ThreadSafeModule createDemoModule() {     auto Context = std::make_unique<LLVMContext>();     auto M = std::make_unique<Module>("test", *Context);      // Create the add1 function entry and insert this entry into module M.  The     // function will have a return type of "int" and take an argument of "int".     Function *Add1F = Function::Create(FunctionType::get(Type::getInt32Ty(*Context),{Type::getInt32Ty(*Context)}, false),     Function::ExternalLinkage, "add1", M.get());      // Add a basic block to the function. As before, it automatically inserts     // because of the last argument.     BasicBlock *BB = BasicBlock::Create(*Context, "EntryBlock", Add1F);      // Create a basic block builder with default parameters.  The builder will     // automatically append instructions to the basic block `BB'.     IRBuilder<> builder(BB);      // Get pointers to the constant `1'.     Value *One = builder.getInt32(1);      // Get pointers to the integer argument of the add1 function...     assert(Add1F->arg_begin() != Add1F->arg_end()); // Make sure there's an arg     Argument *ArgX = &*Add1F->arg_begin(); // Get the arg     ArgX->setName("AnArg"); // Give it a nice symbolic name for fun.      // Create the add instruction, inserting it into the end of BB.     Value *Add = builder.CreateAdd(One, ArgX);      // Create the return instruction and add it to the basic block     builder.CreateRet(Add);      return ThreadSafeModule(std::move(M), std::move(Context)); }  int main(int argc, char *argv[]) {     // Initialize LLVM.     InitLLVM X(argc, argv);      InitializeNativeTarget();     InitializeNativeTargetAsmPrinter();      cl::ParseCommandLineOptions(argc, argv, "HowToUseLLJIT");     ExitOnErr.setBanner(std::string(argv[0]) + ": ");      // Create an LLJIT instance.     auto J = ExitOnErr(LLJITBuilder().create());     //    auto M = createDemoModule();     auto M = ThreadSafeModule(std::move(CompileCpp(func_text)), std::make_unique<LLVMContext>());      std::string dump;     llvm::raw_string_ostream err(dump);      ExecutionSession &ES = J->getExecutionSession();      //    JITDylib *plat = ES.getJITDylibByName("<Platform>");     //    assert(plat);     //    dump.clear();     //    plat->dump(err);     //    std::cout << "<Platform>:\n" << dump << "\n";     //     //    JITDylib *proc = ES.getJITDylibByName("<Process Symbols>");     //    assert(proc);     //    dump.clear();     //    proc->dump(err);     //    std::cout << "<Process Symbols>:\n" << dump << "\n";      ExitOnErr(J->addIRModule(std::move(M)));      // Функция с именем run_extern отсуствует (JIT session error: Symbols not found: [ run_extern ])     // Подставим вместо нее указатель на другу функцию, но с таким же прототипом (func_extern_stub)     const SymbolStringPtr Foo = ES.intern("run_extern");     const ExecutorSymbolDef FooSym(ExecutorAddr::fromPtr(&ns_stub::func_extern_stub), llvm::JITSymbolFlags::Exported | llvm::JITSymbolFlags::Absolute);     auto as = absoluteSymbols({         {Foo, FooSym}     });      if (auto Err = J->getMainJITDylib().define(as)) {         std::cout << "JD.define error: !\n";         return 0;     }      Expected<ExecutorAddr> test = J->lookup("nv_add");     if (!test) {         std::cout << "lookup error:\n" << toString(test.takeError());         return 0;     }      DEBUG_MSG("Retrieving nv_add/nv_sub functions...");      auto addAddr = ExitOnErr(J->lookup("nv_add"));     int (*add)(int, int) = addAddr.toPtr<int(int, int) >();     assert(add);      int res = add(40, 2);     assert(42 == res);      auto subAddr = ExitOnErr(J->lookup("nv_sub"));     int (*sub)(int, int) = subAddr.toPtr<int(int, int) >();     assert(sub);      res = sub(50, 7);     assert(43 == res);      printf("Call: run_internal\n");     auto run_internalAddr = ExitOnErr(J->lookup("run_internal"));     int (*run_internal)() = run_internalAddr.toPtr<int() >();     assert(run_internal);      res = run_internal();     assert(44 == res);      // Линкер удаяет не используемый код,      // и если нет обращения к методу то его будет нельзя вызвать в JIT     // JIT session error: Symbols not found: [ _ZN7ns_stub10class_stub6createEii, _ZN7ns_stub10class_stub10method_sumEv ]     ns_stub::class_stub *cl = ns_stub::class_stub::create(0, 0);     printf("Check run_stub.method %d\n", cl->method_sum());     printf("Check run_stub.method_virt %f\n", cl->method_virt());     delete cl;      printf("Call: run_stub\n");     auto run_stubAddr = ExitOnErr(J->lookup("run_stub"));     int (*run_stub)() = run_stubAddr.toPtr<int() >();     assert(run_stub);      res = run_stub();     assert(42 == res);      printf("Call: run_extern_stub\n");     auto run_extern_stubAddr = ExitOnErr(J->lookup("run_extern_stub"));     int (*run_extern_stub)() = run_extern_stubAddr.toPtr<int() >();     assert(run_extern_stub);      res = run_extern_stub();     assert(4242 == res);      /*      *       * Так нельзя !!!!!       * Виртуальные методы изнутри JIT вызываются неправильно при некорректном заголовочном файле!      *       * ERROR !!!!      * Virtual methods from within JIT are called incorrectly when the header file is incorrect!      *            */      printf("Call: run_virt\n");     auto run_virtAddr = ExitOnErr(J->lookup("run_virt"));     int (*run_virt)() = run_virtAddr.toPtr<int() >();     assert(run_virt);      res = run_virt();     assert(0 == res);      return 0; }

Особенности работы JIT и ограничения

В исходном тексте я добавил комментарии, чтобы в будущем не забыть важные особенности и ограничения данной реализации, ведь это все таки тестовый пример, а не полноценный JIT компилятор C/C++.

  • Если в коде встречаются не разрешенные ссылки, но к ним нет обращения в скомпилированном коде, то все запустится и будет нормально работать даже несмотря на наличие не разрешенных символов.
  • Так как компоновщик во время оптимизации удаляет из финального файла функции к которым не было обращения, то JIT компилятор просто не сможет их найти, после чего выдаст сообщение об ошибке (не удалось разрешить символы). Причем это касается не только обычных функций, но и методов классов.

Ограничения при работе с классами С++:

  • Мне так и не удалось заставить JIT создать экземпляр класса, предоставив ему только прототип без реализации конструктора. Пришлось выкручиваться, создавая статический метод — фабрику экземпляров объектов.
  • При вызове виртуальных методов класса возникают ошибки (вызывается не та функция), что связано с тем, что если прототип класса отличается от скомпилированного в JIT, то вызов виртуального метода (который определяется индексом в таблице виртуальных методов), естественно будет отличатся от реального. Из-за этого могут возникать проблемы с отработкой вызова деструктора объектов при работе JIT (который обычно тоже бывает виртуальным).
  • Если прототип класса переданный в JIT отличается от реального, то к полям объекта лучше вообще не обращаться, так как в этом случае закрешиться легче легкого (обращения к полям происходит по смещению и если прототипы классов отличаются, то ой будет обязательно). Кстати, из подобного подхода получится JIT для хакеров исследователей безопасности 🙂

P.S.

Чуть не забыл отметить очень важный нюанс. Чтобы после линковки приложения с JIT мог разрешить отсуствующие ссылки, программу нужно собирать с аргументами:
-fvisibility=default -Wl,--export-dynamic -rdynamic, которые сообщают линковщику о необходимости экспортировать все символы и переключает их области видимости с visibility=hidden (по умолчанию) на default.


ссылка на оригинал статьи https://habr.com/ru/articles/803787/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *