[Перевод] ​Как создать собственное расширение компилятора C++

l2d7y9zqxbes7dmdb_yl-tmrg2q.jpeg


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

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


​Понимание компиляторов

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


  1. Лексический анализ: Этот этап преобразует исходный код программы в токены (лексемы). Токен — это минимальный текстовый фрагмент (например, идентификаторы, числа, знаки операций и т.д.).
  2. Синтаксический анализ: также известный как парсинг, этот этап проверяет, образуют ли токены допустимую последовательность в соответствии с грамматикой языка. На этом этапе создается синтаксическое дерево (дерево синтаксического анализа или AST).
  3. Семантический анализ: Этот этап проверяет синтаксическое дерево на наличие семантических ошибок. Он гарантирует, что дерево разбора следует правилам языка, таким как проверка типов.
  4. Генерация промежуточного кода: компилятор транслирует дерево синтаксического анализа в промежуточное представление (IR), которое легче оптимизировать и транслировать в машинный код.
  5. Оптимизация: Промежуточное представление оптимизируется для повышения производительности, например, за счет сокращения количества инструкций.
  6. Генерация кода: оптимизированный IR транслируется в целевой машинный код.
  7. Связывание кода: сгенерированный машинный код связывается с библиотеками и другими модулями для создания исполняемого файла.


​Популярные компиляторы C++

В этом руководстве мы сосредоточимся на двух популярных компиляторах C++ с открытым исходным кодом: GCC (GNU Compiler Collection) и Clang (часть проекта LLVM).


​GCC (GNU Compiler Collection)

GCC — это система компилятора, созданная проектом GNU, поддерживающая различные языки программирования. Это стандартный компилятор для многих Unix-подобных операционных систем, включая Linux. GCC имеет модульную архитектуру, которая допускает расширения и модификации.


Clang/LLVM

Clang — это front-end компилятор для языков программирования C, C++ и Objective-C. Он использует LLVM в качестве back-end`а. LLVM (Low-Level Virtual Machine) — это набор модульных и повторно используемых компиляторов и технологий цепочки инструментов. Clang стремится предоставить легкий и модульный компилятор, который можно использовать для создания более крупных систем.


​Настройка среды разработки

Прежде чем приступить к разработке собственного расширения компилятора, вам необходимо настроить среду разработки.


​Установка GCC

В системе Linux вы можете установить GCC с помощью менеджера пакетов. Например, в Ubuntu \:

sudo apt-get update
sudo apt-get install build-essential gcc-9-plugin-dev

Эта команда устанавливает GCC вместе с другими необходимыми инструментами сборки.


​Установка Clang/LLVM

Аналогичным образом вы можете установить Clang и LLVM на Ubuntu *:

sudo apt-get install clang llvm


*) Инструкции по установке для других операционных систем смотрите в соответствующей документации.


​Настройка проекта

Для этого урока мы создадим каталог проекта, где будем хранить весь наш код и связанные файлы. Создайте новый каталог для вашего проекта:

mkdir CustomCompilerExtension
cd CustomCompilerExtension


​Расширение компилятора GCC

Начнем с расширения компилятора GCC. Предположим, мы хотим добавить пользовательский атрибут к функциям, которые будут вызывать определенное поведение во время компиляции. Это может быть полезно для различных целей, таких как пользовательские оптимизации или настройки генерации кода.


​Понимание плагинов GCC

GCC поддерживает плагины, которые являются динамическими библиотеками, которые загружаются во время выполнения компилятора. Плагины могут расширять GCC, добавляя новые проходы оптимизации, пользовательские атрибуты или даже новые языковые функции.


​Написание плагина GCC

Давайте напишем простой плагин GCC, который вводит новый атрибут под названием custom_attr .


  1. Создайте исходный файл плагина. Создайте файл с именем custom_plugin.c в каталоге вашего проекта:
   #include 
   #include 
   #include 
   #include 

   int plugin_is_GPL_compatible;

   static void handle_custom_attr(tree *node, tree name, tree args, int flags, bool *no_add_attrs) {
       if (TREE_CODE(*node) == FUNCTION_DECL) {
           fprintf(stderr, "Function %s has custom_attr attribute\n", IDENTIFIER_POINTER(DECL_NAME(*node)));
       }
   }

   static struct attribute_spec custom_attr = {
       "custom_attr", 0, 0, false, false, false, handle_custom_attr, false
   };

   static void register_attributes(void *event_data, void *data) {
       register_scoped_attributes(&custom_attr, 1);
   }

   int plugin_init(struct plugin_name_args *plugin_info, struct plugin_gcc_version *version) {
       if (!plugin_default_version_check(version, &gcc_version)) {
           fprintf(stderr, "This GCC plugin is for version %s\n", gcc_version.basever);
           return 1;
       }

       register_callback(plugin_info->base_name, PLUGIN_ATTRIBUTES, register_attributes, NULL);
       return 0;
   }

Этот плагин определяет новый атрибут custom_attr и функцию-обработчик handle_custom_attr. Когда в коде будет встречаться функция с этим атрибутом, обработчик должен быть вывести сообщение в stderr.


  1. Скомпилируйте плагин Чтобы скомпилировать плагин, используйте следующую команду:
gcc -fPIC -shared -o custom_plugin.so custom_plugin.c -I$(gcc --print-file-name=plugin)/include

Эта команда создает файл динамической библиотеки custom_plugin.so.


  1. Использование плагина Чтобы использовать плагин, вам необходимо передать параметр -fplugin в GCC вместе с путем к файлу общего объекта:
gcc -fplugin=./custom_plugin.so -c your_source_file.c

Если your_source_file.c содержит функцию с атрибутом custom_attr, вы должны увидеть соответствующее сообщение, выведенное на stderr.


​Пример использования

Рассмотрим следующий исходный файл C++ example.cpp:

void __attribute__((custom_attr)) my_function() {
    // Function implementation
}

Скомпилируйте его с помощью специального плагина:

gcc -fplugin=./custom_plugin.so -o example example.cpp

Вы должны увидеть сообщение «Function my_function has custom_attr attribute», выведенное на stderr .


​Расширение компилятора Clang

Далее мы расширим компилятор Clang. Предположим, мы хотим добавить пользовательскую диагностику, которая предупреждает, когда функция имеет больше указанного количества параметров.


​Понимание плагинов Clang

Clang поддерживает плагины, которые позволяют вам расширить его возможности. Плагины могут добавлять новые диагностики, посетителей AST или даже пользовательские преобразования кода.


​Написание плагина Clang

Давайте напишем плагин Clang, который реализует пользовательскую диагностику для функций со слишком большим количеством параметров.


  1. Создайте исходный файл плагина. Создайте файл с именем TooManyParams.cpp в каталоге вашего проекта:
   #include "clang/AST/AST.h"
   #include "clang/Frontend/FrontendPluginRegistry.h"
   #include "clang/Frontend/CompilerInstance.h"
   #include "clang/AST/RecursiveASTVisitor.h"
   #include "clang/Basic/Diagnostic.h"

   using namespace clang;

   namespace {

   class TooManyParamsVisitor : public RecursiveASTVisitor {
   public:
       explicit TooManyParamsVisitor(ASTContext *Context)
           : Context(Context) {}

       bool VisitFunctionDecl(FunctionDecl *D) {
           if (D->param_size() > 3) {
               DiagnosticsEngine &Diag = Context->getDiagnostics();
               unsigned DiagID = Diag.getCustomDiagID(DiagnosticsEngine::Warning, "Function has too many parameters");
               Diag.Report(D->getLocation(), DiagID);
           }
           return true;
       }

   private:
       ASTContext *Context;
   };

   class TooManyParamsConsumer : public ASTConsumer {
   public:
       explicit TooManyParamsConsumer(ASTContext *Context)
           : Visitor(Context) {}

       void HandleTranslationUnit(ASTContext &Context) override {
           Visitor.TraverseDecl(Context.getTranslationUnitDecl());
       }

   private:
       TooManyParamsVisitor Visitor;
   };

   class TooManyParamsAction : public PluginASTAction {
   protected:
       std::unique_ptr CreateASTConsumer(CompilerInstance &CI, llvm::StringRef) override {
           return std::make_unique(&CI.getASTContext());
       }

       bool ParseArgs(const CompilerInstance &CI, const std::vector &args) override {
           return true;
       }
   };

   }

   static FrontendPluginRegistry::Add 
   X("too-many-params", "warn about functions with too many parameters");

Этот плагин определяет пользовательский AST visitor, который проверяет количество параметров для каждого объявления функции. Если функция имеет более трех параметров, она выдает предупреждение.


  1. Скомпилируйте плагин Чтобы скомпилировать плагин, используйте следующую команду:
    clang++ -fPIC -shared -o TooManyParams.so TooManyParams.cpp `llvm-config --cxxflags --ldflags --system-libs --libs all`

Эта команда создает разделяемую библиотеку TooManyParams.so.


  1. Использование плагина Чтобы использовать плагин, вам необходимо передать параметры -Xclang -load -Xclang в Clang вместе с путем к файлу плагина:
    clang++ -Xclang -load -Xclang ./TooManyParams.so -Xclang -plugin -Xclang too-many-params your_source_file.cpp

Если your_source_file.cpp содержит функцию с более чем тремя параметрами Вы должны увидеть:

your_source_file.cpp:1:6: warning: Function has too many parameters
    1 | void my_function_clang(int a, int b, int c, int d) {
      |      ^
1 warning generated.


​Интеграция и тестирование пользовательских расширений

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


​Автоматизированное тестирование

Автоматизированные тесты помогают гарантировать, что ваши пользовательские расширения компилятора работают правильно и согласованно. Вы можете использовать тестовые фреймворки или писать пользовательские скрипты для автоматизации процесса тестирования.


​Использование фреймворка тестирования

Для проектов C++ вы можете использовать такие фреймворки, как Google Test или Catch2, для написания и запуска автоматизированных тестов.


  1. Установка Google Test В Ubuntu вы можете установить Google Test с помощью менеджера пакетов:
   sudo apt-get install libgtest-dev

Затем скомпилируйте библиотеку Google Test:

   cd /usr/src/gtest
   sudo cmake CMakeLists.txt
   sudo make
   sudo cp lib/*.a /usr/lib


  1. Написание тестов Создайте тестовый файл с именем test_example.cpp в каталоге вашего проекта:
    #include 

    extern void my_function(int, int, int, int);

    TEST(MyFunctionTest, TooManyParams) {
       EXPECT_NO_FATAL_FAILURE(my_function(1, 2, 3, 4));
    }

int main(int argc, char **argv) {
       ::testing::InitGoogleTest(&argc, argv);
       return RUN_ALL_TESTS();
}


  1. Компиляция и запуск тестов Скомпилируйте тестовый файл вместе с исходным файлом, используя Google Test и ваш пользовательский плагин:
clang++ -Xclang -load -Xclang ./TooManyParams.so -o test_example test_example.cpp your_source_file.cpp -lgtest -lgtest_main -pthread

Эта команда компилирует и запускает тест, и вы должны увидеть вывод Google Test, указывающий, пройден ли тест или нет.


​Ручное тестирование

В дополнение к автоматизированным тестам вы можете выполнять ручные тесты для проверки поведения ваших пользовательских расширений компилятора. Создавайте различные тестовые случаи с разными сценариями, чтобы обеспечить всестороннее покрытие.


​Примеры ручных тестовых случаев


  1. Функция с параметрами, меньшими или равными трем
void my_function(int a, int b, int c) {
       // Function implementation
}

Ожидаемый результат: предупреждение не выдается.


  1. Функция с более чем тремя параметрами
void my_function(int a, int b, int c, int d)  {
       // Function implementation
}

Ожидаемый результат: выдано предупреждение «Function has too many parameters».


  1. Функции с разными сигнатурами
   void my_function(int a) {
       // Function implementation
   }

   void another_function(double a, double b, double c, double d, double e) {
       // Function implementation
   }

Ожидаемый результат: предупреждение выдается только для another_function .


​Непрерывная интеграция

Интеграция ваших пользовательских расширений компилятора в конвейер непрерывной интеграции (CI) гарантирует, что они будут автоматически тестироваться при каждом изменении кода. Вы можете использовать службы CI, такие как GitHub Actions, Travis CI или Jenkins, чтобы настроить автоматическое тестирование и развертывание.


​Пример рабочего процесса GitHub Actions

Создайте файл .github/workflows/ci.yml в вашем репозитории:

name: CI

on: [push, pull_request]

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
    - name: Checkout code
      uses: actions/checkout@v2

    - name: Install dependencies
      run: sudo apt-get install -y clang llvm libgtest-dev cmake

    - name: Compile Google Test
      run: |
        cd /usr/src/gtest
        sudo cmake CMakeLists.txt
        sudo make
        sudo cp *.a /usr/lib

    - name: Build custom plugin
      run: clang++ -fPIC -shared -o TooManyParams.so TooManyParams.cpp `llvm-config --cxxflags --ldflags --system-libs --libs all`

    - name: Run tests
      run: |
        clang++ -Xclang -load -Xclang ./TooManyParams.so -o test_example test_example.cpp example.cpp -lgtest -lgtest_main -pthread
        ./test_example

Этот рабочий процесс проверяет ваш код, устанавливает необходимые зависимости, компилирует библиотеку Google Test и ваш пользовательский плагин, а также запускает тесты.


​Заключение

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


​Дальнейшее чтение

Понимая внутреннее устройство компиляторов и экспериментируя с пользовательскими расширениями, вы можете открыть новые возможности для оптимизации и анализа вашего кода C++. Удачного кодирования!


P.S.


При работе примерами кода из статьи у меня не получилось собрать плагин для GCC, поэтому код я оставил код статьи без изменений. А плагин для Clang собрался и корректно отработал. Правда я не проверял запуск тестов и примеры с CI, поэтому за их корректность ручаться не могу.


P.P. S.


Пока разбирался с запуском плагина для clang нашел еще одну статью десятилетней давности на тему разработки плагинов для компилятора, в которой описано создание плагина для Clang значительно более подробно.

Habrahabr.ru прочитано 6236 раз