Создание действующего компилятора с помощью инфраструктуры LLVM. Часть 2
Использование clang для первичной обработки кода на языке C/C++
В первой статье данной серии рассматривалось т. н. "промежуточное представление" (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
Инфраструктура 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 IR, предлагаются способы генерации IR-кода в ручном режиме и с помощью API-интерфейсов LLVM, демонстрируется процесс создания собственного подключаемого модуля для бэкенда LLVM, а также описывается фронтенд LLVM и его обширный набор заголовочных файлов. Вы также узнали, как использовать данный фронтенд для первичной обработки и для задействования AST-дерева. Создание и расширение компилятора, особенно для таких сложных языков, как C++
, всегда было весьма сложной задачей, однако с появлением инфраструктуры LLVM ситуация в этой области существенно упростилась. Документация по LLVM и по clang по-прежнему нуждается в дальнейшем совершенствовании, а пока я рекомендую вам пользоваться VIM/doxygen для просмотра заголовочных файлов. Успехов!
Ресурсы для скачивания
Похожие темы
- Оригинал статьи: Create a working compiler with the LLVM framework, Part 2.
- Основы LLVM описываются в статье: Создание действующего компилятора с помощью инфраструктуры LLVM. Часть 1. Построение собственного компилятора с помощью инфраструктуры LLVM и ее промежуточного представления (LLVM IR) (Арпан Сен (Arpan Sen), developerWorks июнь 2012 г.). Инфраструктура компилятора LLVM предоставляет мощные возможности для оптимизации приложений вне зависимости от используемого языка программирования. Построение собственного компилятора стало еще проще!
- Дополнительная информация по проходам LLVM.
- Список рассылаемых материалов по разработке clang.
- Статья Getting Started: Building and Running Clang содержит подробную информацию по построению и установке clang.
- Официальное руководство по LLVM содержит превосходное введение в LLVM.
- Руководство программиста LLVM— ценный ресурс при работе с API-интерфейсом LLVM.
- Посетите веб-сайт проекта LLVM и загрузите последнюю версию.
- Подробная информация по clang на сайте LLVM.