- PVSM.RU - https://www.pvsm.ru -

Это перевод статьи [1], которая, к сожалению, у меня не доступна без слова из трех букв. А так как тема довольно интересная, то я решил совместить полезное с полезным и не только самому покопаться с примерами из публикации [2], но и сделать её перевод на Хабре. Вдруг еще кому данный материал будет интересен?
Создание пользовательского расширения компилятора C++ подразумевает понимание базовых механизмов работы компиляторов, изменение или расширение их функциональности и бесшовную интеграцию этих изменений в существующую инфраструктуру компилятора. Это руководство проведет вас через весь процесс, от понимания основ до внедрения и тестирования вашего пользовательского расширения. Целевая аудитория этого руководства — разработчики, которые уже знакомы с C++ и имеют базовое понимание концепций компилятора.
Прежде чем погрузиться в создание пользовательского расширения компилятора, важно иметь четкое представление о том, что делает компилятор на различных этапах работы. Типичный компилятор выполняет следующие задачи:
В этом руководстве мы сосредоточимся на двух популярных компиляторах C++ с открытым исходным кодом: GCC (GNU Compiler Collection) и Clang (часть проекта LLVM).
GCC — это система компилятора, созданная проектом GNU, поддерживающая различные языки программирования. Это стандартный компилятор для многих Unix-подобных операционных систем, включая Linux. GCC имеет модульную архитектуру, которая допускает расширения и модификации.
Clang — это front-end компилятор для языков программирования C, C++ и Objective-C. Он использует LLVM в качестве back-end`а. LLVM (Low-Level Virtual Machine) — это набор модульных и повторно используемых компиляторов и технологий цепочки инструментов. Clang стремится предоставить легкий и модульный компилятор, который можно использовать для создания более крупных систем.
Прежде чем приступить к разработке собственного расширения компилятора, вам необходимо настроить среду разработки.
В системе Linux вы можете установить GCC с помощью менеджера пакетов. Например, в Ubuntu :
sudo apt-get update
sudo apt-get install build-essential gcc-9-plugin-dev
Эта команда устанавливает GCC вместе с другими необходимыми инструментами сборки.
Аналогичным образом вы можете установить Clang и LLVM на Ubuntu *:
sudo apt-get install clang llvm
*) Инструкции по установке для других операционных систем смотрите в соответствующей документации.
Для этого урока мы создадим каталог проекта, где будем хранить весь наш код и связанные файлы. Создайте новый каталог для вашего проекта:
mkdir CustomCompilerExtension
cd CustomCompilerExtension
Начнем с расширения компилятора 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 attributen", 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 %sn", 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 поддерживает плагины, которые позволяют вам расширить его возможности. Плагины могут добавлять новые диагностики, посетителей AST или даже пользовательские преобразования кода.
Давайте напишем плагин 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, для написания и запуска автоматизированных тестов.
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();
}
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/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, от написания простых плагинов до их интеграции и тестирования. Выполнив эти шаги, вы сможете создавать мощные и гибкие расширения компилятора, соответствующие вашим конкретным требованиям.
Внутренняя документация GCC [5]
Документация по плагинам Clang [6]
Руководство программиста LLVM [7]
Плагины GCC [8]
Документация по тестированию Google [10]
Понимая внутреннее устройство компиляторов и экспериментируя с пользовательскими расширениями, вы можете открыть новые возможности для оптимизации и анализа вашего кода C++. Удачного кодирования!
При работе примерами кода из статьи у меня не получилось собрать плагин для GCC, поэтому код статьи я оставил без изменений. А плагин для Clang собрался и корректно отработал. Правда я не проверял запуск тестов и примеры с CI, поэтому за их корректность ручаться не могу.
Пока разбирался с запуском плагина для clang нашел еще одну статью десятилетней давности [11] на тему разработки плагинов для компилятора, в которой описано создание плагина для Clang значительно более подробно.
Раз уж решил использовать Хабр как запсисную книжку [12] для сохранения результатов различных экспериментов, подведения итогов поиска в решения проблем и публикации итоговых выводов, то стоит и для это статьи следать UPDATE.
Если для загрузки плагина использовать аргумент командной строки -Xclang -plugin -Xclang <имя плагина>, то clang после парсинга исходного файла будет выполнять только это плагин и больше ничего. Поэтому если требуется с помощью плагина не только выполнить дополнительные проверки, но и, например, выполнить компиляцию файла, то вместо -plugin следут использовать аргумент -add-plugin. Причем, что интересно, я не нашел этот параметр командрой строки в списке аргументов у clang`a [13].
Не используйте имя плагина с дефисами, иначе вы не сможете передавать ему параметры, так как дефис используется как разделитель между параметром командной строки, именем плагина и непосредственно самим аргуменнтом плагина -fplugin-arg-<name>-<arg> [14]
Автор: rsashka
Источник [15]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/c-3/406547
Ссылки в тексте:
[1] статьи: https://www.w3computing.com/articles/how-to-create-a-custom-cpp-compiler-extension/
[2] примерами из публикации: https://github.com/rsashka/CustomCompilerExtension.git
[3] Токен: https://ru.wikipedia.org/wiki/%D0%9B%D0%B5%D0%BA%D1%81%D0%B8%D1%87%D0%B5%D1%81%D0%BA%D0%B8%D0%B9_%D0%B0%D0%BD%D0%B0%D0%BB%D0%B8%D0%B7
[4] AST: https://ru.wikipedia.org/wiki/%D0%90%D0%B1%D1%81%D1%82%D1%80%D0%B0%D0%BA%D1%82%D0%BD%D0%BE%D0%B5_%D1%81%D0%B8%D0%BD%D1%82%D0%B0%D0%BA%D1%81%D0%B8%D1%87%D0%B5%D1%81%D0%BA%D0%BE%D0%B5_%D0%B4%D0%B5%D1%80%D0%B5%D0%B2%D0%BE
[5] Внутренняя документация GCC: https://gcc.gnu.org/onlinedocs/gccint/
[6] Документация по плагинам Clang: https://clang.llvm.org/docs/ClangPlugins.html
[7] Руководство программиста LLVM: https://llvm.org/docs/ProgrammersManual.html
[8] Плагины GCC: https://gcc.gnu.org/wiki/plugins
[9] Документация Clang: https://clang.llvm.org/docs/
[10] Документация по тестированию Google: https://github.com/google/googletest
[11] статью десятилетней давности: https://habr.com/ru/articles/214275/
[12] Хабр как запсисную книжку: https://habr.com/ru/articles/803787/
[13] списке аргументов у clang`a: https://clang.llvm.org/docs/ClangCommandLineReference.html
[14] -fplugin-arg-<name>-<arg>: https://clang.llvm.org/docs/ClangCommandLineReference.html#cmdoption-clang-fplugin-arg-name-arg
[15] Источник: https://habr.com/ru/articles/870840/?utm_source=habrahabr&utm_medium=rss&utm_campaign=870840
Нажмите здесь для печати.