In einem persönlichen C ++ - Projekt musste ich zur Laufzeit Informationen über die Objekttypen abrufen. C ++ verfügt über einen integrierten RTTI-Mechanismus (Run-Time Type Information), und natürlich war der erste Gedanke, ihn zu verwenden. Ich habe mich jedoch entschlossen, meine Implementierung zu schreiben, da ich nicht den gesamten integrierten Mechanismus abrufen wollte, da ich nur einen kleinen Teil seiner Funktionalität benötigte. Ich wollte auch die neuen Funktionen von C ++ 17 ausprobieren, mit denen ich nicht besonders vertraut war.
In diesem Beitrag werde ich ein Beispiel für die Arbeit mit dem libclang-Parser in Python vorstellen.
Ich werde die Details zur Veröffentlichung meiner RTTI weglassen. Wichtig für uns sind in diesem Fall nur folgende Punkte:
- Jede Klasse oder Struktur, die Informationen zu ihrem Typ bereitstellen kann, muss die
IRttiTypeIdProvider
Schnittstelle erben. - In jeder dieser Klassen (wenn sie nicht abstrakt sind) müssen Sie das Makro
RTTI_HAS_TYPE_ID
hinzufügen, das einem RttiTypeId
Objekt ein statisches Feld vom RttiTypeId
. Um die MyClass::__typeId
getTypeId
, können Sie MyClass::__typeId
schreiben oder die getTypeId
Methode getTypeId
Laufzeit getTypeId
eine bestimmte Instanz der Klasse aufrufen.
Ein Beispiel:
#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 };
Es war bereits möglich, damit zu arbeiten, aber nach einiger Zeit musste ich Informationen über die Felder dieser Klassen erhalten: Feldname, Versatz und Größe. Um all dies zu implementieren, müssen Sie manuell eine Struktur mit einer Beschreibung jedes Felds der interessierenden Klasse irgendwo in der CPP-Datei erstellen. Nachdem mehrere Makros geschrieben wurden, sah die Beschreibung des Typs und seiner Felder folgendermaßen aus:
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) )
Und das ist nur für 4 Klassen. Welche Probleme können identifiziert werden?
- Wenn Sie Codeblöcke manuell einfügen, können Sie den Klassennamen beim Definieren des Felds aus den Augen verlieren (wir haben den Block von SourceNode nach DestinationNode kopiert, aber vergessen, SourceNode in DestinationNode in einem der Felder zu ändern). Der Compiler überspringt alles, die Anwendung fällt möglicherweise nicht einmal, aber die Informationen über das Feld sind falsch. Und wenn Sie Daten basierend auf Informationen aus einem solchen Feld aufzeichnen oder lesen, explodiert alles (sie sagen es, aber ich möchte es nicht selbst überprüfen).
- Wenn Sie der Basisklasse ein Feld hinzufügen, müssen ALLE Datensätze aktualisiert werden.
- Wenn Sie den Namen oder die Reihenfolge der Felder in der Klasse ändern, müssen Sie daran denken, den Namen und die Reihenfolge in diesem Fußtuchcode zu aktualisieren.
Hauptsache aber, alles muss manuell geschrieben werden. Wenn es um solch einen monotonen Code geht, werde ich sehr faul und suche nach einer Möglichkeit, ihn automatisch zu generieren, selbst wenn es mehr Zeit und Mühe kostet als manuelles Schreiben.
Python hilft mir dabei, ich schreibe Skripte darauf, um solche Probleme zu lösen. Wir haben es aber nicht nur mit Vorlagentext zu tun, sondern auch mit Text, der auf C ++ - Quellcode basiert. Wir benötigen ein Tool, um Informationen über C ++ - Code zu erhalten, und libclang hilft uns dabei.
libclang ist eine übergeordnete C-Schnittstelle für Clang. Bietet eine API für Tools zum Parsen von Quellcode in einem abstrakten Syntaxbaum (AST), zum Laden bereits analysierter ASTs, zum Durchlaufen von ASTs, zum Zuordnen physischer Quellpositionen zu Elementen innerhalb des AST und zu anderen Clang-Toolsets.
Wie aus der Beschreibung hervorgeht, bietet libclang eine C-Schnittstelle. Um mit Python arbeiten zu können, benötigen Sie eine Bindungsbibliothek (Bindung). Zum Zeitpunkt des Schreibens dieses Beitrags gibt es keine offizielle Bibliothek für Python, aber von der inoffiziellen gibt es diese https://github.com/ethanhs/clang .
Installieren Sie es über den Paketmanager:
pip install clang
Die Bibliothek enthält Kommentare im Quellcode. Um das libclang-Gerät zu verstehen, müssen Sie die libclang-Dokumentation lesen. Es gibt nicht viele Beispiele für die Verwendung der Bibliothek, und es gibt keine Kommentare, die erklären, warum alles so funktioniert, und nicht anders. Diejenigen, die bereits Erfahrung mit libclang hatten, werden weniger Fragen haben, aber ich persönlich hatte keine solche Erfahrung, also musste ich tief in den Code eintauchen und im Debugger herumstöbern.
Beginnen wir mit einem einfachen Beispiel:
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)
Hier wird ein Objekt vom Typ Index
erstellt, das eine Datei mit C ++ - Code analysieren kann. Die parse
gibt ein Objekt vom Typ TranslationUnit
. Dies ist eine Einheit der Codeübersetzung. TranslationUnit
ist ein AST-Knoten, und jeder AST-Knoten speichert Informationen über seine Position im Quellcode (Umfang). Wir durchlaufen alle Token in TranslationUnit
und drucken den Typ dieser Token (kind-Eigenschaft).
Nehmen Sie zum Beispiel den folgenden C ++ - Code:
class X {}; class Y {}; class Z : public X {};
Ergebnis der Skriptausführung 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
Lassen Sie uns jetzt AST behandeln. Bevor wir Python-Code schreiben, wollen wir sehen, was wir vom Clang-Parser erwarten können. Führen Sie clang im AST-Dump-Modus aus:
clang++ -cc1 -ast-dump my_source.cpp
Befehlsergebnis 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
Hier ist CXXRecordDecl
der Knotentyp, der die Klassendeklaration darstellt. Möglicherweise stellen Sie fest, dass die Quelldatei mehr solche Knoten als Klassen enthält. Dies liegt daran, dass Referenzknoten durch denselben Typ dargestellt werden, d. H. Knoten, die Links zu anderen Knoten sind. In unserem Fall ist die Angabe der Basisklasse die Referenz. Wenn Sie diesen Baum zerlegen, können Sie den Referenzknoten mithilfe eines speziellen Flags bestimmen.
Jetzt schreiben wir ein Skript, in dem die Klassen in der Quelldatei aufgelistet sind:
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)
Der Klassenname wird in der spelling
gespeichert. Für verschiedene Knotentypen kann der spelling
einige Typmodifikatoren enthalten, für eine Klassen- oder Strukturdeklaration enthält er jedoch einen Namen ohne Modifikatoren.
Ausführungsergebnis:
X Y Z
Beim Parsen analysiert AST clang auch Dateien, die über #include
. Versuchen Sie, der Quelle #include <string>
hinzuzufügen, und im Dump erhalten Sie 84.000 Zeilen, was offensichtlich ein bisschen viel ist, um unser Problem zu lösen.
Um den AST-Speicherauszug solcher Dateien über die Befehlszeile anzuzeigen, ist es besser, alle #include
zu löschen. Bringen Sie sie zurück, wenn Sie AST studieren, und machen Sie sich ein Bild von der Hierarchie und den Typen in der gewünschten Datei.
Um nur AST zu filtern, das zur Quelldatei gehört und nicht über #include
, können Sie im Skript die folgende Filterfunktion nach Datei hinzufügen:
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)
Jetzt können Sie Felder extrahieren. Als nächstes werde ich den vollständigen Code angeben, der eine Liste von Feldern unter Berücksichtigung der Vererbung generiert und Text gemäß der Vorlage generiert. Hier gibt es nichts Spezielles zu klirren, also keinen Kommentar.
Vollständiger Skriptcode 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)))
Dieses Skript berücksichtigt nicht, ob die Klasse RTTI hat. Daher müssen Sie nach Erhalt des Ergebnisses die Blöcke, die Klassen ohne RTTI beschreiben, manuell löschen. Aber das ist eine Kleinigkeit.
Ich hoffe, jemand wird nützlich sein und Zeit sparen. Der gesamte Code wird auf GitHub veröffentlicht .