Um exemplo de análise de código C ++ usando libclang em Python

Em um projeto C ++ pessoal, eu precisava obter informações sobre os tipos de objetos em tempo de execução. O C ++ possui um mecanismo interno de informações de tipo de tempo de execução (RTTI) e, é claro, o primeiro pensamento foi usá-lo, mas decidi escrever minha implementação porque não queria extrair todo o mecanismo interno, porque precisava de apenas uma pequena parte de sua funcionalidade. Eu também queria experimentar na prática os novos recursos do C ++ 17, com os quais não estava particularmente familiarizado.


Neste post, apresentarei um exemplo de trabalho com o analisador libclang no Python.


Omitirei os detalhes da liberação do meu RTTI. Importante para nós, neste caso, são apenas os seguintes pontos:


  • Cada classe ou estrutura que pode fornecer informações sobre seu tipo deve herdar a interface IRttiTypeIdProvider ;
  • Em cada uma dessas classes (se não for abstrata), você deve adicionar a macro RTTI_HAS_TYPE_ID , que adiciona um campo estático do ponteiro de tipo a um objeto RttiTypeId . Portanto, para obter um identificador de tipo, você pode escrever MyClass::__typeId ou chamar o método getTypeId uma instância específica da classe em tempo de execução.

Um exemplo:


 #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 }; 

Já era possível trabalhar com isso, mas depois de algum tempo eu precisei obter informações sobre os campos dessas classes: nome do campo, deslocamento e tamanho. Para implementar tudo isso, você precisará formar manualmente uma estrutura com uma descrição de cada campo da classe de interesse em algum lugar do arquivo .cpp. Depois de escrever várias macros, a descrição do tipo e seus campos começaram a ficar assim:


 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) ) 

E isso é apenas para 4 classes. Quais problemas podem ser identificados?


  1. Ao copiar colar blocos de código manualmente, você pode perder de vista o nome da classe ao definir o campo (copiamos o bloco de SourceNode para DestinationNode, mas esquecemos de alterar SourceNode para DestinationNode em um dos campos). O compilador pulará tudo, o aplicativo pode nem cair, mas as informações sobre o campo estarão incorretas. E se você gravar ou ler dados com base nas informações desse campo, tudo explodirá (eles dizem isso, mas eu não quero checar).
  2. Se você adicionar um campo à classe base, TODOS os registros deverão ser atualizados.
  3. Se você alterar o nome ou a ordem dos campos na classe, lembre-se de atualizar o nome e a ordem neste código de código.

Mas o principal é que tudo isso deve ser escrito manualmente. Quando se trata de um código tão monótono, fico muito preguiçoso e procuro uma maneira de gerá-lo automaticamente, mesmo que leve mais tempo e esforço do que a escrita manual.


Python me ajuda com isso, escrevo scripts para resolver esses problemas. Mas não estamos lidando apenas com o texto do modelo, mas com o texto criado com base no código-fonte C ++. Precisamos de uma ferramenta para obter informações sobre o código C ++, e a libclang nos ajudará com isso.


libclang é uma interface C de alto nível para o Clang. Fornece uma API para ferramentas para analisar o código-fonte em uma árvore de sintaxe abstrata (AST), carregando ASTs já analisados, atravessando ASTs, mapeando locais de origem física para elementos dentro de ASTs e outro conjunto de ferramentas Clang.

Como segue a descrição, libclang fornece uma interface C e, para trabalhar com ela através do Python, você precisa de uma biblioteca de ligação (binding). No momento em que escrevi este post, não havia uma biblioteca oficial desse tipo para Python, mas do não-oficial existe este https://github.com/ethanhs/clang .


Instale-o através do gerenciador de pacotes:


 pip install clang 

A biblioteca é fornecida com comentários no código fonte. Mas para entender o dispositivo libclang, você precisa ler a documentação da libclang . Não há muitos exemplos de uso da biblioteca e não há comentários explicando por que tudo funciona assim, e não o contrário. Quem já teve experiência com libclang terá menos perguntas, mas eu pessoalmente não tinha essa experiência, então tive que me aprofundar no código e vasculhar o depurador.


Vamos começar com um exemplo simples:


 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) 

Aqui, é criado um objeto do tipo Index que pode analisar um arquivo com código C ++. O método de parse retorna um objeto do tipo TranslationUnit , que é uma unidade de tradução de código. TranslationUnit é um nó AST, e cada nó AST armazena informações sobre sua posição no código-fonte (extensão). Percorremos todos os tokens no TranslationUnit e imprimimos o tipo desses tokens (propriedade de tipo).


Por exemplo, use o seguinte código C ++:


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

Resultado da execução do script
 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 

Agora vamos lidar com o AST. Antes de escrever o código Python, vamos ver o que devemos esperar do analisador de clang. Execute clang no modo de despejo AST:


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

Resultado do comando
 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 

Aqui CXXRecordDecl é o tipo de nó que representa a declaração de classe. Você pode perceber que existem mais nós do que classes no arquivo de origem. Isso ocorre porque os nós de referência são representados pelo mesmo tipo, ou seja, nós que são links para outros nós. No nosso caso, especificar a classe base é a referência. Ao desmontar esta árvore, você pode determinar o nó de referência usando um sinalizador especial.


Agora, escreveremos um script listando as classes no arquivo de origem:


 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) 

O nome da classe é armazenado na propriedade de spelling . Para diferentes tipos de nós, o valor spelling pode conter alguns modificadores de tipo, mas para uma declaração de classe ou estrutura, ele contém um nome sem modificadores.


Resultado da execução:


 X Y Z 

Ao analisar, o AST clang também analisa arquivos conectados via #include . Tente adicionar #include <string> à fonte e, no despejo, você terá 84 mil linhas, o que é obviamente um pouco demais para resolver nosso problema.


Para visualizar o despejo AST desses arquivos por meio da linha de comando, é melhor excluir todos os #include . Traga-os de volta ao estudar AST e tenha uma idéia da hierarquia e tipos no arquivo de interesse.


No script, para filtrar apenas o AST pertencente ao arquivo de origem e não conectado por meio de #include , você pode adicionar a seguinte função de filtragem por arquivo:


 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) 

Agora você pode fazer a extração de campos. A seguir, fornecerei o código completo que gera uma lista de campos levando em consideração a herança e gera o texto de acordo com o modelo. Não há nada específico para clang aqui, então nenhum comentário.


Código de script completo
 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))) 

Este script não considera se a classe possui RTTI. Portanto, após receber o resultado, você terá que excluir manualmente os blocos que descrevem as classes sem o RTTI. Mas isso é um pouco.


Espero que alguém seja útil e economize tempo. Todo o código é publicado no GitHub .

Source: https://habr.com/ru/post/pt439270/


All Articles