Главная » Хабрахабр » Пример разбора C++ кода с помощью libclang на Python

Пример разбора C++ кода с помощью libclang на Python

В C++ есть встроенный механизм Run-Time Type Information (RTTI), и конечно же первая мысль была использовать именно его, но я решил написать свою реализацию, потому что не хотел тянуть весь встроенный механизм, ведь мне нужна была лишь малая часть его функционала. На одном личном проекте на C++ мне потребовалось получать информацию о типах объектов во время выполнения приложения. А еще хотелось попробовать на практике новые возможности C++ 17, с которыми я был не особо знаком.

В этом посте представлю пример работы с парсером libclang на языке Python.

Важным для нас в данном случае являются только следующие моменты: Детали релизации своего RTTI я опущу.

  • Каждый класс или структура, которые могут предоставить информацию о своём типе должны наследовать интерфейс IRttiTypeIdProvider;
  • В каждом таком классе (если он не является абстрактным) необходимо добавить макрос RTTI_HAS_TYPE_ID, который добавляет статическое поле типа указатель на объект RttiTypeId. Таким образом, чтобы получить идентификатор типа можно писать MyClass::__typeId или вызывать метод getTypeId у конкретного экземпляра класса во время выполнения приложения.

Пример:

#pragma once #include <string>
#include "RTTI.h" struct BaseNode : public IRttiTypeIdProvider { virtual ~BaseNode() = default; bool bypass = false;
}; struct SourceNode : public BaseNode { RTTI_HAS_TYPE_ID std::string inputFilePath;
}; struct DestinationNode : public BaseNode { RTTI_HAS_TYPE_ID bool includeDebugInfo = false; std::string outputFilePath;
}; struct MultiplierNode : public BaseNode { RTTI_HAS_TYPE_ID double multiplier;
}; struct InverterNode : public BaseNode { RTTI_HAS_TYPE_ID
};

Чтобы реализовать всё это, придётся вручную формировать структуру с описанием каждого поля интересующего класса где-то в .cpp-файле. С этим уже можно было работать, но через какое-то время мне понадобилось получать информацию о полях этих классов: имя поля, смещение и размер. Написав несколько макросов, описание типа и его полей стало выглядеть так:

RTTI_PROVIDER_BEGIN_TYPE(SourceNode)
( RTTI_DEFINE_FIELD(SourceNode, bypass) RTTI_DEFINE_FIELD(SourceNode, inputFilePath)
)
RTTI_PROVIDER_END_TYPE() RTTI_PROVIDER_BEGIN_TYPE(DestinationNode)
( RTTI_DEFINE_FIELD(DestinationNode, bypass) RTTI_DEFINE_FIELD(DestinationNode, includeDebugInfo) RTTI_DEFINE_FIELD(DestinationNode, outputFilePath)
)
RTTI_PROVIDER_END_TYPE() RTTI_PROVIDER_BEGIN_TYPE(MultiplierNode)
( RTTI_DEFINE_FIELD(MultiplierNode, bypass) RTTI_DEFINE_FIELD(MultiplierNode, multiplier)
)
RTTI_PROVIDER_END_TYPE() RTTI_PROVIDER_BEGIN_TYPE(InverterNode)
( RTTI_DEFINE_FIELD(InverterNode, bypass)
)

Какие можно выявить проблемы? И это всего для 4-х классов.

  1. При копипасте блоков кода вручную можно упустить из виду имя класса при определении поля (скопистили блок с SourceNode для DestinationNode, но в одном из полей забыли поменять SourceNode на DestinationNode). Компилятор всё пропустит, приложение может даже не упасть, однако информация о поле будет некорректной. А если делать запись или чтение данных основываясь на информации из такого поля, то всё взорвётся (так говорят, но сам не хочу проверять).
  2. Если добавить поле в базовый класс, то нужно обновлять ВСЕ записи.
  3. Если поменяли имя или порядок полей в классе, то нужно не забыть обновить имя и порядок в этой портянке кода.

Когда дело касается подобного монотонного кода я становлюсь очень ленивым и ищу способ сгенерировать его автоматически, даже если на это уйдёт больше времени и сил чем на ручное написание. Но главное — это всё нужно писать вручную.

Но мы имеем дело не просто с шаблонным текстом, а текстом, построенным на основе исходного кода на C++. В этом мне помогает Python, на нём я пишу скрипты для решения подобных проблем. Нужно средство для получения информации о C++ коде, и в этом нам поможет libclang.

Предоставляет API к средствам для синтаксического анализа исходного кода в абстрактном синтаксическом дереве (AST), загрузки уже проанализированных AST, обхода AST, сопоставления местоположений физического источника с элементами внутри AST и других средств из набора Clang. libclang — это высокоуровневый C-интерфейс для Clang.

На момент написания этого поста официальной такой библиотеки для Python нет, но из неофициальных есть вот эта https://github.com/ethanhs/clang. Как следует из описания, libclang предоставляет C-интерфейс, а для работы с ним через Python нужна связывающая библиотека (binding).

Устанавливаем её через менеджер пакетов:

pip install clang

Но для понимания устройства libclang нужно читать документацию libclang. Библиотека снабжена комментариями в исходом коде. У тех, кто уже имел опыт с libclang вопросов будет меньше, а лично я такого опыта не имел, поэтому пришлось знатно покопаться в коде и потыкаться в отладчике. Примеров использования библиотеки не много, и там нет комментариев поясняющих почему всё работает вот так, а не иначе.

Начнём с простого примера:

import clang.cindex index = clang.cindex.Index.create()
translation_unit = index.parse('my_source.cpp', args=['-std=c++17']) for i in translation_unit.get_tokens(extent=translation_unit.cursor.extent): print (i.kind)

Метод parse возвращает объект типа TranslationUnit, это единица трансляции кода. Здесь создаётся объект типа Index, который умеет парсить файл с кодом на C++. Циклом проходим по всем лексемам (token) в TranslationUnit и выводим тип этих лексем (свойство kind). TranslationUnit является узлом (node) AST, а каждый узел AST хранит информацию о своём положении в исходном коде (extent).

Для примера возьмём следующий C++ код:

class X ; class Y {}; class Z : public X {};

Результат выполнения скрипта

TokenKind.KEYWORD
TokenKind.IDENTIFIER
TokenKind.PUNCTUATION
TokenKind.PUNCTUATION
TokenKind.PUNCTUATION
TokenKind.KEYWORD
TokenKind.IDENTIFIER
TokenKind.PUNCTUATION
TokenKind.PUNCTUATION
TokenKind.PUNCTUATION
TokenKind.KEYWORD
TokenKind.IDENTIFIER
TokenKind.PUNCTUATION
TokenKind.KEYWORD
TokenKind.IDENTIFIER
TokenKind.PUNCTUATION
TokenKind.PUNCTUATION
TokenKind.PUNCTUATION

Прежде чем писать код на Python давайте посмотрим что вообще нам стоит ожидать от парсера clang. Теперь займёмся обработкой AST. Запустим clang в режиме дампа AST:

clang++ -cc1 -ast-dump my_source.cpp

Результат выполнения команды

TranslationUnitDecl 0xaaaa9b9fa8 <<invalid sloc>> <invalid sloc>
|-TypedefDecl 0xaaaa9ba880 <<invalid sloc>> <invalid sloc> implicit __int128_t '__int128'
| `-BuiltinType 0xaaaa9ba540 '__int128'
|-TypedefDecl 0xaaaa9ba8e8 <<invalid sloc>> <invalid sloc> implicit __uint128_t 'unsigned __int128'
| `-BuiltinType 0xaaaa9ba560 'unsigned __int128'
|-TypedefDecl 0xaaaa9bac48 <<invalid sloc>> <invalid sloc> implicit __NSConstantString '__NSConstantString_tag'
| `-RecordType 0xaaaa9ba9d0 '__NSConstantString_tag'
| `-CXXRecord 0xaaaa9ba938 '__NSConstantString_tag'
|-TypedefDecl 0xaaaa9e6570 <<invalid sloc>> <invalid sloc> implicit __builtin_ms_va_list 'char *'
| `-PointerType 0xaaaa9e6530 'char *'
| `-BuiltinType 0xaaaa9ba040 'char'
|-TypedefDecl 0xaaaa9e65d8 <<invalid sloc>> <invalid sloc> implicit __builtin_va_list 'char *'
| `-PointerType 0xaaaa9e6530 'char *'
| `-BuiltinType 0xaaaa9ba040 'char'
|-CXXRecordDecl 0xaaaa9e6628 <my_source.cpp:1:1, col:10> col:7 referenced class X definition
| |-DefinitionData pass_in_registers empty aggregate standard_layout trivially_copyable pod trivial literal has_constexpr_non_copy_move_ctor can_const_default_init
| | |-DefaultConstructor exists trivial constexpr needs_implicit defaulted_is_constexpr
| | |-CopyConstructor simple trivial has_const_param needs_implicit implicit_has_const_param
| | |-MoveConstructor exists simple trivial needs_implicit
| | |-CopyAssignment trivial has_const_param needs_implicit implicit_has_const_param
| | |-MoveAssignment exists simple trivial needs_implicit
| | `-Destructor simple irrelevant trivial needs_implicit
| `-CXXRecordDecl 0xaaaa9e6748 <col:1, col:7> col:7 implicit class X
|-CXXRecordDecl 0xaaaa9e6800 <line:3:1, col:10> col:7 class Y definition
| |-DefinitionData pass_in_registers empty aggregate standard_layout trivially_copyable pod trivial literal has_constexpr_non_copy_move_ctor can_const_default_init
| | |-DefaultConstructor exists trivial constexpr needs_implicit defaulted_is_constexpr
| | |-CopyConstructor simple trivial has_const_param needs_implicit implicit_has_const_param
| | |-MoveConstructor exists simple trivial needs_implicit
| | |-CopyAssignment trivial has_const_param needs_implicit implicit_has_const_param
| | |-MoveAssignment exists simple trivial needs_implicit
| | `-Destructor simple irrelevant trivial needs_implicit
| `-CXXRecordDecl 0xaaaa9e6928 <col:1, col:7> col:7 implicit class Y
`-CXXRecordDecl 0xaaaa9e69e0 <line:5:1, col:21> col:7 class Z definition |-DefinitionData pass_in_registers empty standard_layout trivially_copyable trivial literal has_constexpr_non_copy_move_ctor can_const_default_init | |-DefaultConstructor exists trivial constexpr needs_implicit defaulted_is_constexpr | |-CopyConstructor simple trivial has_const_param needs_implicit implicit_has_const_param | |-MoveConstructor exists simple trivial needs_implicit | |-CopyAssignment trivial has_const_param needs_implicit implicit_has_const_param | |-MoveAssignment exists simple trivial needs_implicit | `-Destructor simple irrelevant trivial needs_implicit |-public 'X' `-CXXRecordDecl 0xaaaa9e6b48 <col:1, col:7> col:7 implicit class Z

Можно заметить, что таких узлов здесь больше чем классов в исходном файле. Здесь CXXRecordDecl это тип узла, представляющего декларацию класса. узлы, которые являются ссылками на другие узлы. Это потому, что таким же типом представлены референсные узлы, т.е. При разборке этого дерева определить референсный узел можно при помощи специального флага. В нашем случае указание базового класса это и есть референс.

Теперь напишем скрипт, выводящий список классов в исходном файле:

import clang.cindex
import typing index = clang.cindex.Index.create()
translation_unit = index.parse('my_source.cpp', args=['-std=c++17']) def filter_node_list_by_node_kind( nodes: typing.Iterable[clang.cindex.Cursor], kinds: list
) -> typing.Iterable[clang.cindex.Cursor]: result = [] for i in nodes: if i.kind in kinds: result.append(i) return result all_classes = filter_node_list_by_node_kind(translation_unit.cursor.get_children(), [clang.cindex.CursorKind.CLASS_DECL, clang.cindex.CursorKind.STRUCT_DECL]) for i in all_classes: print (i.spelling)

Для разного типа узлов значение spelling может содержать какие-нибудь модификаторы типа, но для декларации класса или структуры оно содержит имя без модификаторов. Имя класса хранится в свойстве spelling.

Результат выполнения:

X
Y
Z

Попробуйте добавить #include <string> в исходник, и в дампе получите 84 тысячи строк, что явно многовато для решения нашей задачки. При парсинге AST clang также парсит файлы, подключенные через #include.

Вернёте их обратно когда изучите AST и получите представление об иерархии и типах в интересующем файле. Для просмотра дампа AST таких файлов через командную строку лучше удалить все #include.

В скрипте для того чтобы фильтровать только AST принадлежащие исходному файлу, а не подключенные через #include, можно добавить такую функцию фильтрации по файлу:

def filter_node_list_by_file( nodes: typing.Iterable[clang.cindex.Cursor], file_name: str
) -> typing.Iterable[clang.cindex.Cursor]: result = [] for i in nodes: if i.location.file.name == file_name: result.append(i) return result
... filtered_ast = filter_by_file(translation_unit.cursor, translation_unit.spelling)

Далее приведу полный код, который формирует список полей с учётом наследования и генерирует текст по шаблону. Теперь можно заняться извлечением полей. Ничего специфического для clang здесь нет, поэтому без комментариев.

Полный код скрипта

import clang.cindex
import typing index = clang.cindex.Index.create()
translation_unit = index.parse('Input.h', args=['-std=c++17']) def filter_node_list_by_file( nodes: typing.Iterable[clang.cindex.Cursor], file_name: str
) -> typing.Iterable[clang.cindex.Cursor]: result = [] for i in nodes: if i.location.file.name == file_name: result.append(i) return result def filter_node_list_by_node_kind( nodes: typing.Iterable[clang.cindex.Cursor], kinds: list
) -> typing.Iterable[clang.cindex.Cursor]: result = [] for i in nodes: if i.kind in kinds: result.append(i) return result def is_exposed_field(node): return node.access_specifier == clang.cindex.AccessSpecifier.PUBLIC def find_all_exposed_fields( cursor: clang.cindex.Cursor
): result = [] field_declarations = filter_node_list_by_node_kind(cursor.get_children(), [clang.cindex.CursorKind.FIELD_DECL]) for i in field_declarations: if not is_exposed_field(i): continue result.append(i.displayname) return result source_nodes = filter_node_list_by_file(translation_unit.cursor.get_children(), translation_unit.spelling)
all_classes = filter_node_list_by_node_kind(source_nodes, [clang.cindex.CursorKind.CLASS_DECL, clang.cindex.CursorKind.STRUCT_DECL]) class_inheritance_map = {}
class_field_map = {} for i in all_classes: bases = [] for node in i.get_children(): if node.kind == clang.cindex.CursorKind.CXX_BASE_SPECIFIER: referenceNode = node.referenced bases.append(node.referenced) class_inheritance_map[i.spelling] = bases for i in all_classes: fields = find_all_exposed_fields(i) class_field_map[i.spelling] = fields def populate_field_list_recursively(class_name: str): field_list = class_field_map.get(class_name) if field_list is None: return [] baseClasses = class_inheritance_map[class_name] for i in baseClasses: field_list = populate_field_list_recursively(i.spelling) + field_list return field_list rtti_map = {} for class_name, class_list in class_inheritance_map.items(): rtti_map[class_name] = populate_field_list_recursively(class_name) for class_name, field_list in rtti_map.items(): wrapper_template = """\
RTTI_PROVIDER_BEGIN_TYPE(%s)
(
%s
)
RTTI_PROVIDER_END_TYPE() """ rendered_fields = [] for f in field_list: rendered_fields.append(" RTTI_DEFINE_FIELD(%s, %s)" % (class_name, f)) print (wrapper_template % (class_name, ",\n".join(rendered_fields)))

Поэтому после получения результата придётся вручную удалить блоки, описывающие классы без RTTI. Этот скрипт не учитывает имеет ли класс RTTI. Но это мелочь.

Весь код выложен на GitHub. Надеюсь кому-нибудь это будет полезно и сэкономит время.


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

Ваш email нигде не будет показан
Обязательные для заполнения поля помечены *

*

x

Ещё Hi-Tech Интересное!

Древности: невероятная видеокассета

Сейчас, в 2019 году видеокассета потеряла всякую актуальность. Когда год назад я решил оцифровать свои старые записи, и не без труда вывел картинку с видеомагнитофона на современную метровую ЖК-панель, это был опыт, сравнимый с прослушиванием грамофонных пластинок на 78 оборотов. ...

Я прочитал 80 резюме, у меня есть вопросы

У нас не очень простое собеседование. Нужно пройти 3 шага: Прислать резюме, программист его посмотрит, лайкнет если всё хорошо. Рекрутер позвонит, задаст несколько вопросов Встретиться или созвониться с нами. Узнаем, какой вы специалист. Прийти на тестовый день. Познакомиться с командой ...