Динамическая JIT компиляция С/С++ в LLVM с помощью Clang

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.


При создании компилятора для собственного языка программирования я сделал его как транспайлер в исходный код на С++, вот только реализация сильно подкачала. Сначала приходится генерировать динамическую библиотеку с помощью вызова 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 заключается в том, что он не может анализировать файл из памяти, даже если для этого есть классы. Причина в том, что в экземпляре компилятора он проверяет, является ли ввод файлом.
Первоначальный пример был сделан для 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 (т.к. githab.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 компиляция С++ кода, это очень круто!



Исходники примера 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;
}



Источник: https://habr.com/ru/company/timeweb/blog/572878/


Интересные статьи

Интересные статьи

Системы мониторинга — очень нужная для админа вещь, ведь они позволяют получать от сервисов метрики, которые:
ML на основе нейросетей открывает для программного обеспечения новые возможности в области логического вывода. Как правило, ML-модели выполняются в облаке, а это означает, что для кла...
Новая методика взлома преодолевает проблему «джиттера сети», которая может влиять на успешность атак по сторонним каналам Новая методика, разработанная исследователями Левенско...
Бинауральные ритмы, вполне возможно, могут влиять на мозговые волны. И магии в этом на самом деле меньше, чем может показаться. Что, если я скажу вам, что есть способ улучшить концентрацию и...
Тема довольно изъезжена, знаю. К примеру, есть отличная статья, но там рассматривается только IP-часть блоклиста. Мы же добавим еще и домены. В связи с тем, что суды и РКН блокируют всё направ...