Попробуем теперь использовать получившуюся программу рисования диаграммы наследования с реальным кодом, т.е. взятым из реального мира. Вопрос в том, какой именно код взять? Лучший кандидат для проверки - наша программа рисования, testclang.cpp! Что может быть реальнее? :)
Напоминаю, что эксперименты с CLang API я провожу на следующей конфигурации:
- SUSE Linux 3.12.39 x86_64 GNU/Linux
- gcc version 4.8.3
- llvm/clang version 3.7.0 (tags/RELEASE_370/final)
Предыдущая статья
Подготовительные действия
Сначала необходимо модифицировать программу так, чтобы она могла принимать параметры командной строки и запускать обработку кода с этими параметрами.
Для обработки кода с параметрами в clang предусмотрена функция runToolOnCodeWithArgs
. В нее, помимо уже известных параметров, необходимо передать также массив с параметрами компиляции кода.
Я добавил в программу код обработки входных параметров, чтения кода из входного файла и парсинга опций компиляции. На этом я не буду останавливаться, работающий код можно посмотреть в исходнике. Скажу только, что можно задавать входной файл, а также параметры компиляции этого файла с помощью опции -o
или --options
. Вот так:
> ./a.out [-o "<опции компиляции>"] <имя_файла>
Однако, перед тем, как запускать программу, нужно задуматься о следующем: в построенном дереве будут присутствовать все классы, попавшиеся компилятору, а это значит, что в AST попадут не только классы стандартной библиотеки, но и классы из заголовочных файлов LLVM и CLang. Нужны ли нам эти классы при построении диаграммы? Вряд ли.
Необходимо каким-то образом ограничить множество классов только теми классами, которые нас интересуют. Фильтровать классы можно с помощью объекта SourceLocation
, который присутствует у любого объекта типа clang::Decl
:
bool VisitCXXRecordDecl (CXXRecordDecl *D) { auto loc = D->getLocation(); if (!loc.isValid() || !D->getASTContext().getSourceManager().isInMainFile(loc)) { return true; } ...
В начало метода VisitCXXRecordDecl
я добавил проверку, что текущая декларация находится в главном компилируемом файле. Таким образом в диаграмму попадут только те классы, которые объявлены в файле testclang.cpp
. У класса SourceManager
, кроме метода isInMainFile()
есть много других интересных методов, с ними можно экспериментировать, чтобы добиться желаемого результата.
Запускаем программу
Итак, теперь "натравим" нашу программу на саму себя - на файл testlang.cpp
.
> make > ./a.out testclang.cpp testclang.cpp:1:10: fatal error: 'clang/AST/RecursiveASTVisitor.h' file not found #include "clang/AST/RecursiveASTVisitor.h" ^
Ошибка, сообщающая о не найденных заголовочных файлах, вполне логична и ожидаема. Необходимо указать нашей программе, где искать заголовочные файлы, и вообще, указать все остальные параметры, ведь программе необходимо правильно обработать код, а для этого нужны не только заголовочные файлы, но и макросы и другие параметры компиляции.
Так какие же опции нужно задать?
В нашем конкретном случае для правильной обработки файла testclang.cpp
нужно задать следующие опции:
- Опции компиляции этой конкретной программы - результат вывода команды '
llvm-config --cxxflags
' - Несколько директорий для поиска заголовочных файлов стандартной библиотеки. Например, директории, используемые GCC можно посмотреть командой "
gcc -xc++ -E -v -
". На другой платформе и/или с другим компилятором придется искать другой способ. В моем случае было достаточно добавления следующих директорий:- -I/usr/include/c++/4.8
- -I/usr/include/c++/4.8/x86_64-suse-linux
- -I/usr/include/c++/4.8/backward
- Обязательно добавьте опцию "
-stdlib=libc++
", иначе получите ошибку вида "use of undeclared identifier '__builtin_ia32_bsrsi'
" Про библиотеку LLVM libc++ и ее использование можно почитать здесь.
Последняя проблема
Попробуем запустить программу с параметрами:
> ./a.out -o "`~/opt/llvm/bin/llvm-config --cxxflags` -I/usr/include/c++/4.8 -I/usr/include/c++/4.8/x86_64-suse-linux -I/usr/include/c++/4.8/backward -stdlib=libc++" testclang.cpp ... In file included from /usr/include/c++/4.8/cwchar:44: /usr/include/wchar.h:39:11: fatal error: 'stdarg.h' file not found # include <stdarg.h> ^
Внезапно. Почему stdarg.h
не найден? Дело в том, что stdarg.h
, как и некоторые другие заголовочные файлы библиотеки C, является сильно зависимым от компилятора, а поэтому компиляторы часто имеют папку со "своими" заголовочными файлами.
Давайте посмотрим, какие директории поиска заголовочных используются в clang. Для этого запустим еще раз обработку файла testclang.cpp
с добавленной опцией компиляции "-v
". Эта опция включает verbose-режим компилятора и заставляет его выводить в поток вывода огромное количество вспомогательной информации. Помимо всего прочего компилятор выдаст список директорий, по которому выполняется поиск заголовочных файлов.
> ./a.out -o "-v ..." testclang.cpp ... clang -cc1 version 3.7.0 based upon LLVM 3.7.0 default target x86_64-unknown-linux-gnu ignoring nonexistent directory "../lib/clang/3.7.0/include" ignoring nonexistent directory "/include" #include "..." search starts here: #include <...> search starts here: /home/c4dev/opt/llvm/include /usr/include/c++/4.8 /usr/include/c++/4.8/x86_64-suse-linux /usr/include/c++/4.8/backward /usr/local/include /usr/include End of search list. ...
Обратите внимание на сообщение "ignoring nonexistent directory "../lib/clang/3.7.0/include"
". Это как раз и есть та самая директория, в которой содержатся "свои" заголовочные файлы. Однако, проблема в том, что компилятор по каким-то своим соображениям использует относительный формат пути к этим файлам.
Есть три способа решения этой проблемы: 1) создать в родительской директории копию папки lib из директории, куда был установлен llvm (у меня это ~/opt/llvm/lib/
); 2) создать в родительской директории ссылку на нужную директорию; 3) сменить рабочую директорию при запуске программы.
Первые два способа я не хочу использовать по причине их громоздкости (ведь при переносе проекта придется делать всё заново). А вот третий способ кажется мне легким и гибким. Его я и буду использовать впредь, к тому же этот способ легко автоматизировать.
> CDIR=`pwd`; WDIR=`~/opt/llvm/bin/llvm-config --bindir`; (cd $WDIR; $CDIR/a.out -o "-v `~/opt/llvm/bin/llvm-config --cxxflags` -I/usr/include/c++/4.8 -I/usr/include/c++/4.8/x86_64-suse-linux -I/usr/include/c++/4.8/backward -stdlib=libc++" $CDIR/testclang.cpp)
Разбор команды:
- имя текущей директории запоминается в переменную
CDIR
- с помощью команды "
llvm-config --bindir
" выясняется имя директории, где хранятся исполняемые файлы llvm/clang, и запоминается в переменнойWDIR
- перед запуском программы происходит смена директории; так как команды смены папки и запуска программы указаны в скобках, то по завершении bash вернется в исходную директорию.
Заключение
Как видно из иллюстраций ниже программа отработала корректно, и в результате мы имеем диаграмму только для тех классов, которые объявлены в файле testclang.cpp
.
... CXXRecord: class, id: MyVisitor 0xa6bf848 | MyVisitor - Bases (1) - - RecursiveASTVisitor<class MyVisitor> | clang::RecursiveASTVisitor - All bases - - id: RecursiveASTVisitor 0xa6bf970 | clang::RecursiveASTVisitor - Methods - - MyVisitor::VisitTranslationUnitDecl - - MyVisitor::VisitCXXRecordDecl - - MyVisitor::MyVisitor - - MyVisitor::MyVisitor - - MyVisitor::~MyVisitor - - MyVisitor::operator= - - MyVisitor::operator= - - MyVisitor::MyVisitor ------------------------------------------ CXXRecord: class, id: MyConsumer 0xa764548 | MyConsumer - Bases (1) - - clang::ASTConsumer | clang::ASTConsumer - All bases - - id: ASTConsumer 0x7ccae08 | clang::ASTConsumer - Methods - - MyConsumer::HandleTranslationUnit - - MyConsumer::MyConsumer - - MyConsumer::operator= - - MyConsumer::operator= - - MyConsumer::~MyConsumer - - MyConsumer::MyConsumer - - MyConsumer::MyConsumer ------------------------------------------ CXXRecord: class, id: MyAction 0xa77ea18 | MyAction - Bases (1) - - clang::ASTFrontendAction | clang::ASTFrontendAction - All bases - - id: ASTFrontendAction 0xa391d28 | clang::ASTFrontendAction - - id: FrontendAction 0xa313d48 | clang::FrontendAction - Methods - - MyAction::CreateASTConsumer - - MyAction::MyAction - - MyAction::MyAction - - MyAction::operator= - - MyAction::operator= - - MyAction::~MyAction - - MyAction::MyAction ------------------------------------------ 1 warning generated. Writing '/tmp/my-6a3bd2.dot'... done.
Преобразуем полученный dot-файл в PNG:
> dot -Tpng /tmp/my-6a3bd2.dot > f.png > xdg-open f.png
Вот такая картинка у меня получилась. Как видно, она полностью соответствует действительности.
Исходный код
Исходники можно взять на GitHub.
Комментариев нет:
Отправить комментарий