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

 

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

Автор: admin от 6-02-2019, 22:50, посмотрело: 32

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



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

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

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



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



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)


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



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



class X {};

class Y {};

class Z : public X {};




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



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




Здесь 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


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



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



В скрипте для того чтобы фильтровать только 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 здесь нет, поэтому без комментариев.





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



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



Источник: Хабр / Интересные публикации

Категория: Android

Уважаемый посетитель, Вы зашли на сайт как незарегистрированный пользователь.
Мы рекомендуем Вам зарегистрироваться либо войти на сайт под своим именем.

Добавление комментария

Имя:*
E-Mail:
Комментарий:
Полужирный Наклонный текст Подчеркнутый текст Зачеркнутый текст | Выравнивание по левому краю По центру Выравнивание по правому краю | Вставка смайликов Выбор цвета | Скрытый текст Вставка цитаты Преобразовать выбранный текст из транслитерации в кириллицу Вставка спойлера
Введите два слова, показанных на изображении: *