- PVSM.RU - https://www.pvsm.ru -
Сейчас с уверенностью можно утверждать, что времена самописных C++-парсеров постепенно отходят в прошлое. На сцену медленно, но неумолимо выходит clang — полноценный C++-фронренд и компилятор, предоставляющий своим пользователям богатое API. С помощью этого API можно распарсить исходный текст на C/C++/Objective C, и вытащить из него всю необходимую информацию — от простого лексического значения токенов, до таблицы символов, AST-деревьев и результатов статического анализа кода на предмет всяких разных проблем. В связке с llvm и при сильном на то желании C++ можно использовать в качестве скриптового языка, парся и исполняя C++-программы «на лету». В общем, возможности перед программистами открываются богатые, надо только понять — как ими правильно воспользоваться. А тут, как это не редко случается, и начинается самое интересное.
Надо начать с того, что разработчики clang'а предоставляют своим клиентам два вида API. Первое — полностью «плюсовое», но… потенциально нестабильное (в том смысле, что может меняться от версии к версии). Второе — гарантированно стабильное, но… чисто «сишное». Выбор в пользу того или другого надо делать по ситуации, и исходя из потребностей разрабатываемого на базе clang продукта.
В дереве исходников clang'а реализация этой библиотеки размещена в ветке tools (реализация самого ядра clang'а размещена в lib). Эта библиотека компилируется в динамически-подгружаемый модуль, и ее интерфейс предоставляет клиенту ряд гарантий:
Но, за перечисленные преимущества надо платить. По этому clang-c API обладает следующим набором недостатков:
Использовать этот вариант API имеет смысл в тех случаях, когда имеющийся набор «плюсов» существенен для клиентского кода. Ну или «минусы» не столь принципиальны. Это API вполне подходит для извлечения семантической информации из исходного текста (как в виде AST, так и в виде семантической нагрузки каждого конкретного токена в исходном тексте), его нечастой индексации, верификации «на лету» со сбором всей диагностики, и т. п. задач. Соответственно, это подходит для различного рода standalone-трансляторов, генераторов метаинформации, статических анализаторов и верификаторов кода, и т. п.
И, в свою очередь, это API плохо подходит для задач, где требуется повышенная производительность, или более плотное взаимодействие с ядром компилятора.
Этот вариант API — по сути своей интерфейс самого ядра компилятора. Это API чисто C++-ное, и предоставляет широкий доступ ко всем возможностям ядра clang'а. К его достоинствам можно отнести:
И, отчасти как следствие, недостатки:
Насколько существенны недостатки, и перевешивают ли они достоинства — решать надо по ситуации. На мой взгляд, этот вариант использования clang'а надо выбирать везде, ге требуется хорошая производительность или доступ к специфическим возможностям, недоступным посредством clang-c. В частности, при использовании clang'а в качестве on-the-fly-парсера для IDE имеет смысл использовать именно этот вариант API.
А зачем тогда, спрашивается, вообще этот clang нужен? Действительно, распарсить исходный текст с целью извлечения из него интересующей разработчика информации или для превращения его в байт-код — это основная задача clang frontend. И для решения этой задачи clang предоставляет богатые возможности. Наверное даже слишком богатые. Признаться, я слегка опешил, когда в первый раз открыл один из примеров к clang'у — производимые в нем манипуляции показались мне из области черной магии, поскольку не совпадали с интуитивными представлениями о том, как этот самый парсинг должен бы выглядеть. В итоге, все оказалось вполне логичным, хотя установка опций парсинга путем передачи массива строк, описывающего аргументы командной строки, меня до сих пор несколько обескураживает.
Если бы мое знакомство с clang началось бы с примера, построенного на базе этого API, удивлений было бы меньше. Фактически, парсинг файла производится двумя вызовами. Первый создает экземпляр объекта CXIndex, второй — инициирует собственно разбор исходного текста и построение AST. Вот как это выглядит в исходном тексте:
#include <iostream>
#include <clang-c/Index.h>
int main (int argc, char** argv)
{
CXIndex index = clang_createIndex (
false, // excludeDeclarationFromPCH
true // displayDiagnostics
);
CXTranslationUnit unit = clang_parseTranslationUnit (
index, // CIdx
"main.cpp", // source_filename
argv + 1 , // command_line_args
argc - 1 , // num_command_line_args
0, // unsave_files
0, // num_unsaved_files
CXTranslationUnit_None // options
);
if (unit != 0 )
std::cout << "Translation unit successfully created" << std::endl;
else
std::cout << "Translation unit was not created" << std::endl;
clang_disposeTranslationUnit(unit);
clang_disposeIndex(index);
}
Первый метод (clang_createIndex) создает контекст, в рамках которого будут создаваться и парситься экземпляры единиц трансляции (CXTranslationUnit). Он принимает два параметра. Первый (excludeDeclarationsFromPCH) управляет видимостью объявлений, прочитанных из precompiled-заголовка в процессе обхода полученного AST. Значение 1 означает, что такие объявления будут исключены из итоговой AST. Второй параметр (displayDiagnostics) управляет выводом диагностики, полученной в процессе трансляции, на консоль.
Второй метод (clang_parseTranslationUnit) выполняет собственно парсинг файла с исходным текстом. Этот метод имеет следующие параметры:
Как можно видеть, вся настройка парсера выполняется посредством передачи парсеру аргументов командной строки в текстовом виде. Параметр unsaved_files полезен в сценариях использования clang'а из редакторов или IDE. Посредством него можно передать парсеру те файлы, которые были модифицированны пользователем, но еще не сохранены на диск. Это коллекция структур типа CXUnsavedFile, содержащих имя файла, его содержимое и размер содержимого в байтах. Имя и содержимое задается в виде C-строк, а размер — в виде беззнакового целого.
Последний параметр (options) — это набор следующих флагов:
Флаги могут комбинироваться посредством операции '|'.
Два последних метода (clang_disposeTranslationUnit и clang_disposeIndex) удаляют ранее созданные хэндлы, описывающие единицу трансляции и контекст.
Для успешной сборки этого примера кода достаточно подключить библиотеку libclang.
Аналогичный (по функционалу) код с использованием clang API выглядит следующим образом:
#include <vector>
#include <iostream>
#include <clang/Basic/Diagnostic.h>
#include <clang/Frontend/DiagnosticOptions.h>
#include <clang/Frontend/CompilerInstance.h>
#include <clang/Frontend/CompilerInvocation.h>
#include <clang/Frontend/Utils.h>
#include <clang/Frontend/ASTUnit.h>
int main(int argc, char ** argv)
{
using namespace clang ;
using namespace llvm ;
// Initialize compiler options list
std::vector< const char *> args;
for (int n = 1; n < argc; ++ n)
args.push_back(argv[n]);
args.push_back("main_clang.cpp" );
const char** opts = &args.front();
int opts_num = args.size();
// Create and setup diagnostic consumer
DiagnosticOptions diagOpts;
IntrusiveRefCntPtr< DiagnosticsEngine> diags(CompilerInstance::createDiagnostics(
diagOpts, // Opts
opts_num, // Argc
opts, // Argv
0, // Client
true, // ShouldOwnClient
false // ShouldCloneClient
));
// Create compiler invocation
IntrusiveRefCntPtr< CompilerInvocation> compInvoke = clang::createInvocationFromCommandLine(
makeArrayRef(opts, opts + opts_num), // Args
diags // Diags
);
if (!compInvoke) {
std::cout << "Can't create compiler invocation for given args" ;
return -1;
}
// Parse file
clang::ASTUnit *tu = ASTUnit ::LoadFromCompilerInvocation(
compInvoke.getPtr(), // CI
diags, // Diags
false, // OnlyLocalDecls
true, // CaptureDiagnostics
false, // PrecompilePreamble
TU_Complete, // TUKind
false // CacheCodeCompletionResults
);
if (tu == 0 )
std::cout << "Translation unit was not created" ;
else
std::cout << "Translation unit successfully created" ;
return 0;
}
Букв в нём гораздо больше, а для сборки требуется ликовка со следующим набором библиотек: clangLex, clangBasic, clangAST, clangSerialization, clangEdit, clangAnalysis, clangFrontend, clangSema, clangDriver, clangParse, LLVMCore, LLVMMC, и LLVMSupport. При сборке под Windows также требуется добавить advapi32 и shell32. Зато на выходе получится исполняемый модуль без лишних внешних зависимостей.
Представленный выше код можно разделить на четыре части:
Как я уже писал выше, опции работы clang'а устанавливаются путем передачи соответствующим классам коллекции строк, эти настройки описывающих. Строки передаются в виде массива указателей и удобнее всего сделать это посредством промежуточного вектора. В этом случае можно добавить к полученным извне аргументам любое количество своих собственных. В частности, имя файла, которое будет парсится.
Создание DE необходимо для того, чтобы получать от парсера clang различную диагностическую информацию, которую он генерирует в процессе разбора исходного текста. Такие параметры, как максимальное количество отображаемых ошибок, какие именно ошибки/предупреждения отображать и т. п. DE берет из командной строки, которые передается вторым и третьим параметрами. Последние три параметра описывают «диагностического клиента». Это специальный класс, которому DE будет передавать сообщения парсера (по мере их возникновения) для дальнейшей обработки специфичрым для пользователя clang образом. DE может взять контроль за временем жизни клиента на себя, либо работать с клоном переданного объекта. Это позволяет использовать разные сценарии реализации клиента — в виде статического/автоматического объекта, в виде объекта в куче, как часть класса, в методах которого производится работа с clang API, и т. п.
На этом шаге, фактически, создается контекст, в рамках которого будет производиться парсинг. Анализируются все параметры переданной командной строки, переменные окружения, создается вся внутренняя инфраструктура (в соответствии с этими параметрами), подключается Diagnostic Engine. После этого clang полностью готов к парсингу того файла, который был передан в качестве последнего параметра.
Он осуществляется посредством вызова одного из статических методов класса clang:ASTUnit. Таких методов несколько, они заточены под разные сценарии. В примере приведен один из возможных вариантов. В данном случае парсеру передается экземпляр compiler invocation (парсер его потом сам удалит!), экземпляр Diagnostic Engine (его парсер автоматически удалять не будет), и несколько параметров, контролирующих поведение парсера:
В своих первых экспериментах (это был парсинг заголовочных файлов на предмет извлечения деклараций) я долго не понимал причину, по которой парсинг завершался с большим количеством ошибок. В итоге все оказалось довольно просто. Итак, опции, которые могут оказаться полезными:
На этом первое знакомство с clang API можно считать законченным. Подробнее про clang-c API можно почитать на официальном сайте clang: clang.llvm.org/doxygen/group__CINDEX.html [1]
Там же [2] можно ознакомится со всей иерархией классов clang API. К некоторому сожалению, документация генерируется автоматически из апстрима clang, по этому сигнатуры функций, их набор и т. п., описанные на сайте с документацией, могут отличаться от тех, которые представлены в том или ином релизе.
В следующей заметке я расскажу о том, как из созданной с помощью clang AST можно получить дерево деклараций.
Автор: FlexFerrum
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/c-3/12092
Ссылки в тексте:
[1] clang.llvm.org/doxygen/group__CINDEX.html: http://clang.llvm.org/doxygen/group__CINDEX.html
[2] Там же: http://clang.llvm.org/doxygen/annotated.html
Нажмите здесь для печати.