Создание действующего компилятора с помощью инфраструктуры LLVM. Часть 2

Использование clang для первичной обработки кода на языке C/C++

Инфраструктура компилятора LLVM предоставляет мощные возможности для оптимизации приложений вне зависимости от используемого языка программирования. Во второй статье этого цикла из двух частей описывается применение API-интерфейса clang в среде LLVM для первичной обработки кода на языке C/C++.

Арпан Сен, технический директор, Synapti Computer Aided Design Pvt Ltd

Арпан Сен (Arpan Sen) – ведущий инженер, работающий над разработкой программного обеспечения в области автоматизации электронного проектирования. На протяжении нескольких лет он работал над некоторыми функциями UNIX, в том числе Solaris, SunOS, HP-UX и IRIX, а также Linux и Microsoft Windows. Он проявляет живой интерес к методикам оптимизации производительности программного обеспечения, теории графов и параллельным вычислениям. Арпан является аспирантов в области программных систем.



12.11.2012

Другие статьи данного цикла

Просмотреть другие статьи данного цикла: Создание действующего компилятора с помощью инфраструктуры LLVM.

В первой статье данной серии рассматривалось т. н. "промежуточное представление" (intermediate representation, IR) программного кода для инфраструктуры LLVM. Вы "вручную" создали тестовую программу "Hello World", изучили некоторые нюансы LLVM, в том числе типизацию, а затем создали аналогичную программу с помощью предлагаемых инфраструктурой LLVM интерфейсов прикладного программирования (API-интерфейсов). В ходе этого процесса вы также изучили некоторые инструменты инфраструктуры LLVM, в том числе llc и lli, и узнали о том, как использовать инструмент llvm-gcc для создания кода LLVM IR. Во второй и заключительной статье данной серии рассматривается несколько других интересных вещей, которые можно сделать с помощью LLVM. В частности, здесь идет речь об инструментовке программного кода – добавлении информации к итоговому исполняемому файлу, который мы генерируем. В статье также рассматривается фронтенд clang для инфраструктуры LLVM, поддерживающий такие языки, как C, C++ и Objective-C. Мы будем применим API-интерфейс clang для первичной обработки и генерации абстрактного синтаксического дерева (abstract syntax tree, AST) для кода на C/C++.

Проходы LLVM

Инфраструктура LLVM хорошо известна своими оптимизационными возможностями. Оптимизация реализуется посредством т.н. "проходов" (ссылка на общее описание проходов LLVM приведена в разделе Ресурсы). В этой связи необходимо отметить, что LLVM позволяет создавать служебные проходы с минимальным объемом кода. Например, если вы не хотите, чтобы имена ваших функций начинались с "hello", вы можете использовать для этого отдельный служебный проход.

LLVM-инструмент opt

Согласно руководству по инструменту opt, "команда opt представляет собой модульный оптимизатор/анализатор для инфраструктуры LLVM". Если вы подготовили программный код для специального прохода, вы можете скомпилировать его в виде совместно используемой библиотеки, а затем загрузить с помощью opt. В случае успешной установки LLVM инструмент opt уже должен присутствовать в вашей системе. Команда opt принимает промежуточный код LLVM IR (расширение .ll) и биткодовые форматы LLVM (расширение .bc), а также способна генерировать свою выходную информацию в виде кода LLVM IR или в виде бит-кода. Ниже показан пример использования команды opt для загрузки специальной совместно используемой библиотеки.

tintin# opt –load=mycustom_pass.so –help –S

Кроме того, обратите внимание, что исполнение команды opt –help из командной строки генерирует длинный список проходов, выполняемых LLVM. Использование опции load вместе с help генерирует сообщение справочной системы, содержащее информацию о вашем специальном проходе.

Создание специального прохода LLVM

Декларирование проходов LLVM осуществляется в файле Pass.h; к примеру, в моей системе этот файл установлен в каталоге /usr/include/llvm. Данный файл описывает интерфейс для отдельного прохода как часть класса Pass. Типы отдельных проходов (каждый из которых извлекается из Pass) также декларируются в этом файле. Типы проходов:

  • Класс BasicBlockPass. Используется для осуществления локальной оптимизации; как правило, каждая оптимизация воздействует лишь на один базовый блок или на одну инструкцию.
  • Класс FunctionPass. Используется для глобальной оптимизации (по одной функции за один раз)
  • Класс ModulePass. Используется для выполнения практически любой неструктурированной межпроцедурной оптимизации

Мы собираемся создать проход, который должен реагировать на любые имена функций, начинающихся с "Hello", поэтому нам необходимо создать собственный проход, извлеченный из FunctionPass. Скопируйте код в листинге 1 из файла Pass.h.

Листинг 1. Перезапись класса runOnFunction в классе FunctionPass
Class FunctionPass : public Pass { 
  /// explicit FunctionPass(char &pid) : Pass(PT_Function, pid) {}
  /// runOnFunction - Virtual method overridden by subclasses to do the
  /// per-function processing of the pass.
  ///
  virtual bool runOnFunction(Function &F) = 0;
  /// …
};

Аналогично, класс BasicBlockPass декларирует runOnBasicBlock, а класс ModulePass декларирует чисто виртуальный метод runOnModule. Дочерний класс должен предоставлять определение для виртуального метода.

Вернувшись обратно к методу runOnFunction в листинг 1, вы увидите, что входом является объект типа Function. Проанализируйте файл /usr/include/llvm/Function.h, и вы увидите, что класс Function используется инфраструктурой LLVM для инкапсуляции возможностей функции C/C++. В свою очередь, класс Function базируется на классе Value, описанном в файле Value.h, и поддерживает метод getName. Соответствующий код показан в листинге 2.

Листинг 2. Создание специального прохода LLVM
#include "llvm/Pass.h"
#include "llvm/Function.h"
class TestClass : public llvm::FunctionPass {
public:
virtual bool runOnFunction(llvm::Function &F)
  {
    if (F.getName().startswith("hello"))
    {
      std::cout << "Function name starts with hello\n";
    }
    return false;
  }
};

В программном коде в листинге 2 отсутствуют две важные детали.

  • Конструктору FunctionPass необходима переменная char, которая используется внутри LLVM. LLVM использует адрес переменной char, поэтому неважно, что именно будет использовано для ее инициализации.
  • Вам нужно, чтобы система LLVM каким-либо образом понимала, что созданный вами класс — это новый проход. Пришло время применить LLVM-шаблон под названием RegisterPass. Шаблон RegisterPass декларируется в заголовочном файле PassSupport.h; этот файл включен в файл Pass.h, поэтому дополнительные заголовки не нужны.

Полный код показан в листинге 3.

Листинг 3. Регистрация LLVM-прохода FunctionPass
class TestClass : public llvm::FunctionPass
{
public:
  TestClass() : llvm::FunctionPass(TestClass::ID) { }
  virtual bool runOnFunction(llvm::Function &F) {
    if (F.getName().startswith("hello")) {
      std::cout << "Function name starts with hello\n";
    }
    return false;
  }
  static char ID; // could be a global too
};
char TestClass::ID = 'a';
static llvm::RegisterPass<TestClass> global_("test_llvm", "test llvm", false, false);

Параметр template в шаблоне RegisterPass представляет собой имя прохода, который будет использоваться с командой opt в командной строке. Это все: теперь вам достаточно создать из кода в листинге 3совместно используемую библиотеку, а затем загрузить эту библиотеку, для чего выполнить команду opt, сопровождаемую именем команды, которую вы зарегистрировали с помощью RegisterPass (в данном случае —test_llvm), и именем биткодового файла, с которым будет исполняться ваш специальный проход (а также другие проходы). Эти шаги показаны в листинге 4.

Листинг 4. Исполнение специального прохода
bash$ g++ -c pass.cpp -I/usr/local/include `llvm-config --cxxflags`
bash$ g++ -shared -o pass.so pass.o -L/usr/local/lib `llvm-config --ldflags -libs`
bash$ opt -load=./pass.so –test_llvm < test.bc

Теперь рассмотрим обратную сторону медали, а именно, фронтенд под названием clang для LLVM-бэкенда.


Представляем clang

Исходные замечания

Clang находится в стадии разработки, поэтому, как и в любом другом проекте подобного масштаба, его документация, как правило, запаздывает относительно его программного кода. По этой причине рекомендуется отслеживать список рассылаемых материалов по разработке clang (соответствующая ссылка приведена в разделе Ресурсы). Вы можете собрать и установить на своем компьютере исходные коды clang. Для этого следуйте инструкциям по началу работы с clang (см. раздел Ресурсы). Обратите внимание: после завершения сборки необходимо выполнить команду make install для установки в системные папки по умолчанию. Далее в статье предполагается, что заголовки и библиотеки clang размещены в системных папках /usr/local/include и /usr/local/lib соответственно.

Инфраструктура LLVM имеет собственный фронтенд — это инструмент (вполне самодостаточный) под названием clang. Clang — это мощный компилятор языков C/C++/Objective-C, скорость компиляции у которого не меньше, чем у инструментов GCC (GNU Compiler Collection). В разделе Ресурсы содержится ссылка на дополнительную информацию. Что еще важнее, кодовая база clang упрощает создание специальных расширений. Подобно тому, как в первой статье данной серии вы использовали API-интерфейс бэкенда LLVM для своего специального плагина, в этой статье вы используете API-интерфейс clang в качестве фронтенда LLVM и создадите несколько небольших приложений для первичной обработки и парсинга.

Общие классы clang

Вам нужно познакомиться со следующими наиболее распространенными классами clang

  • CompilerInstance
  • Preprocessor
  • FileManager
  • SourceManager
  • DiagnosticsEngine
  • LangOptions
  • TargetInfo
  • ASTConsumer
  • Sema
  • ParseAST (наверное, это самый важный метод clang).

Краткие дополнительные сведения по методу ParseAST.

Для решения любых практических задач используйте класс CompilerInstance. Он предоставляет интерфейсы и управляет доступом к AST-дереву, а также осуществляет первичную обработку поступающих на вход исходных кодов и сопровождение целевой информации. Чтобы сделать что-либо полезное, типичное приложение должно создать объект CompilerInstance. В листинге 5 показан заголовочный файл CompilerInstance.h.

Листинг 5. Класс CompilerInstance
class CompilerInstance : public ModuleLoader {
  /// The options used in this compiler instance.
  llvm::IntrusiveRefCntPtr<CompilerInvocation> Invocation;
  /// The diagnostics engine instance.
  llvm::IntrusiveRefCntPtr<DiagnosticsEngine> Diagnostics;
  /// The target being compiled for.
  llvm::IntrusiveRefCntPtr<TargetInfo> Target;
  /// The file manager.
  llvm::IntrusiveRefCntPtr<FileManager> FileMgr;
  /// The source manager.
  llvm::IntrusiveRefCntPtr<SourceManager> SourceMgr;
  /// The preprocessor.
  llvm::IntrusiveRefCntPtr<Preprocessor> PP;
  /// The AST context.
  llvm::IntrusiveRefCntPtr<ASTContext> Context;
  /// The AST consumer.
  OwningPtr<ASTConsumer> Consumer;
 /// \brief The semantic analysis object.
  OwningPtr<Sema> TheSema;
 //… the list continues
};

Первичная обработка файла на C

Имеется не менее двух способов для создания объекта Preprocessor в clang:

  • Непосредственное создание объекта Preprocessor
  • Использование класса CompilerInstance для создания объекта Preprocessor

Начнем с последнего из этих двух подходов.

Вспомогательные классы, необходимые для первичной обработки

Один только объект Preprocessor не сможет решить все задачи. Необходимы также класс FileManager для чтения файлов и класс SourceManager для отслеживания исходных местоположений с целью диагностики. Класс FileManager поддерживает поиск в файловой системе, кэширование файловой системы и поиск в каталогах. Посмотрите на содержимое класса FileEntry, который описывает clang-абстракцию для файла с исходным текстом. В листинге 6 представлен фрагмент заголовочного файла FileManager.h.

Листинг 6. Clang-класс FileManager
class FileManager : public llvm::RefCountedBase<FileManager> {
  FileSystemOptions FileSystemOpts;
   /// \brief The virtual directories that we have allocated.  For each
  /// virtual file (e.g. foo/bar/baz.cpp), we add all of its parent
  /// directories (foo/ and foo/bar/) here.
  SmallVector<DirectoryEntry*, 4> VirtualDirectoryEntries;
  /// \brief The virtual files that we have allocated.
  SmallVector<FileEntry*, 4> VirtualFileEntries;
 /// NextFileUID - Each FileEntry we create is assigned a unique ID #.
  unsigned NextFileUID;
  // Statistics.
  unsigned NumDirLookups, NumFileLookups;
  unsigned NumDirCacheMisses, NumFileCacheMisses;
 // …
  // Caching.
  OwningPtr<FileSystemStatCache> StatCache;

Как правило, запросы относительно объектов SourceLocation поступают в класс SourceManager. В листинге 7 показана информация об объектах SourceLocation, полученная из заголовочного файла SourceManager.h.

Листинг 7. Информация SourceLocation
/// There are three different types of locations in a file: a spelling
/// location, an expansion location, and a presumed location.
///
/// Given an example of:
/// #define min(x, y) x < y ? x : y
///
/// and then later on a use of min:
/// #line 17
/// return min(a, b);
///
/// The expansion location is the line in the source code where the macro
/// was expanded (the return statement), the spelling location is the
/// location in the source where the macro was originally defined,
/// and the presumed location is where the line directive states that
/// the line is 17, or any other line.

Безусловно, между классом SourceManager и классом FileManager существует определенная зависимость; и действительно, конструктор класса SourceManager принимает класс FileManager в качестве входного аргумента. И, наконец, вам необходимо отслеживать ошибки, которые могут встретиться при обработке исходного кода, и иметь соответствующую отчетность. Эта задача решается с помощью класса DiagnosticsEngine. Как и в случае с классом Preprocessor, имеется два способа действий.

  • Создание всех необходимых объектов своими силами.
  • Использование с этой целью класса CompilerInstance.

Воспользуемся последней из двух вышеназванных опций. В листинге 8 показан код для Preprocessor; все остальные элементы уже были объяснены выше.

Листинг 8. Создание препроцессора с помощью API-интерфейса clang
using namespace clang;
int main()
{
    CompilerInstance ci;
    ci.createDiagnostics(0,NULL); // create DiagnosticsEngine
    ci.createFileManager();  // create FileManager
    ci.createSourceManager(ci.getFileManager()); // create SourceManager
    ci.createPreprocessor();  // create Preprocessor
    const FileEntry *pFile = ci.getFileManager().getFile("hello.c");
    ci.getSourceManager().createMainFileID(pFile);
    ci.getPreprocessor().EnterMainSourceFile();
    ci.getDiagnosticClient().BeginSourceFile(ci.getLangOpts(), &ci.getPreprocessor());
    Token tok;
    do {
        ci.getPreprocessor().Lex(tok);
        if( ci.getDiagnostics().hasErrorOccurred())
            break;
        ci.getPreprocessor().DumpToken(tok);
        std::cerr << std::endl;
    } while ( tok.isNot(clang::tok::eof));
    ci.getDiagnosticClient().EndSourceFile();
}

В листинге 8 класс CompilerInstance используется для поочередного создания класса DiagnosticsEngine (вызов метода ci.createDiagnostics) и класса FileManager (ci.createFileManager и ci.CreateSourceManager). После того, как с помощью FileEntry будет осуществлено ассоциирование файла, необходимо продолжить обработку каждого токена в файле исходного кода вплоть до достижения конца этого файла (EOF). Метод препроцессора под названием DumpToken осуществляет вывод токена на экран.

Для компиляции и исполнения кода в листинге 8 используйте makefile, предоставленный в листинге 9 (с соответствующими изменениями согласно размещению ваших установочных папок clang и LLVM). Идея состоит в использовании инструмента llvm-config для предоставления всех необходимых маршрутов и библиотек LLVM. Не пытайтесь осуществлять это связывание в ручном режиме с помощью командной строки g++.

Листинг 9. Makefile-файл для построения кода препроцессора
CXX := g++
RTTIFLAG := -fno-rtti
CXXFLAGS := $(shell llvm-config --cxxflags) $(RTTIFLAG)
LLVMLDFLAGS := $(shell llvm-config --ldflags --libs)
DDD := $(shell echo $(LLVMLDFLAGS))
SOURCES = main.cpp
OBJECTS = $(SOURCES:.cpp=.o)
EXES = $(OBJECTS:.o=)
CLANGLIBS = \
    -L /usr/local/lib \
    -lclangFrontend \
    -lclangParse \
    -lclangSema \
    -lclangAnalysis \
    -lclangAST \
    -lclangLex \
    -lclangBasic \
    -lclangDriver \
    -lclangSerialization \
    -lLLVMMC \
    -lLLVMSupport \
all: $(OBJECTS) $(EXES)
%: %.o
        $(CXX) -o $@ $< $(CLANGLIBS) $(LLVMLDFLAGS)

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

Листинг 10. Ошибка при исполнении кода в листинге 7
Assertion failed: (Target && "Compiler instance has no target!"), 
   function getTarget, file 
   /Users/Arpan/llvm/tools/clang/lib/Frontend/../..
   /include/clang/Frontend/CompilerInstance.h,
   line 294.
Abort trap: 6

Что же произошло? Вы упустили еще одну настройку для класса CompilerInstance: вы не указали целевую платформу, для которой должен быть скомпилирован этот код. Для этого воспользуемся классами TargetInfo и TargetOptions. Согласно clang-заголовку TargetInfo.h, класс TargetInfo хранит необходимую информацию о целевой системе для генерации кода, поэтому он должен быть создан до начала компиляции или первичной обработки. Предполагается, что класс TargetInfo располагает информацией о ширине, о выравнивании и о других параметрах для целых чисел и для чисел с плавающей точкой. В листинге 11 представлен фрагмент заголовочного файла TargetInfo.h.

Листинг 11. Clang-класс TargetInfo
class TargetInfo : public llvm::RefCountedBase<TargetInfo> {
  llvm::Triple Triple;
protected:
  bool BigEndian;
  unsigned char PointerWidth, PointerAlign;
  unsigned char IntWidth, IntAlign;
  unsigned char HalfWidth, HalfAlign;
  unsigned char FloatWidth, FloatAlign;
  unsigned char DoubleWidth, DoubleAlign;
  unsigned char LongDoubleWidth, LongDoubleAlign;
  // …

При инициализации класса TargetInfo используются два аргумента, а именно DiagnosticsEngine и TargetOptions. Последний из этих аргументов должен иметь строку Triple с соответствующим значением для текущей платформы. Здесь снова проявляется удобство инфраструктуры LLVM. В листинге 12 показано дополнение к листингу 9 для задействования препроцессора.

Листинг 12. Настройка целевых опций для компилятора
int main()
{
    CompilerInstance ci;
    ci.createDiagnostics(0,NULL);
    // create TargetOptions
    TargetOptions to;
    to.Triple = llvm::sys::getDefaultTargetTriple();
    // create TargetInfo
    TargetInfo *pti = TargetInfo::CreateTargetInfo(ci.getDiagnostics(), to);
    ci.setTarget(pti);
    // rest of the code same as in Listing 9…
    ci.createFileManager();
    // …

Готово! После исполнения этого кода вы увидите следующую выходную информацию простого теста hello.c.

#include <stdio.h>
int main() {  printf("hello world!\n"); }

В листинге 13 показан фрагмент выходной информации препроцессора.

Листинг 13. Выходная информация препроцессора (фрагмент)
typedef 'typedef'
struct 'struct'
identifier '__va_list_tag'
l_brace '{'
unsigned 'unsigned'
identifier 'gp_offset'
semi ';'
unsigned 'unsigned'
identifier 'fp_offset'
semi ';'
void 'void'
star '*'
identifier 'overflow_arg_area'
semi ';'
void 'void'
star '*'
identifier 'reg_save_area'
semi ';'
r_brace '}'
identifier '__va_list_tag'
semi ';'

identifier '__va_list_tag'
identifier '__builtin_va_list'
l_square '['
numeric_constant '1'
r_square ']'
semi ';'

Создание объекта Preprocessor в ручном режиме

Одно из достоинств библиотек clang заключается в том, что они позволяют получать один и тот же результат несколькими разными способами. В этом разделе мы создадим объект Preprocessor без прямого запроса к классу CompilerInstance. В листинге 14 показан конструктор для объекта Preprocessor из заголовочного файла Preprocessor.h.

Листинг 14. Конструктор объекта Preprocessor
Preprocessor(DiagnosticsEngine &diags, LangOptions &opts,
               const TargetInfo *target,
               SourceManager &SM, HeaderSearch &Headers,
               ModuleLoader &TheModuleLoader,
               IdentifierInfoLookup *IILookup = 0,
               bool OwnsHeaderSearch = false,
               bool DelayInitialization = false);

При рассмотрении этого конструктора становится очевидным, что для того, чтобы этот монстр смог запуститься, необходимо создать шесть разных объектов. Объекты DiagnosticsEngine, TargetInfo и SourceManager вам уже знакомы. Объект CompilerInstance создается на основе ModuleLoader. Таким образом, нам необходимо создать два новых объекта — один для LangOptions и один для HeaderSearch. Класс LangOptions позволяет компилировать несколько диалектов C/C++, в том числе C99, C11 и C++0x. Для получения дополнительной информации обратитесь к заголовочным файлам LangOptions.h и LangOptions.def. И, наконец, класс HeaderSearch хранит вектор std::vector, который, помимо прочего, содержит каталоги для поиска. Код препроцессора показан в листинге 15.

Листинг 15. Созданный в ручном режиме препроцессор
using namespace clang;
int main()  {
    DiagnosticOptions diagnosticOptions;
    TextDiagnosticPrinter *printer = 
      new TextDiagnosticPrinter(llvm::outs(), diagnosticOptions);
    llvm::IntrusiveRefCntPtr<clang::DiagnosticIDs> diagIDs;
    DiagnosticsEngine diagnostics(diagIDs, printer);
    LangOptions langOpts;
    clang::TargetOptions to;
    to.Triple = llvm::sys::getDefaultTargetTriple();
    TargetInfo *pti = TargetInfo::CreateTargetInfo(diagnostics, to);
    FileSystemOptions fsopts;
    FileManager fileManager(fsopts);
    SourceManager sourceManager(diagnostics, fileManager);
    HeaderSearch headerSearch(fileManager, diagnostics, langOpts, pti);
    CompilerInstance ci;
    Preprocessor preprocessor(diagnostics, langOpts, pti,
      sourceManager, headerSearch, ci);
    const FileEntry *pFile = fileManager.getFile("test.c");
    sourceManager.createMainFileID(pFile);
    preprocessor.EnterMainSourceFile();
    printer->BeginSourceFile(langOpts, &preprocessor);
    // … similar to Listing 8 here on
}

Обратите внимание на несколько моментов в коде, показанном в листинге 15:

  • Вы не инициализировали HeaderSearch указанием на какой-либо конкретный каталог. Вам следует это сделать.
  • API-интерфейс clang требует, чтобы элемент TextDiagnosticPrinter был размещен в "куче". Его размещение в стеке приведет к аварийному завершению.
  • Вы не смогли избавиться от класса CompilerInstance. Поскольку вы все равно используете класс CompilerInstance, не следует заниматься его созданием в ручном режиме — гораздо удобнее сделать это с помощью API-интерфеса clang.

Поддержка языка C++

Вплоть до настоящего времени вы работали с тестовым кодом на языке C. Теперь вы будете иметь дело с языком C++. К коду в листинге 15 добавьте строку langOpts.CPlusPlus = 1; и протестируйте код в листинге 16.

Листинг 16. Тестовый код на языке C++ для препроцессора
template <typename T, int n>
struct s { 
  T array[n];
};
int main() {
  s<int, 20> var;
}

В листинге 17 показан фрагмент результатов работы вашей программы.

Листинг 17. Результаты исполнения кода в листинге 16 (фрагмент)
identifier 'template'
less '<'
identifier 'typename'
identifier 'T'
comma ','
int 'int'
identifier 'n'
greater '>'
struct 'struct'
identifier 's'
l_brace '{'
identifier 'T'
identifier 'array'
l_square '['
identifier 'n'
r_square ']'
semi ';'
r_brace '}'
semi ';'
int 'int'
identifier 'main'
l_paren '('
r_paren ')'

Создание дерева грамматического разбора

Метод ParseAST, описанный в файле clang/Parse/ParseAST.h, является одним из важнейших методов, предоставляемых компилятором clang. Ниже показана декларация соответствующей процедуры, скопированная из файла ParseAST.h:

void ParseAST(Preprocessor &pp, ASTConsumer *C,
       ASTContext &Ctx, bool PrintStats = false,
       TranslationUnitKind TUKind = TU_Complete,
       CodeCompleteConsumer *CompletionConsumer = 0);

Класс ASTConsumer предоставляет разработчику базовый абстрактный интерфейс. Это полезный момент, поскольку разные клиенты, скорее всего, будут по-разному осуществлять выгрузку или обработку AST-дерева. Ваш клиентский код будет базироваться на классе ASTConsumer. Класс ASTContext хранит — помимо прочего — информацию о декларациях типов. Простейший способ опробовать его на практике состоит в следующем. Распечатайте список глобальных переменных в своем коде с помощью API-интерфейса ASTConsumer, предоставляемого clang. Во многих технических организациях установлены строгие правила по использованию глобальных переменных в коде на C++. Кроме того, этот интерфейс позволит вам приступить к созданию собственного инструмента, соответствующего стандартам программирования (см. код в листинге 18).

Листинг 18. Специальный класс ASTConsumer
class CustomASTConsumer : public ASTConsumer {
public:
 CustomASTConsumer () :  ASTConsumer() { }
    virtual ~ CustomASTConsumer () { }
    virtual bool HandleTopLevelDecl(DeclGroupRef decls)
    {
        clang::DeclGroupRef::iterator it;
        for( it = decls.begin(); it != decls.end(); it++)
        {
            clang::VarDecl *vd = llvm::dyn_cast<clang::VarDecl>(*it);
            if(vd)
               std::cout << vd->getDeclName().getAsString() << std::endl;;
        }
        return true;
    }
};

Вы перезаписываете метод HandleTopLevelDecl (который первоначально был предоставлен в классе ASTConsumer) своей собственной версией. Clang передает вам список глобальных переменных, а вы осуществляете итеративное прохождение по списку и вывод на печать имен переменных. В листинге 19 показан фрагмент файла ASTConsumer.h, содержащий несколько других методов, которые могут быть перезаписаны клиентским кодом.

Листинг 19. Другие методы, которые могут быть перезаписаны клиентским кодом
/// HandleInterestingDecl - Handle the specified interesting declaration. This
/// is called by the AST reader when deserializing things that might interest
/// the consumer. The default implementation forwards to HandleTopLevelDecl.
virtual void HandleInterestingDecl(DeclGroupRef D);

/// HandleTranslationUnit - This method is called when the ASTs for entire
/// translation unit have been parsed.
virtual void HandleTranslationUnit(ASTContext &Ctx) {}

/// HandleTagDeclDefinition - This callback is invoked each time a TagDecl
/// (e.g. struct, union, enum, class) is completed.  This allows the client to
/// hack on the type, which can occur at any point in the file (because these
/// can be defined in declspecs).
virtual void HandleTagDeclDefinition(TagDecl *D) {}

/// Note that at this point it does not have a body, its body is
  /// instantiated at the end of the translation unit and passed to
  /// HandleTopLevelDecl.
  virtual void HandleCXXImplicitFunctionInstantiation(FunctionDecl *D) {}

И, наконец, в листинге 20 показан реальный клиентский код, в котором используется созданный вами специальный класс ASTConsumer.

Листинг 20. Клиентский код, в котором используется специальный класс ASTConsumer
int main() { 
    CompilerInstance ci;
    ci.createDiagnostics(0,NULL);
    TargetOptions to;
    to.Triple = llvm::sys::getDefaultTargetTriple();
    TargetInfo *tin = TargetInfo::CreateTargetInfo(ci.getDiagnostics(), to);
    ci.setTarget(tin);
    ci.createFileManager();
    ci.createSourceManager(ci.getFileManager());
    ci.createPreprocessor();
    ci.createASTContext();
    CustomASTConsumer *astConsumer = new CustomASTConsumer ();
    ci.setASTConsumer(astConsumer);
    const FileEntry *file = ci.getFileManager().getFile("hello.c");
    ci.getSourceManager().createMainFileID(file);
    ci.getDiagnosticClient().BeginSourceFile(
       ci.getLangOpts(), &ci.getPreprocessor());
    clang::ParseAST(ci.getPreprocessor(), astConsumer, ci.getASTContext());
    ci.getDiagnosticClient().EndSourceFile();
    return 0;
}

Заключение

Другие статьи данного цикла

Читайте другие статьи данного цикла: Создание действующего компилятора с помощью инфраструктуры LLVM.

Данная серия из двух статей предоставляет большой объем базовых сведений. В частности, в этих статьях описывается промежуточное представление LLVM IR, предлагаются способы генерации IR-кода в ручном режиме и с помощью API-интерфейсов LLVM, демонстрируется процесс создания собственного подключаемого модуля для бэкенда LLVM, а также описывается фронтенд LLVM и его обширный набор заголовочных файлов. Вы также узнали, как использовать данный фронтенд для первичной обработки и для задействования AST-дерева. Создание и расширение компилятора, особенно для таких сложных языков, как C++, всегда было весьма сложной задачей, однако с появлением инфраструктуры LLVM ситуация в этой области существенно упростилась. Документация по LLVM и по clang по-прежнему нуждается в дальнейшем совершенствовании, а пока я рекомендую вам пользоваться VIM/doxygen для просмотра заголовочных файлов. Успехов!

Ресурсы

Научиться

Получить продукты и технологии

Комментарии

developerWorks: Войти

Обязательные поля отмечены звездочкой (*).


Нужен IBM ID?
Забыли Ваш IBM ID?


Забыли Ваш пароль?
Изменить пароль

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Профиль создается, когда вы первый раз заходите в developerWorks. Информация в вашем профиле (имя, страна / регион, название компании) отображается для всех пользователей и будет сопровождать любой опубликованный вами контент пока вы специально не укажите скрыть название вашей компании. Вы можете обновить ваш IBM аккаунт в любое время.

Вся введенная информация защищена.

Выберите имя, которое будет отображаться на экране



При первом входе в developerWorks для Вас будет создан профиль и Вам нужно будет выбрать Отображаемое имя. Оно будет выводиться рядом с контентом, опубликованным Вами в developerWorks.

Отображаемое имя должно иметь длину от 3 символов до 31 символа. Ваше Имя в системе должно быть уникальным. В качестве имени по соображениям приватности нельзя использовать контактный e-mail.

Обязательные поля отмечены звездочкой (*).

(Отображаемое имя должно иметь длину от 3 символов до 31 символа.)

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Вся введенная информация защищена.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Open source, Linux
ArticleID=845344
ArticleTitle=Создание действующего компилятора с помощью инфраструктуры LLVM. Часть 2
publish-date=11122012