Clang 是 LLVM 项目提供的一个 C 语言家族的编译器前端。它最初开发的目的是替代 GNU Compiler Collection (GCC) 的 C 语言前端,目标是提供更快的编译速度、更好的诊断信息和更灵活的架构。Clang 包含一个 C、C++ 和 Objective-C 编译器前端,这些前端设计为可以嵌入到其他项目中。Clang 的一个重要特点是其模块化架构,使开发者能够更轻松地扩展和定制编译器的功能。Clang 被广泛应用于许多项目,包括 LLVM 自身、一些操作系统内核的开发以及一些编程语言的编译器实现。
除了作为编译器使用之外,Clang 还可以作为一个库提供,使开发者能够在其应用程序中利用编译器的功能,例如源代码分析和生成。Clang 可以用来获取 C++ 源文件的抽象语法树 (AST),以便进一步处理这些信息。本文将介绍如何使用 Clang 工具。
Installation & Usage #
目前,Clang 被划分为以下库和工具:libsupport、libsystem、libbasic、libast、liblex、libparse、libsema、libcodegen、librewrite、libanalysis。由于 Clang 本身是用 C++ 编写的,所以相关的接口都是 C++ 的。然而,由于 C++ 接口本身的复杂性和不稳定性(例如:在 Windows 上由 GCC 编译出来的 DLL 无法给 MSVC 使用,或者 Clang 自身版本升级导致 API 变动,从而出现不兼容性),官方并不推荐优先使用 C++ 接口。
除了 C++ 接口之外,官方还提供了一个叫做 libclang 的 C 语言接口,这个接口不仅使用起来相对简单,而且本身也比较稳定。唯一的缺点是无法获取完整的 C++ 抽象语法树 (AST),不过鉴于 C++ 完整的语法树本身就极度复杂,很多时候我们只需要其中的一小部分信息,所以这个问题通常可以忽略,除非你真的有这方面的需求。
如果你想要使用 libclang,你需要先安装 LLVM 和 Clang。在 LLVM Release 页面,有若干预发布的二进制包可以下载。如果你有定制化需求,请参考 Getting Started 页面进行手动编译。安装完成后,只需将llvm/lib
目录下的libclang.dll
链接到程序中,并包含llvm/include
目录下的clang-c/Index.h
头文件即可使用。
然而,由于 C 语言没有一些高级抽象,操作字符串都很麻烦。如果大规模使用,还需要我们自己用 C++ 封装一层。幸好,官方基于这套 C 接口还提供了一个 Python 绑定,即 clang 这个包,这使得使用起来更加方便。然而,官方提供的 Python 绑定并没有打包 libclang 的这个 DLL,因此你仍然需要在电脑上手动配置 LLVM 的环境,这可能会有些麻烦。不过,社区中有人在 PyPI 上提供了打包好的包:libclang。
于是如果你想使用 libclang 来获取 C++ 语法树,只需要
pip install libclang
什么额外的事情都不用做。本文就基于这个 python binding 的版本进行介绍。C 版本的 API 和 Python 版本的 API 基本是完全一致的,如果你觉得 Python 性能不够,你也可以参考这个教程对照着写 C 版本的代码。另外官方提供的包并没有 type hint,这样的话用 Python 写就没有代码补全,用起来也不舒服。我自己补了一个类型提示的 cindex.pyi,下载下来之后直接和 放在同一文件夹内就能有代码提示了。
Quick Start #
示例的 C++ 源文件代码如下
// main.cpp
struct Person {
int age;
const char* name;
};
int main() {
Person person = {1, "John"};
return 0;
}
解析它的 Python 代码如下
import clang.cindex as CX
def traverse(node: CX.Cursor, prefix="", is_last=True):
branch = "└──" if is_last else "├──"
text = f"{str(node.kind).removeprefix('CursorKind.')}: {node.spelling}"
if node.kind == CX.CursorKind.INTEGER_LITERAL:
value = list(node.get_tokens())[0].spelling
text = f"{text}{value}"
print(f"{prefix}{branch} {text}")
new_prefix = prefix + (" " if is_last else "│ ")
children = list(node.get_children())
for child in children:
traverse(child, new_prefix, child is children[-1])
index = CX.Index.create(excludeDecls=True)
tu = index.parse('main.cpp', args=['-std=c++20'])
traverse(tu.cursor)
输出结果如下
TRANSLATION_UNIT: main.cpp
├── STRUCT_DECL: Person
│ ├── FIELD_DECL: age
│ └── FIELD_DECL: name
└── FUNCTION_DECL: main
└── COMPOUND_STMT:
├── DECL_STMT:
│ └── VAR_DECL: person
│ ├── TYPE_REF: struct Person
│ └── INIT_LIST_EXPR:
│ ├── INTEGER_LITERAL: 1
│ └── STRING_LITERAL: "John"
└── RETURN_STMT:
└── INTEGER_LITERAL: 0
前面的是语法树节点类型,后面是节点的内容。可以发现还是非常清晰的,几乎能和源代码一一对应。
Basic Types #
注意,本文假定读者对语法树有一定的认识,不在这里做过多介绍了。如果不知道语法树是什么的话,可以看一下 为什么说 C/C++ 编译器不保留元信息。下面对 cindex 中的一些常用类型做一些介绍
Cursor #
相当于语法树的基本节点,整个语法树都是由Cursor
组成的。通过kind
属性返回一个CursorKind
类型枚举值,就代表了这个节点实际对应的类型。
for kind in CursorKind.get_all_kinds():
print(kind)
这样可以打印出所有支持的节点类型,也可以直接去源码查看。Cursor
还有一些其它的属性和方法让我们使用,常用的有如下这些:
@property
def spelling(self) -> str:
@property
def displayname(self) -> str:
@property
def mangled_name(self) -> str:
获取节点的名字,例如一个变量声明的节点,它的spelling
就是这个变量的名字。而displayname
则是节点的简短名字,大多数时候和spelling
是一样的。但是有些时候会有区别,例如一个函数的spelling
会带上参数类型,例如func(int)
,但是它的displayname
就只是func
。而mangled_name
就是该符号经过 name mangling 之后用于链接的名字。
@property
def type(self) -> Type:
节点元素的类型,例如一个变量声明的节点,它的type
就是这个变量的类型。或者一个字段声明的节点,它的type
就是这个字段的类型。返回类型为Type
。
@property
def location(self) -> SourceLocation:
节点的位置信息,返回类型为SourceLocation
,其中携带了该节点在源码中的行数,列数,文件名等信息。
@property
def extent(self) -> SourceRange:
节点的范围信息,返回类型为SourceRange
,由两个SourceLocation
组成,其中携带了该节点在源码中的起始位置和结束位置
@property
def access_specifier(self) -> AccessSpecifier:
节点的访问权限,返回类型为AccessSpecifier
。有PUBLIC
, PROTECTED
, PRIVATE
, NONE
, INVALID
五种。
def get_children(self) -> iterable[Cursor]:
获取所有子节点,返回类型为Cursor
的iterable
。这个函数是最常用的,因为我们可以通过递归的方式遍历整个语法树。
def get_tokens(self) -> iterable[Token]:
获取代表该节点的所有token
,返回类型为Token
的iterable
。token
是语法树的最小单位,例如一个变量声明的节点,它的token
就是int
,a
,;
这三个。这个函数可以用来获取一些细节信息,例如获取整数字面量和浮点数字面量的值。
def is_definition(self) -> bool:
def is_const_method(self) -> bool:
def is_converting_constructor(self) -> bool:
def is_copy_constructor(self) -> bool:
def is_default_constructor(self) -> bool:
def is_move_constructor(self) -> bool:
def is_default_method(self) -> bool:
def is_deleted_method(self) -> bool:
def is_copy_assignment_operator_method(self) -> bool:
def is_move_assignment_operator_method(self) -> bool:
def is_mutable_field(self) -> bool:
def is_pure_virtual_method(self) -> bool:
def is_static_method(self) -> bool:
def is_virtual_method(self) -> bool:
def is_abstract_record(self) -> bool:
def is_scoped_enum(self) -> bool:
这些函数基本就见名知意了,例如is_definition
就是判断该节点是否是一个定义,is_const_method
就是判断该节点是否是一个const
方法。
Type #
如果该节点有类型的话,代表该节点的类型。常用的属性有
@property
def kind(self) -> TypeKind:
类型的类型,返回类型为TypeKind
。例如INT
, FLOAT
, POINTER
, FUNCTIONPROTO
等等。
@property
def spelling(self) -> str:
类型的名字,例如int
, float
, void
等等。
def get_align(self) -> int:
def get_size(self) -> int:
def get_offset(self, fieldname: str) -> int:
获取类型的对齐,大小,字段偏移量等等。
以及一些is
开头的函数,例如is_const_qualified
, is_function_variadic
, is_pod
等等。这里也就不多说了。
TranslationUnit #
一般来说一个 C++ 源文件就代表一个TranslationUnit
,也就是我们常说的编译单元。
常用的有
@property
def cursor(self) -> Cursor:
获取该TranslationUnit
的根节点,也就是TRANSLATION_UNIT
类型的Cursor
。
@property
def spelling(self) -> str:
获取该TranslationUnit
的文件名。
def get_includes(self, depth: int = -1) -> iterable[FileInclusion]:
获取该TranslationUnit
的所有include
,返回类型为FileInclusion
的list
,注意由于include
的文件里面可能还会包含别的文件所以,可以用depth
这个参数来限制,比如我只想获取第一层也就是直接包含的头文件可以这么写。
index = CX.Index.create()
tu = index.parse('main.cpp', args=['-std=c++20'])
for file in tu.get_includes():
if file.depth == 1:
print(file.include.name)
这样就会打印出所有直接使用的头文件了。
Index #
一个Index
就是一个TranslationUnit
的集合,并且最终被链接到一起,形成一个可执行文件或者库。
有一个静态方法create
用于创建一个新的Index
,然后成员方法parse
可以解析一个C++
源文件,返回一个TranslationUnit
。
def parse(self, path: str,
args: list[str] | None = ...,
unsaved_files: list[tuple[str, str]] | None = ...,
options: int = ...) -> TranslationUnit:
path
是源文件路径,args
是编译参数,unsaved_files
是未保存的文件,options
是一些定义在TranslationUnit.PARSE_XXX
中的参数,例如PARSE_SKIP_FUNCTION_BODIES
和PARSE_INCOMPLETE
。可以用来定制化解析过程,加快解析速度,或者保留宏信息等。
Examples #
Namespace #
由于 clang 在解析的时候会把所有的头文件都展开,全部输出内容太多了。但是我们主要可能只是想要我们自己代码的信息,这时候就可以利用命名空间进行筛选了。示例如下:
#include <iostream>
namespace local {
struct Person {
int age;
std::string name;
};
}
解析代码如下
import clang.cindex as CX
def traverse_my(node: CX.Cursor):
if node.kind == CX.CursorKind.NAMESPACE:
if node.spelling == "local":
traverse(node) # forward to the previous function
for child in node.get_children():
traverse_my(child)
index = CX.Index.create()
tu = index.parse('main.cpp', args=['-std=c++20'])
traverse_my(tu.cursor)
写一个函数对类型空间名进行筛选,然后转发到我们之前那个函数就行,这样就只会输出我们想要的的命名空间里面的内容了。
Class & Struct #
我们主要是获取它们里面的字段名,类型,方法名,类型等,示例如下:
struct Person {
int age;
const char* name;
void say_hello(int a, char b);
};
解析代码如下
def traverse_class(node: CX.Cursor):
match node.kind:
case CX.CursorKind.STRUCT_DECL | CX.CursorKind.CLASS_DECL:
print(f"Class: {node.spelling}:")
case CX.CursorKind.FIELD_DECL:
print(f" Field: {node.spelling}: {node.type.spelling}")
case CX.CursorKind.CXX_METHOD:
print(f" Method: {node.spelling}: {node.type.spelling}")
for arg in node.get_arguments():
print(f" Param: {arg.spelling}: {arg.type.spelling}")
for child in node.get_children():
traverse_class(child)
# Class: Person:
# Field: age: int
# Field: name: const char *
# Method: say_hello: void (int, char)
# Param: a: int
# Param: b: char
Comment #
可以获取 Doxygen 风格的注释
@property
def brief_comment(self) -> str:
@property
def raw_comment(self) -> str:
brief_comment
获取@brief
后面的内容,raw_comment
获取整个注释的内容。
/**
* @brief func description
* @param param1
* @return int
*/
int func(int param1){
return param1 + 10000000;
}
解析代码如下
def traverse_comment(node: CX.Cursor):
if node.brief_comment:
print(f"brief_comment => {node.brief_comment}")
if node.raw_comment:
print(f"raw_comment => {node.raw_comment}")
for child in node.get_children():
traverse_comment(child)
# brief_comment => func description
# raw_comment => /**
# * @brief func description
# * @param param1
# * @return int
# */
Enum #
获取枚举名以及对应的枚举常量值,还有它的底层类型
enum class Color{
RED = 0,
GREEN,
BLUE
};
解析代码如下
def traverse_enum(node: CX.Cursor):
if node.kind == CX.CursorKind.ENUM_DECL:
print(f"enum: {node.spelling}, underlying type: {node.enum_type.spelling}")
print(f"is scoped?: {node.is_scoped_enum()}")
for child in node.get_children():
print(f" enum_value: {child.spelling}: {child.enum_value}")
for child in node.get_children():
traverse_enum(child)
# enum: Color, underlying type: int
# is scoped?: True
# enum_value: RED: 0
# enum_value: GREEN: 1
# enum_value: BLUE: 2
Attribute #
C++11 加入了新的 attribute 语法:[[ ... ]]
,可以用来给函数或者变量添加额外的信息。例如[[nodiscard]]
和[[deprecated]]
。但是我们有时候在自己定义一些标记来给我们的与预处理工具使用,比如标记一个类型需要不需要生成元信息,我们也希望这些标记也能被 libclang 识别出来。但是遗憾的是如果直接写不被标准支持的属性会被 libclang 忽略,也就是最终的 AST 中是没有它的
struct [[Reflect]] Person{}; // ignored
一个可行的解决办法是利用get_tokens
获取声明中的所有token
,然后自己裁剪出来。比如这里获取到的结果就是struct
,[
,[
,Reflect
,]
,]
,Person
,{
,}
,我们可以从中获取出我们想要的信息。
但是 clang 给我们提供了一种更好的办法。那就是利用clang::annotate(...)
这个 clang 的扩展属性,例如像下面这样
#define Reflect clang::annotate("reflect")
struct [[Reflect]] A {};
这样对于A
这个Cursor
来说,它的子节点中就会有一个ANNOTATE_ATTR
的类型的Cursor
,而spelling
就是里面存的信息,这里就是reflect
。这样我们就可以很方便的获取到我们自定义的属性了。而且 C++ 标准规定了,当编译器遇到一个不认识的 attribute 的时候,它会忽略这个 attribute,而不是报错。这样的话,这个属性它就只作用于我们的预处理器,不会影响到正常编译。
Macro #
clang 在实际解析语法树之前,会把所有的预处理指令都替换成实际的代码。所以最后的语法树信息中就没有它们了。但是有些时候我们的确想要获取到这些信息,比如我们想要获取到#define
的信息,这里需要把parse
的options
参数设为TranslationUnit.PARSE_DETAILED_PROCESSING_RECORD
。如果想要获取宏的内容就用get_tokens
就行了。
#define CONCAT(a, b) a#b
auto x = CONCAT(1, 2);
解析代码如下
def traverse_macro(node: CX.Cursor):
if node.kind == CX.CursorKind.MACRO_DEFINITION:
if not node.spelling.startswith('_'): # Exclude internal macros
print(f"MACRO: {node.spelling}")
print([token.spelling for token in node.get_tokens()])
elif node.kind == CX.CursorKind.MACRO_INSTANTIATION:
print(f"MACRO_INSTANTIATION: {node.spelling}")
print([token.spelling for token in node.get_tokens()])
for child in node.get_children():
traverse_macro(child)
# MACRO: CONCAT
# ['CONCAT', '(', 'a', ',', 'b', ')', 'a', '#', 'b']
# MACRO_INSTANTIATION: CONCAT
# ['CONCAT', '(', '1', ',', '2', ')']
Rewrite #
有时候我们希望对源代码进行一些简单的修改,在某个位置插入一段代码或者删除一段代码。这时候我们可以使用Rewriter
这个类。示例如下:
void func(){
int a = 1;
int b = 2;
int c = 3;
}
使用下面的代码对源文件进行修改
def rewrite(node: CX.Cursor, rewriter: CX.Rewriter):
if node.kind == CX.CursorKind.VAR_DECL:
if node.spelling == "a":
rewriter.replace_text(node.extent, "int a = 100")
elif node.spelling == "b":
rewriter.remove_text(node.extent)
elif node.spelling == "c":
rewriter.insert_text_before(node.extent.start, "[[maybe_unused]]")
for child in node.get_children():
rewrite(child, rewriter)
index = CX.Index.create()
tu = index.parse('main.cpp', args=['-std=c++20'])
rewriter = CX.Rewriter.create(tu)
rewrite(tu.cursor, rewriter)
rewriter.overwrite_changed_files()
运行之后,main.cpp
的内容就变成了
void func() {
int a = 100;
;
[[maybe_unused]] int c = 3;
}
Conclusion #
如果要获取类型的size
, align
, offset
等 ABI 相关的内容,需要注意 platform。不同 ABI 的情况下它们的值可能不同,例如 MSVC 和 GCC 一般关于这些内容就不同,可以通过在编译参数中指定-target
来指定目标平台。如果需要和 MSVC 一致的结果,可以使用--target=x86_64-pc-windows-msvc
。如果是 GCC 的话,可以使用--target=x86_64-pc-linux-gnu
。
前文提到,libclang 无法提供完整的 C++ 语法树。例如,它在解析 Expr
方面缺少许多接口。这意味着,如果你需要解析具体的表达式内容,那么使用其 C++ 接口可能更为适合,因为它提供了完整且复杂的语法树。
国内关于 Clang 工具具体使用的文章较少。本文尝试对一些常用功能进行了具体介绍,尽管并不十分完善。若有任何疑问,可直接阅读 Index.h
源码,其中的注释非常详尽。或者也可以在评论区留言,我会尽力解答。此外,若需要获取 libclang 不提供的信息,可使用 get_tokens
函数自行获取。例如,libclang 不支持获取整数和浮点数面值的值,这时可通过 get_tokens
手动获取。
在从语法树中提取这些信息后,你可以进一步处理它们,如生成元信息或直接生成代码等。当然,这些都是后话,具体取决于你的需求。
本文到这里就结束了,这是反射系列中的其中一篇,欢迎阅读系列中的其它文章!