En un proyecto personal de C ++, necesitaba obtener información sobre los tipos de objetos en tiempo de ejecución. C ++ tiene un mecanismo de información de tipo de tiempo de ejecución (RTTI) incorporado y, por supuesto, lo primero que pensé fue usarlo, pero decidí escribir mi implementación porque no quería extraer todo el mecanismo incorporado, porque solo necesitaba una pequeña parte de su funcionalidad. También quería probar en la práctica las nuevas características de C ++ 17, con las cuales no estaba particularmente familiarizado.
En esta publicación presentaré un ejemplo de trabajo con el analizador libclang en Python.
Omitiré los detalles de lanzamiento de mi RTTI. Lo importante para nosotros en este caso son solo los siguientes puntos:
- Cada clase o estructura que puede proporcionar información sobre su tipo debe heredar la interfaz
IRttiTypeIdProvider
; - En cada una de esas clases (si no es abstracta), debe agregar la macro
RTTI_HAS_TYPE_ID
, que agrega un campo estático de puntero de tipo a un objeto RttiTypeId
. Por lo tanto, para obtener un identificador de tipo, puede escribir MyClass::__typeId
o llamar al método getTypeId
una instancia específica de la clase en tiempo de ejecución.
Un ejemplo:
#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 };
Ya era posible trabajar con esto, pero después de un tiempo necesitaba obtener información sobre los campos de estas clases: nombre del campo, desplazamiento y tamaño. Para implementar todo esto, deberá formar manualmente una estructura con una descripción de cada campo de la clase de interés en algún lugar del archivo .cpp. Después de escribir varias macros, la descripción del tipo y sus campos comenzó a verse así:
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) )
Y esto es solo para 4 clases. ¿Qué problemas se pueden identificar?
- Al copiar pegar bloques de código manualmente, puede perder de vista el nombre de la clase al definir el campo (copiamos el bloque de SourceNode a DestinationNode, pero olvidamos cambiar SourceNode a DestinationNode en uno de los campos). El compilador omitirá todo, es posible que la aplicación ni siquiera se caiga, pero la información sobre el campo será incorrecta. Y si registra o lee datos basados en información de dicho campo, entonces todo explotará (lo dicen, pero no quiero comprobarlo yo mismo).
- Si agrega un campo a la clase base, TODOS los registros deben actualizarse.
- Si cambia el nombre o el orden de los campos en la clase, debe recordar actualizar el nombre y el orden en esta plantilla de código.
Pero lo principal es que todo esto debe escribirse manualmente. Cuando se trata de un código tan monótono, me vuelvo muy vago y busco una forma de generarlo automáticamente, incluso si toma más tiempo y esfuerzo que la escritura manual.
Python me ayuda con esto, escribo scripts para resolver tales problemas. Pero estamos tratando no solo con el texto de la plantilla, sino también con el texto creado sobre la base del código fuente de C ++. Necesitamos una herramienta para obtener información sobre el código C ++, y libclang nos ayudará con esto.
libclang es una interfaz C de alto nivel para Clang. Proporciona una API para herramientas para analizar el código fuente en un árbol de sintaxis abstracta (AST), cargar AST ya analizadas, recorrer AST, asignar ubicaciones de origen físico a elementos dentro de AST y otro conjunto de herramientas de Clang.
Como se deduce de la descripción, libclang proporciona una interfaz C, y para trabajar con ella a través de Python necesita una biblioteca de enlace (enlace). En el momento de escribir esta publicación, no hay una biblioteca oficial de este tipo para Python, pero de lo no oficial está https://github.com/ethanhs/clang .
Instálelo a través del administrador de paquetes:
pip install clang
La biblioteca recibe comentarios en el código fuente. Pero para comprender el dispositivo libclang, debe leer la documentación de libclang . No hay muchos ejemplos de uso de la biblioteca, y no hay comentarios que expliquen por qué todo funciona así, y no de otra manera. Aquellos que ya tenían experiencia con libclang tendrán menos preguntas, pero yo personalmente no tenía esa experiencia, así que tuve que profundizar en el código y hurgar en el depurador.
Comencemos con un ejemplo simple:
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)
Aquí se crea un objeto de tipo Index
que puede analizar un archivo con código C ++. El método de parse
devuelve un objeto de tipo TranslationUnit
, esta es una unidad de traducción de código. TranslationUnit
es un nodo AST, y cada nodo AST almacena información sobre su posición en el código fuente (extensión). Recorremos todos los tokens en TranslationUnit
e imprimimos el tipo de estos tokens (propiedad kind).
Por ejemplo, tome el siguiente código C ++:
class X {}; class Y {}; class Z : public X {};
Resultado de ejecución del 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
Ahora manejemos AST. Antes de escribir el código Python, veamos qué debemos esperar del analizador clang. Ejecute clang en modo de volcado AST:
clang++ -cc1 -ast-dump my_source.cpp
Resultado del 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
Aquí CXXRecordDecl
es el tipo de nodo que representa la declaración de clase. Puede notar que hay más nodos de este tipo que clases en el archivo fuente. Esto se debe a que los nodos de referencia están representados por el mismo tipo, es decir nodos que son enlaces a otros nodos. En nuestro caso, especificar la clase base es la referencia. Al desmontar este árbol, puede determinar el nodo de referencia utilizando una bandera especial.
Ahora escribiremos un script que enumere las clases en el archivo fuente:
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)
El nombre de la clase se almacena en la propiedad de spelling
. Para diferentes tipos de nodos, el valor de spelling
puede contener algunos modificadores de tipo, pero para una declaración de clase o estructura, contiene un nombre sin modificadores.
Resultado de ejecución:
X Y Z
Al analizar, AST clang también analiza los archivos conectados a través de #include
. Intente agregar #include <string>
a la fuente, y en el volcado obtendrá 84 mil líneas, lo que obviamente es un poco demasiado para resolver nuestro problema.
Para ver el volcado de AST de dichos archivos a través de la línea de comando, es mejor eliminar todos los #include
. Tráigalos de vuelta cuando estudies AST y ten una idea de la jerarquía y los tipos en el archivo de interés.
En el script, para filtrar solo AST que pertenece al archivo fuente y no está conectado a través de #include
, puede agregar la siguiente función de filtrado por archivo:
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)
Ahora puedes hacer la extracción de campos. A continuación, daré el código completo que genera una lista de campos teniendo en cuenta la herencia y genera texto de acuerdo con la plantilla. No hay nada específico sobre el sonido metálico aquí, así que no hay comentarios.
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 no considera si la clase tiene RTTI. Por lo tanto, después de recibir el resultado, deberá eliminar manualmente los bloques que describen las clases sin RTTI. Pero esto es un poco.
Espero que alguien sea útil y ahorre tiempo. Todo el código se publica en GitHub .