Когда-то давно я писал статью-наблюдение про использование Хабра в качестве площадки для архитектурных ревью технических решений практически любой сложности Хабр — ума палата, и продолжаю пользоваться этим способом на постоянной основе.
А теперь, похоже, я нашел еще один очень полезный Хаброхак, который заключается в использования Хабра как записной книжки, но не как в том анекдоте я думал, что это блокнот для заметок, а всем видно оказывается что я здесь написал
, а в самом хорошем смысле — для хранения результатов различных экспериментов, подведения итогов поиска в решения проблем и публикации итоговых выводов.
Так, на днях искал материалы про 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-компиляторов. Как написано в документации, существует несколько вариантов его использования:
- В учебных пособиях по LLVM используется простой JIT-класс на основе ORC для выполнения выражений, скомпилированных из игрушечного языка: калейдоскопа.
- Отладчик LLVM, LLDB, использует JIT кросс-компиляции для оценки выражений. В этом случае кросс-компиляция позволяет выполнять выражения, скомпилированные в процессе отладчика, в целевом процессе отладки, который может находиться на другом устройстве/архитектуре.
- В высокопроизводительных JIT (например, JVM, Julia), которые хотят использовать оптимизации LLVM в существующей JIT-инфраструктуре.
- В интерпретаторах и 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
.
unreal_undead2
А ORC и MCJIT как друг к другу относятся?
rsashka Автор
MCJIT - Это один из старых вариантов реализации JIT c собственным интерфейсом.
А ORC - Это API для JIT независимо от реализации. Сейчас с помощью ORC можно использовать обычный JIT компилятор, как альтернативу старому MCJIT (как в статье) или его расширенную версию LLLazyJIT, которая поддерживает отложенную компиляцию LLVM IR.