21 марта 2016 г.

CLang API: связь с реальным миром (clang-3.7)

Попробуем теперь использовать получившуюся программу рисования диаграммы наследования с реальным кодом, т.е. взятым из реального мира. Вопрос в том, какой именно код взять? Лучший кандидат для проверки - наша программа рисования, 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 нужно задать следующие опции:

  1. Опции компиляции этой конкретной программы - результат вывода команды 'llvm-config --cxxflags'
  2. Несколько директорий для поиска заголовочных файлов стандартной библиотеки. Например, директории, используемые 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
  3. Обязательно добавьте опцию "-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)

Разбор команды:

  1. имя текущей директории запоминается в переменную CDIR
  2. с помощью команды "llvm-config --bindir" выясняется имя директории, где хранятся исполняемые файлы llvm/clang, и запоминается в переменной WDIR
  3. перед запуском программы происходит смена директории; так как команды смены папки и запуска программы указаны в скобках, то по завершении 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.

Комментариев нет:

Отправить комментарий