вторник, 21 февраля 2012 г.

Пишем плагин для gcc. Часть 1.

Одной из крайне интересных фич, внедрённых в gcc 4.5 стала возможность создания плагинов для компилятора. К сожалению, в сети пока очень мало информации, описывающей данную возможность, а на русском языке её нет совсем. Что ж, постараюсь исправить эту ситуацию.


Перед началом хотелось бы указать небольшой (больше просто не нашёл) список статей, касающихся данной темы:
  1. документация gcc, описывающая построение плагинов
  2. цикл статей по парсингу C++ при помощи плагинов gcc
  3. введение в создание плагинов gcc
Итак, пора начинать. Т.к. эту статью я пишу, параллельно создавая свой первый плагин для gcc, то здесь будет мало теории, но будет написан, скомпилирован и применён при компиляции совершенно бесполезный продукт.

Для начала уточним, где на конкретной системе находятся заголовочные файлы с плагинами:

$ gcc -print-file-name=plugin
/usr/lib/gcc/i686-pc-linux-gnu/4.5.3/plugin


круто, в нашей системе у gcc реально есть плагины ;) Каждый плагин должен подключать заголовочный файл gcc-plugin.h. В нём описаны структуры, содержащие информацию о плагине, а также объявлены все основные функции.

Обязательной для любого плагина является функция plugin_init. Она вызывается сразу после загрузки плагина. Эта функция отвечает за инициализацию данных плагина, а также за регистрацию всех callback вызовов, которые будут применены в ходе его работы.

Ещё одним обязательным условием работы плагина является его совместимость с лицензией GPL. Да, да, именно обязательным условием, без этого gcc просто откажется с ним работать. Чтобы подтвердить свою приверженность идеям свободного ПО, нужно в плагине объявить глобальную переменную plugin_is_GPL_compatible. Теперь у нас есть всё что необходимо для создания минимального плагина. Вот его код:

#include "gcc-plugin.h"

int plugin_is_GPL_compatible;

int plugin_init (struct plugin_name_args *plugin_info,
                 struct plugin_gcc_version *version )
{

    return 0;
}


Как видим, всё не так страшно. Осталось скомпилировать и применить. Делается это очень просто. Сначала создаём разделяемую библиотеку:

gcc -I`gcc -print-file-name=plugin`/include -fPIC -shared plugin.c -o plugin.so

Затем при компиляции программы показываем gcc, что стоит использовать данный плагин:

gcc -fplugin=./plugin.so main.c
$ ./a.out
Hello, world


Программа main.c вполне корректно компилируется и работает. Но всё таки осталось ощущение неуверенности. А вдруг плагин не отработал? Что ж, справедливый вопрос. Чтобы развеять эти сомнения, надо добавить вывод какой-нибудь информации. Как Вы могли заметить, на вход инициализирующей функции подаётся 2 аргумента с информацией о плагине и о самом gcc. Как раз то, что нужно. Добавим в нашу функцию следущие строки:

printf("Plugin info:\n");
    printf("Plugin short name (filename without .so suffix): %s\n", plugin_info->base_name);
    printf("Path to the plugin as specified with -fplugin=: %s\n", plugin_info->full_name);
    printf("Number of arguments specified with -fplugin-arg-...: %d\n", plugin_info->argc);
    printf("Version string provided by plugin: %s\n", plugin_info->version);
    printf("Help string provided by plugin: %s\n", plugin_info->help);
    printf("\nGCC info:\n");
    printf("Base version: %s\n", version->basever);
    printf("Datestamp: %s\n", version->datestamp);
    printf("Devphase: %s\n", version->devphase);
    printf("Revision: %s\n", version->revision);
    printf("Configuration arguments: %s\n", version->configuration_arguments);


Перекомпиливаем:

$ gcc -I`gcc -print-file-name=plugin`/include -fPIC -shared plugin.c -o plugin.so
$ gcc -fplugin=./plugin.so main.cpp
Plugin info:
Plugin short name (filename without .so suffix): plugin
Path to the plugin as specified with -fplugin=: ./plugin.so
Number of arguments specified with -fplugin-arg-...: 0
Version string provided by plugin: (null)
Help string provided by plugin: (null)

GCC info:
Base version: 4.5.3
Datestamp: 20110428
Devphase:
Revision:
Configuration arguments: /var/tmp/portage/sys-devel/gcc-4.5.3-r2/work/gcc-4.5.3/configure --prefix=/usr --bindir=/usr/i686-pc-linux-gnu/gcc-bin/4.5.3 --includedir=/usr/lib/gcc/i686-pc-linux-gnu/4.5.3/include --datadir=/usr/share/gcc-data/i686-pc-linux-gnu/4.5.3 --mandir=/usr/share/gcc-data/i686-pc-linux-gnu/4.5.3/man --infodir=/usr/share/gcc-data/i686-pc-linux-gnu/4.5.3/info --with-gxx-include-dir=/usr/lib/gcc/i686-pc-linux-gnu/4.5.3/include/g++-v4 --host=i686-pc-linux-gnu --build=i686-pc-linux-gnu --disable-altivec --disable-fixed-point --with-ppl --with-cloog --disable-ppl-version-check --with-cloog-include=/usr/include/cloog-ppl --disable-lto --enable-nls --without-included-gettext --with-system-zlib --disable-werror --enable-secureplt --disable-multilib --enable-libmudflap --disable-libssp --enable-libgomp --with-python-dir=/share/gcc-data/i686-pc-linux-gnu/4.5.3/python --enable-checking=release --disable-libgcj --with-arch=i686 --enable-languages=c,c++,fortran --enable-shared --enable-threads=posix --enable-__cxa_atexit --enable-clocale=gnu --enable-targets=all --with-bugurl=http://bugs.gentoo.org/ --with-pkgversion='Gentoo 4.5.3-r2 p1.0, pie-0.4.6'


Да, оно действительно работает! Осталось только нанести последние штрихи. А именно заполнить информацию о версии и справку:

plugin_info->version = "0.01";
plugin_info->help = "This plugin does nothing\n";

Теперь, если перекомпилировать плагин, он будет выводить всё корректно. Вот его итоговый код:

#include "gcc-plugin.h"

#include <stdio.h>

int plugin_is_GPL_compatible;

int plugin_init (struct plugin_name_args *plugin_info,
                 struct plugin_gcc_version *version )
{
    plugin_info->version = "0.01";
    plugin_info->help = "This plugin does nothing\n";

    printf("Plugin info:\n");
    printf("Plugin short name (filename without .so suffix): %s\n", plugin_info->base_name);
    printf("Path to the plugin as specified with -fplugin=: %s\n", plugin_info->full_name);
    printf("Number of arguments specified with -fplugin-arg-...: %d\n", plugin_info->argc);
    printf("Version string provided by plugin: %s\n", plugin_info->version);
    printf("Help string provided by plugin: %s\n", plugin_info->help);
    printf("\nGCC info:\n");
    printf("Base version: %s\n", version->basever);
    printf("Datestamp: %s\n", version->datestamp);
    printf("Devphase: %s\n", version->devphase);
    printf("Revision: %s\n", version->revision);
    printf("Configuration arguments: %s\n", version->configuration_arguments);
    return 0;
}


10 комментариев:

  1. Я не очень понял зачем это может быть полезно.
    Так и свои ключевые слова вводить можно и AST менять?
    Т.е. можно будет в С сделать мемоизацию, кеширование, декораторы, карирование, асинхронный вызов и прочее?

    ОтветитьУдалить
    Ответы
    1. В принципе - да. Хоть под jvm его портировать. Например, есть DragonEgg (http://dragonegg.llvm.org/), который под LLVM байт-код генерит. Часто используется для получения AST с целью последующего анализа или оптимизации. Если порыться в расширениях gcc, то там много интересного найти можно, например объявление вложенных функций :)

      Удалить
  2. А в промышленных масштабах это смысл делать имеет?
    Для erlang есть parse_transform. Он просто позволяет менять AST.
    http://erlanger.ru/page/1574/monady-i-srezy-v-erlang-e-erlando и https://github.com/nbowe/erlang_decorators
    хочется использовать для больших проектов, ибо сильно уменьшают размер кода и добавляют всяких удобств. Несколько пугают сами изменения AST, и то что это таки хак компилятора, а не стандарт языка.

    ОтветитьУдалить
    Ответы
    1. Я бы назвал это не хаком, а штатной возможностью. Если тебе просто неудобен синтаксис языка, то наверное нет, не стоит. А если тебе нужно скомпилировать код под собственную платформу, или ты считаешь, что придумал крутую оптимизацию, то других вариантов нет. Более того это довольно портабельный способ, т.к. оригинальный код будет работать и без плагина.

      Лично я планировал использовать плагин для получения AST и его анализа во внешней среде. Но как-то пока что забил.

      Удалить
    2. Привет! Как, руки дошли у Вас до получения AST при помощи плагинов к GCC или CLang? Может, порекомендуете по этой теме, что можно почитать для обучения? Хотел попробовать поэкспериментировать с расширением возможностей С++. Может, проекты какие знаете на эту тему (кроме Qt)?

      Удалить
    3. Привет.

      Руки не дошли, и на вряд ли дойдут. Но вообще по gcc в посте есть ссылка сюда: http://codesynthesis.com/~boris/blog/2010/05/10/parsing-cxx-with-gcc-plugin-part-2/. Данный пост писался на основе первой статьи из цикла. Там тупым копированием исходников скорей всего не получится - в новых версиях gcc могли интерфейсы поменяться, но в целом по ней можно что-нибудь сделать.

      Если Вам просто "поиграться", о думаю можно посмотреть в MELT - он (судя по описанию) работает уже не с AST, а с GIMPLE, но оно тоже полезно будет. Ещё можете посмотреть в плагин визуализации аналитических структур gcc: https://code.google.com/p/gcc-vcg-plugin/.

      Вообще лучше, конечно, начать с clang: http://clang.llvm.org/docs/ClangPlugins.html

      А какую задачу Вы хотите решить?

      Удалить
    4. Задача - чисто "поиграться". Посетила мысль, что С++ же мощный язык... ...Его пытаются "расширять", активно используя макросы (в Boost'е том же) - это не есть хорошее решение... ...А вот добавить нужные фичи, имея возможность получать AST кода, это вроде как, очевидно, но что-то люди всё пытаются с нуля делать даже языки с C-подобным синтаксисом, а не на базе C++. Это очень странно.

      За ссылки спасибо.

      Удалить
  3. Что, если после команды gcc -print-file-name=plugin выводит только слово plugin? Как дальше поступать?

    ОтветитьУдалить
    Ответы
    1. А можно по подробнее? Какая версия gcc? Сами собирали? Дистрибутивная?

      У меня на Arch'е, например, плагины лежат здесь: /usr/lib/gcc/i686-pc-linux-gnu/4.9.1/plugin. Думаю путь более или менее стандартный.

      Удалить