背景

libclang是llvm的一个工具,可以使用其提供的接口分析C++代码,并得到源码中期望的数据。

在之前的一个项目中,需要分析获取每个日志宏的字符串信息以及该条日志宏所在的函数。比如下面的例子,需要获取到this is a log这个字符串,以及它所在的函数Test::test。字符串可以通过正则表达式匹配到,但日志宏所在的函数Test::test就很难分析出来,因此需要借助libclang来获取到这些信息。

1
2
3
4
5
namespace Test{
    int test(){
        LINFO("this is a log");
    }
}

使用

libclang实际是个dll,在安装完llvm后,可以在其bin目录下拿到。libclang提供了多种语言的binding,简单起见,使用了python的binding。直接使用命令pip install clang即可安装python的binding,然后将libclang.dll的路径加入到系统环境变量后,便可使用python分析C++代码了。

下面是一个简单的例子。

1
2
3
4
5
6
// src.cpp
#include <stdio.h>
int main() {
    printf("Hello World\n");
    return 0;
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import clang.cindex
def traverse(node, depth):
    print("%s%s %s" %("|   " * depth, str(node.kind), node.spelling))
    for n in node.get_children():
        traverse(n, depth + 1)

index = clang.cindex.Index.create()
parser = index.parse("src.cpp")
cursor = parser.cursor
traverse(cursor, 0)

执行完上面这个例子后会输出解析后的整个语法树,整个语法树非常大,从下图中可以看到真正的main函数只占一小部分。

源码的所有信息都在这颗语法树上,语法树是个多叉树,我们按照树的遍历算法遍历整棵树便能拿到我们想要的任何信息。

在上面的程序中,只输出了每个节点kind值和spelling值,kind值代表当前节点的类型,比如main函数节点的类型是FUNCTION_DECL,而调用printf函数时,对应节点的类型是CALL_EXPRspelling翻译过来就是拼写,可以理解为当前节点的名称,在上图中可以分别看到main函数节点的拼写是main,调用的printf函数的拼写是printf

更多的详细信息可以在llvm的官方文档中查看,目前我们只需要了解kindspelling这两个信息即可实现当前的需求。

注意事项

在使用部分,仅举了一个比较简单的例子进行说明,在对一个比较大的工程分析时,会遇到几个问题。

  1. 工程选项中会配置附加包含目录,在使用libclang编译代码时,如果没有指定这些附加目录,那么libclang将会编译错误,编译错误后会导致语法树有缺失。
  2. 工程选项中会配置一些预定义宏,使用libclang时如果没有指定,那么也会编译错误,进而导致语法树缺失。
  3. libclang分为32位和64位的版本,32位的用来编译32位的代码,64位的编译64位的代码,不可混用。
  4. libclang只支持utf8编码的源码文件,在MSVC中一般使用GBK,同时还会在GBK中使用中文字符串,这会导致libclang编译错误,建议全部使用英文字符串。注意,不影响注释中的中文
  5. MSVC的语法与clang的语法有些不兼容,某些代码MSVC可以编译通过,但clang却不行,这需要修改代码。

针对上面的前两个问题,在调用index.parse方法时,可以将附加包含目录和预定义宏作为参数的形式传递进去。一个例子如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
index = clang.cindex.Index.create()
# 指定编译参数
compile_args = ['-I3rdParty', 
                '-D_DEBUG', 
                '-D_CONSOLE', 
                '-D_UNICODE', 
                '-DUNICODE', 
                '-IC:\\Program Files (x86)\\Microsoft Visual Studio 14.0\\\\VC\\include\\', 
                '-IC:\\Program Files (x86)\\Microsoft Visual Studio 14.0\\\\VC\\atlmfc\\include\\']

parser = index.parse("src.cpp", args = compile_args)
cursor = parser.cursor
# 实现同上
traverse(cursor, 0)

使用-I参数指定附加包含路径,使用-D参数指定预定义宏,然后作为一个列表传递给parse函数。

如果编译有错,可以通过下面这种方式获取到对应的错误信息。

1
2
3
4
5
6
index = clang.cindex.Index.create()
parser = index.parse(self.src_path, args = self.compile_args)

for i in parser.diagnostics:
    if i.severity > 2: # error级别
         print(i)

参考资料

  1. 了解 Clang AST
  2. Clang之语法抽象语法树AST