Это перевод статьи, которая, к сожалению, у меня не доступна без слова из трех букв. А так как тема довольно интересная, то я решил совместить полезное с полезным и не только самому покопаться с примерами из публикации, но и сделать её перевод на Хабре. Вдруг еще кому данный материал будет интересен?
Создание пользовательского расширения компилятора C++ подразумевает понимание базовых механизмов работы компиляторов, изменение или расширение их функциональности и бесшовную интеграцию этих изменений в существующую инфраструктуру компилятора. Это руководство проведет вас через весь процесс, от понимания основ до внедрения и тестирования вашего пользовательского расширения. Целевая аудитория этого руководства — разработчики, которые уже знакомы с C++ и имеют базовое понимание концепций компилятора.
Понимание компиляторов
Прежде чем погрузиться в создание пользовательского расширения компилятора, важно иметь четкое представление о том, что делает компилятор на различных этапах работы. Типичный компилятор выполняет следующие задачи:
- Лексический анализ: Этот этап преобразует исходный код программы в токены (лексемы). Токен — это минимальный текстовый фрагмент (например, идентификаторы, числа, знаки операций и т.д.).
- Синтаксический анализ: также известный как парсинг, этот этап проверяет, образуют ли токены допустимую последовательность в соответствии с грамматикой языка. На этом этапе создается синтаксическое дерево (дерево синтаксического анализа или AST).
- Семантический анализ: Этот этап проверяет синтаксическое дерево на наличие семантических ошибок. Он гарантирует, что дерево разбора следует правилам языка, таким как проверка типов.
- Генерация промежуточного кода: компилятор транслирует дерево синтаксического анализа в промежуточное представление (IR), которое легче оптимизировать и транслировать в машинный код.
- Оптимизация: Промежуточное представление оптимизируется для повышения производительности, например, за счет сокращения количества инструкций.
- Генерация кода: оптимизированный IR транслируется в целевой машинный код.
- Связывание кода: сгенерированный машинный код связывается с библиотеками и другими модулями для создания исполняемого файла.
Популярные компиляторы 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
.
-
Создайте исходный файл плагина. Создайте файл с именем
custom_plugin.c
в каталоге вашего проекта:
#include <gcc-plugin.h>
#include <tree.h>
#include <plugin-version.h>
#include <cp/cp-tree.h>
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
.
- Скомпилируйте плагин Чтобы скомпилировать плагин, используйте следующую команду:
gcc -fPIC -shared -o custom_plugin.so custom_plugin.c -I$(gcc --print-file-name=plugin)/include
Эта команда создает файл динамической библиотеки custom_plugin.so
.
-
Использование плагина Чтобы использовать плагин, вам необходимо передать параметр
-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, который реализует пользовательскую диагностику для функций со слишком большим количеством параметров.
-
Создайте исходный файл плагина. Создайте файл с именем
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<TooManyParamsVisitor> {
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<ASTConsumer> CreateASTConsumer(CompilerInstance &CI, llvm::StringRef) override {
return std::make_unique<TooManyParamsConsumer>(&CI.getASTContext());
}
bool ParseArgs(const CompilerInstance &CI, const std::vector<std::string> &args) override {
return true;
}
};
}
static FrontendPluginRegistry::Add<TooManyParamsAction>
X("too-many-params", "warn about functions with too many parameters");
Этот плагин определяет пользовательский AST visitor, который проверяет количество параметров для каждого объявления функции. Если функция имеет более трех параметров, она выдает предупреждение.
-
Скомпилируйте плагин Чтобы скомпилировать плагин, используйте следующую команду:
clang++ -fPIC -shared -o TooManyParams.so TooManyParams.cpp `llvm-config --cxxflags --ldflags --system-libs --libs all`
Эта команда создает разделяемую библиотеку TooManyParams.so
.
-
Использование плагина Чтобы использовать плагин, вам необходимо передать параметры
-Xclang -load -Xclang
в Clang вместе с путем к файлу плагина:
clang++ -Xclang -load -Xclang ./TooManyParams.so -Xclang -add-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, для написания и запуска автоматизированных тестов.
- Установка 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
-
Написание тестов Создайте тестовый файл с именем
test_example.cpp
в каталоге вашего проекта:
#include <gtest/gtest.h>
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();
}
- Компиляция и запуск тестов Скомпилируйте тестовый файл вместе с исходным файлом, используя Google Test и ваш пользовательский плагин:
clang++ -Xclang -load -Xclang ./TooManyParams.so -Xclang -add-plugin -Xclang too-many-params -o test_example test_example.cpp your_source_file.cpp -lgtest -lgtest_main -pthread
Эта команда компилирует и запускает тест, и вы должны увидеть вывод Google Test, указывающий, пройден ли тест или нет.
Ручное тестирование
В дополнение к автоматизированным тестам вы можете выполнять ручные тесты для проверки поведения ваших пользовательских расширений компилятора. Создавайте различные тестовые случаи с разными сценариями, чтобы обеспечить всестороннее покрытие.
Примеры ручных тестовых случаев
- Функция с параметрами, меньшими или равными трем
void my_function(int a, int b, int c) {
// Function implementation
}
Ожидаемый результат: предупреждение не выдается.
- Функция с более чем тремя параметрами
void my_function(int a, int b, int c, int d) {
// Function implementation
}
Ожидаемый результат: выдано предупреждение «Function has too many parameters».
- Функции с разными сигнатурами
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 -Xclang -add-plugin -Xclang too-many-params -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 значительно более подробно.
UPDATE и дополнительные выводы:
Раз уж решил использовать Хабр как запсисную книжку для сохранения результатов различных экспериментов, подведения итогов поиска в решения проблем и публикации итоговых выводов, то стоит и для это статьи следать UPDATE.
-
Если для загрузки плагина использовать аргумент командной строки
-Xclang -plugin -Xclang <имя плагина>
, то clang после парсинга исходного файла будет выполнять только это плагин и больше ничего. Поэтому если требуется с помощью плагина не только выполнить дополнительные проверки, но и, например, выполнить компиляцию файла, то вместо -plugin следут использовать аргумент -add-plugin. Причем, что интересно, я не нашел этот параметр командрой строки в списке аргументов у clang`a.
-
Не используйте имя плагина с дефисами, иначе вы не сможете передавать ему параметры, так как дефис используется как разделитель между параметром командной строки, именем плагина и непосредственно самим аргуменнтом плагина
-fplugin-arg-<name>-<arg>
nagayev
Хабр торт, спасибо!