Sur un projet C ++ personnel, j'avais besoin d'obtenir des informations sur les types d'objets au moment de l'exécution. C ++ a un mécanisme RTTI (Run-Time Type Information) intégré, et bien sûr la première pensée a été de l'utiliser, mais j'ai décidé d'écrire mon implémentation parce que je ne voulais pas extraire tout le mécanisme intégré, car je n'avais besoin que d'une petite partie de ses fonctionnalités. Je voulais également essayer en pratique les nouvelles fonctionnalités de C ++ 17, avec lesquelles je n'étais pas particulièrement familier.
Dans cet article, je présenterai un exemple de travail avec l'analyseur libclang en Python.
Je vais omettre les détails de la libération de mon RTTI. Pour nous, dans ce cas, seuls les points suivants sont importants:
- Chaque classe ou structure qui peut fournir des informations sur son type doit hériter de l'interface
IRttiTypeIdProvider
; - Dans chacune de ces classes (si elle n'est pas abstraite), vous devez ajouter la macro
RTTI_HAS_TYPE_ID
, qui ajoute un champ statique de pointeur de type Ă un objet RttiTypeId
. Ainsi, pour obtenir l'identifiant de type, vous pouvez écrire MyClass::__typeId
ou appeler la méthode getTypeId
une instance spécifique de la classe au moment de l'exécution.
Un exemple:
#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 };
Il était déjà possible de travailler avec cela, mais après un certain temps, j'avais besoin d'obtenir des informations sur les champs de ces classes: nom du champ, décalage et taille. Pour implémenter tout cela, vous devrez former manuellement une structure avec une description de chaque champ de la classe d'intérêt quelque part dans le fichier .cpp. Après avoir écrit plusieurs macros, la description du type et de ses champs a commencé à ressembler à ceci:
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) )
Et ce n'est que pour 4 classes. Quels problèmes peuvent être identifiés?
- Lors de la copie manuelle de blocs de code, vous pouvez perdre de vue le nom de la classe lors de la définition du champ (nous avons copié le bloc de SourceNode à DestinationNode, mais nous avons oublié de changer SourceNode en DestinationNode dans l'un des champs). Le compilateur sautera tout, l'application peut même ne pas tomber, mais les informations sur le champ seront incorrectes. Et si vous enregistrez ou lisez des données basées sur des informations d'un tel champ, alors tout explosera (ils le disent, mais je ne veux pas le vérifier moi-même).
- Si vous ajoutez un champ Ă la classe de base, TOUS les enregistrements doivent ĂŞtre mis Ă jour.
- Si vous modifiez le nom ou l'ordre des champs dans la classe, vous devez vous rappeler de mettre Ă jour le nom et l'ordre dans ce footcloth de code.
Mais l'essentiel est que tout cela doit être écrit manuellement. Quand il s'agit d'un code aussi monotone, je deviens très paresseux et cherche un moyen de le générer automatiquement, même si cela prend plus de temps et d'efforts que l'écriture manuelle.
Python m'aide, j'écris des scripts dessus pour résoudre de tels problèmes. Mais nous ne traitons pas seulement avec du texte de modèle, mais avec du texte construit sur la base du code source C ++. Nous avons besoin d'un outil pour obtenir des informations sur le code C ++, et libclang nous y aidera.
libclang est une interface C de haut niveau pour Clang. Fournit une API pour les outils permettant d'analyser le code source dans une arborescence de syntaxe abstraite (AST), de charger des AST déjà analysés, de parcourir des AST, de mapper des emplacements de source physique à des éléments dans l'AST et d'autres outils Clang.
Comme indiqué dans la description, libclang fournit une interface C, et pour travailler avec Python, vous avez besoin d'une bibliothèque de liaisons (binding). Au moment de la rédaction de cet article, il n'y a pas de bibliothèque officielle de ce type pour Python, mais du non officiel, il y a ce https://github.com/ethanhs/clang .
Installez-le via le gestionnaire de packages:
pip install clang
La bibliothèque est fournie avec des commentaires dans le code source. Mais pour comprendre le périphérique libclang, vous devez lire la documentation libclang . Il n'y a pas beaucoup d'exemples d'utilisation de la bibliothèque, et aucun commentaire n'explique pourquoi tout fonctionne comme ça, et pas autrement. Ceux qui avaient déjà de l'expérience avec libclang auront moins de questions, mais personnellement, je n'avais pas une telle expérience, j'ai donc dû creuser profondément dans le code et fouiller dans le débogueur.
Commençons par un exemple 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)
Ici, un objet de type Index
est créé qui peut analyser un fichier avec du code C ++. La méthode d' parse
renvoie un objet de type TranslationUnit
, c'est une unité de traduction de code. TranslationUnit
est un nœud AST, et chaque nœud AST stocke des informations sur sa position dans le code source (étendue). Nous parcourons tous les jetons dans TranslationUnit
et imprimons le type de ces jetons (propriété kind).
Par exemple, prenez le code C ++ suivant:
class X {}; class Y {}; class Z : public X {};
Résultat de l'exécution du 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
Maintenant, traitons AST. Avant d'écrire du code Python, voyons ce que nous devons attendre de l'analyseur de clang. Exécutez clang en mode de vidage AST:
clang++ -cc1 -ast-dump my_source.cpp
Résultat de la commande 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
Ici CXXRecordDecl
est le type de nœud représentant la déclaration de classe. Vous pouvez remarquer qu'il y a plus de nœuds de ce type que de classes dans le fichier source. En effet, les nœuds de référence sont représentés par le même type, c'est-à -dire nœuds qui sont des liens vers d'autres nœuds. Dans notre cas, la spécification de la classe de base est la référence. Lors du démontage de cette arborescence, vous pouvez déterminer le nœud de référence à l'aide d'un indicateur spécial.
Nous allons maintenant écrire un script répertoriant les classes dans le fichier source:
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)
Le nom de classe est stocké dans la propriété d' spelling
. Pour différents types de nœuds, la valeur de l' spelling
peut contenir certains modificateurs de type, mais pour une déclaration de classe ou de structure, elle contient un nom sans modificateurs.
Résultat d'exécution:
X Y Z
Lors de l'analyse, AST clang analyse également les fichiers connectés via #include
. Essayez d'ajouter #include <string>
à la source, et dans le vidage, vous obtiendrez 84 000 lignes, ce qui est évidemment un peu beaucoup pour résoudre notre problème.
Pour afficher le vidage AST de ces fichiers via la ligne de commande, il est préférable de supprimer tous les #include
. Ramenez-les lorsque vous étudiez l'AST et ayez une idée de la hiérarchie et des types dans le fichier qui vous intéresse.
Dans le script, afin de filtrer uniquement les AST appartenant au fichier source et non connectés via #include
, vous pouvez ajouter la fonction de filtrage suivante par fichier:
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)
Vous pouvez maintenant faire l'extraction des champs. Ensuite, je donnerai le code complet qui génère une liste de champs en tenant compte de l'héritage et génère du texte selon le modèle. Il n'y a rien de spécifique à cliqueter ici, donc pas de commentaire.
Code de script complet 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)))
Ce script ne considère pas si la classe a RTTI. Par conséquent, après avoir reçu le résultat, vous devrez supprimer manuellement les blocs décrivant les classes sans RTTI. Mais c'est une bagatelle.
J'espère que quelqu'un sera utile et gagnera du temps. Tout le code est publié sur GitHub .