26 ноября 2010 г.

Clang API: получение AST в виде XML


Задача: с помощью Clang API получить XML-файл, описывающий абстрактное синтаксическое дерево (AST) исходного кода. Это можно сделать и простым запуском команды clang с параметром -ast-print-xml в командной строке, но через API — интереснее :)

Библиотеки clang'а еще не входят в официальные дистрибутивы, поэтому первое, что нужно сделать — выгрузить clang из репозитория и собрать. Clang хорош тем, что собрать его можно и для Unix-подобных систем и для Windows. Я буду использовать clang версии 2.8, так как на данный момент она является последней стабильной версией. К сожалению, clang собирается только как часть процесса сборки LLVM, поэтому, если в системе уже установлен LLVM, то его всё равно придется пересобирать.

Теперь создадим тестовый файл с исходным кодом на C++, для которого будем строить AST, назовем его a.cpp и положим куда-нибудь недалеко. Вот он (файл можно взять здесь):

  1. struct MyClass
  2. {
  3.   unsigned long _a;
  4.   mutable unsigned long _b;
  5.  
  6.   MyClass () : _a(4), _b(44) {}
  7.   unsigned long func ()
  8.   {
  9.     unsigned long r = _a + _b;
  10.     return r;
  11.   }
  12. };

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

Первая сущность, с которой предстоит познакомиться, это CompilerInstance — класс, который является фронтэндом компилятора и выполняет всю "грязную" и "неинтересную" работу. Он находится в пространстве имен clang и создаётся просто:

auto_ptr<CompilerInstance> compiler(new CompilerInstance);

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

compiler->createDiagnostics(0, NULL); // to stdout
assert(compiler->hasDiagnostics());

Компилятор есть, можно начинать с ним работать — нужно передать в компилятор входные параметры командной строки, необходимые для его инициализации:

const char *args[] =
{
  "-cc1", // вызов LLVM Clang
  "a.cpp" // входной файл
};

clang::CompilerInvocation::CreateFromArgs(
  compiler->getInvocation(),
  args,
  args + 2,
  compiler->getDiagnostics());
assert(0 == compiler->getDiagnostics().getNumErrors());

Теперь нужно создать класс-действие и передать его компилятору на запуск. Так как мы хотим сгенерировать AST в виде XML-файла, то нужно создать экземпляр класса ASTPrintXMLAction, который является наследником класса FrontendAction, и затем выполнить действие с помощью метода CompilerInstance::ExecuteAction():

auto_ptr<FrontendAction> action(new ASTPrintXMLAction);

bool actionSuccessful = compiler->ExecuteAction(*action);
assert(0 == compiler->getDiagnostics().getNumErrors());
assert(actionSuccessful);

Всё, что нужно, уже написано. Теперь программу нужно скомпилировать, слинковать с библиотеками clang и LLVM и запустить.

Если всё прошло успешно, то в директории, где мы запускаем исполняемый файл, появится файл a.xml, содержащий AST файла a.cpp.

Инициализация фронтэнда вручную

Теперь попробуем не использовать метод CompilerInvocation::CreateFromArgs(), а инициализировать фронтэнд компилятора самостоятельно (будем использовать минимальный набор параметров для успешного генерирования AST). Начало такое же, как и в предыдущем примере. Создаём компилятор и диагностику:

auto_ptr<CompilerInstance> compiler(new CompilerInstance);
compiler->createDiagnostics(0, NULL); // to stdout
assert(compiler->hasDiagnostics());

Зададим входной файл, ну и выходной за компанию:

pair<InputKind, string> input(IK_CXX, "a.cpp");
compiler->getFrontendOpts().Inputs.push_back(input);

compiler->getFrontendOpts().OutputFile = "ast.xml";

Теперь нужно указать платформу, а то без нее никуда, и указать язык исходного кода, так как указания типа входного файла IK_CXX недостаточно (в данном случае там вообще можно указать IK_None):

compiler->getTargetOpts().Triple = "x86_64"; // x86, alpha, ppc, ppc64, ...
compiler->getLangOpts().CPlusPlus = 1;

Теперь создаем экземпляр класса-действия и выполняем его:

auto_ptr<FrontendAction> action(new ASTPrintXMLAction);

bool actionSuccessful = compiler->ExecuteAction(*action);
assert(0 == compiler->getDiagnostics().getNumErrors());
assert(actionSuccessful);

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


UPD: Получившийся XML-файл можно взять здесь.
Исходный код для тестов - здесь.

UPD2: После того, как вы соберете llvm и clang (и выполните make install), у вас появится исполняемый файл llvm-config, который вам очень поможет при компиляции и линковке ваших собственных проектов.

  • Директории с заголовочными файлами: llvm-config --includedir
  • С/C++ флаги: llvm-config --cppflags | --cflags | --cxxflags
  • LD флаги: llvm-config --ldflags
  • Либы: llvm-config --libs (только llvm-либы, либы clang вам придется прописывать руками)
  • И многое другое: llvm-config --help