在一个个人C ++项目中,我需要在运行时获取有关对象类型的信息。 C ++具有内置的运行时类型信息(RTTI)机制,当然首先想到的是使用它,但是我决定编写我的实现,因为我不想提取整个内置的机制,因为我只需要其功能的一小部分即可。 我还想在实践中尝试一下C ++ 17的新功能,对此我并不是特别熟悉。
在这篇文章中,我将提供一个在Python中使用libclang解析器的示例。
我将省略发布RTTI的详细信息。 在这种情况下,对我们而言重要的只有以下几点:
- 可以提供有关其类型的信息的每个类或结构都必须继承
IRttiTypeIdProvider
接口。 - 在每个此类(如果不是抽象的)中,必须添加
RTTI_HAS_TYPE_ID
宏,该宏将类型指针的静态字段添加到RttiTypeId
对象。 因此,要获取类型标识符,可以在运行时在类的特定实例getTypeId
编写MyClass::__typeId
或调用getTypeId
方法。
一个例子:
#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 };
已经可以使用它,但是一段时间后,我需要获取有关这些类的字段的信息:字段名称,偏移量和大小。 要实现所有这些,您将必须手动在.cpp文件中的某个位置形成一个结构,其中包含感兴趣类的每个字段的描述。 编写了几个宏之后,该类型及其字段的描述开始看起来像这样:
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) )
这仅适用于4个班级。 可以发现哪些问题?
- 手动复制粘贴的代码块时,在定义字段时您可能看不到类名(我们将代码块从SourceNode复制到DestinationNode,但是忘记在其中一个字段中将SourceNode更改为DestinationNode)。 编译器将跳过所有内容,应用程序甚至可能不会崩溃,但是有关该字段的信息将不正确。 而且,如果您基于此类字段中的信息来记录或读取数据,那么一切都会爆炸(他们会这样说,但我不想自己检查)。
- 如果将字段添加到基类,则必须更新所有记录。
- 如果您更改类中字段的名称或顺序,则必须记住在此代码脚注中更新名称和顺序。
但是最主要的是,所有这些都必须手动编写。 当涉及到如此单调的代码时,我变得非常懒惰,我寻求一种自动生成代码的方法,即使它比手工编写要花费更多的时间和精力。
Python帮了我这个忙,我在上面写了脚本来解决这些问题。 但是,我们不仅要处理模板文本,还要处理基于C ++源代码构建的文本。 我们需要一个工具来获取有关C ++代码的信息,而libclang会帮助我们。
libclang是Clang的高级C接口。 为用于在抽象语法树(AST)中解析源代码,加载已解析的AST,遍历AST,将物理源位置映射到AST中的元素以及其他Clang工具集的工具提供API。
如描述中所述,libclang提供了一个C接口,要通过Python使用C接口,您需要一个绑定库(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
是一个AST节点,每个AST节点都存储有关其在源代码中(位置)的位置的信息。 我们遍历TranslationUnit
所有标记,并打印这些标记的类型(kind属性)。
例如,采用以下C ++代码:
class X {}; class Y {}; class Z : public X {};
脚本执行结果 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
现在让我们处理AST。 在编写Python代码之前,让我们看看对clang解析器的期望。 在AST转储模式下运行clang:
clang++ -cc1 -ast-dump my_source.cpp
命令结果 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
这里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 <string>
,然后在转储中您将获得8.4万行,这显然可以解决我们的问题。
要通过命令行查看此类文件的AST转储,最好删除所有#include
。 在学习AST时将它们带回,并在感兴趣的文件中了解层次结构和类型。
在脚本中,为了仅过滤属于源文件且未通过#include
连接的AST,可以按文件添加以下过滤功能:
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)
现在,您可以提取字段。 接下来,我将给出完整的代码,该代码将生成考虑继承的字段列表,并根据模板生成文本。 这里没有专门针对c的内容,因此无可奉告。
完整的脚本代码 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)))
该脚本不考虑该类是否具有RTTI。 因此,收到结果后,您将必须手动删除描述类的块,而无需使用RTTI。 但这是一件小事。
我希望有人会有所帮助并节省时间。 所有代码都发布在GitHub上 。