Clang的API发展十分迅速,其中也包括libclang和Python绑定。因此,本次推送中的示例可能不再起作用。如果想要那些基于最新的Clang的工作示例,请检查我在Github上的llvm-clang-samples repository仓库。
对于那些需要在Python中去解析和分析C代码的人,在发现pycparser后一定会很兴奋。然而,如果变成是去解析C++,pycparser并不是好的解决办法。当我被问及关于在pycparser中增加支持C++的计划时,我的回答通常是 -- 没有这样的计划,你应该去别处找找。尤其地,是在Clang中。
Clang是一款针对C,C++和Object C的编译器前端。它是由Apple支持的一款具有自由协议的开源项目,并使用它们自己的工具。连同它的父项目 -- LLVM编译器后端,Clang渐渐成为一款gcc的强大替代品。在Clang(包括LLVM)身后有着一流的开发团队,并且它的代码在开源环境下也是C++代码中设计最好的之一。Clang的发展十分积极,密切关注着最新的C++标准。
于是当我被问关于解析C++的问题的时候,我的回答总是Clang。诚然,它还存在着一些小问题。人们喜欢pycparser因为它是Python实现的,而Clang的API接口是C++,并不是最极客并友好的语言,退一步来说。
libclang
下面进入libclang。不太久之前,Clang的团队明智地意识到Clang并不仅仅可以用于编译器,也可以是分析C/C++/ObjC代码的工具。事实上,Apple的自研Xcode开发工具就是用Clang作为内置库来进行代码提示,交叉引用,等等。
能使Clang如此被利用的组件我们称之为libclang。它是C API编写的,并且Clang团队郑重声明保证其稳定性,允许使用者在抽象语法树(AST)的等级上去检查解析代码。
更多的技术上来讲,libclang是一个通过使用面向公众的API来对Clang进行包装的共享库,并只定义在一份C语言头文件中:clang/include/clang-c/Index.h 。
Python绑定到libclang
libclang于 clang/bindings/python 中附带了Python绑定,位于 clang.cindex 模块中。这个模块依赖于 ctypes 去装载动态的libclang库,并试图尽可能地去封装libclang作为Python API接口。
文档?
非常不幸,目前libclang的相关文档以及Python绑定的状态十分糟糕。官方文档会根据源代码而进行开发(自动生成Doxygen HTML)。此外,我所能找到的所有在线文档只有一份演示和一些来自Clang开发邮件列表中过时的邮件信息。
好的一方面,即使你只浏览了一遍 Index.h 头文件也能记住它试图要实现什么,它的API并不难懂(具体实现也是一样易懂,哪怕你对Clang的内部只熟悉一点)。另一个需要看的地方是 clang/tools/c-index-test 工具,它是用来测试API接口并示例它们该如何使用。
对于Python绑定,也完全没有任何文档,除了它发布的源代码和一些相关示例。因此,我希望这篇文章能够起到帮助!
设置
设置使用Python绑定很简单:
你的脚本需要能够找到 clang.cindex 模块。所以要适当地复制它或建立 PYTHONPATH 来指向它。
clang.cindex 需要能够找到 libclang.so 共享库。取决于你如何构建/安装Clang,你需要适当地拷贝它或是建立 LD_LIBRARY_PATH 来指向它的位置。在Windows操作系统,则是 libclang.dll 并且需要设置到 PATH 环境变量中去。
准备就绪后,要去调用 import clang.cindex 并开始使用。
简单示例
让我们开始一段简单的示例。下面的脚本使用 libclang 的Python绑定来找到一个给定文件中的所有引用类型:
#!/usr/bin/env python
""" Usage: call with <filename> <typename>
"""
import sys
import clang.cindex
def find_typerefs(node, typename):
""" Find all references to the type named 'typename'
"""
if node.kind.is_reference():
ref_node = clang.cindex.Cursor_ref(node)
if ref_node.spelling == typename:
print 'Found %s [line=%s, col=%s]' % (
typename, node.location.line, node.location.column)
# Recurse for children of this node
for c in node.get_children():
find_typerefs(c, typename)
index = clang.cindex.Index.create()
tu = index.parse(sys.argv[1])
print 'Translation unit:', tu.spelling
find_typerefs(tu.cursor, sys.argv[2])
假设我们在下面这段C++代码中调用它:
class Person {
};
class Room {
public:
void add_person(Person person)
{
// do stuff
}
private:
Person* people_in_room;
};
template <class T, int N>
class Bag<T, N> {
};
int main()
{
Person* p = new Person();
Bag<Person, 42> bagofpersons;
return 0;
}
执行去查找 Persion 引用类型,我们会得到:
Translation unit: simple_demo_src.cpp
Found Person [line=7, col=21]
Found Person [line=13, col=5]
Found Person [line=24, col=5]
Found Person [line=24, col=21]
Found Person [line=25, col=9]
理解它是如何工作的
为了理解这个例子做了什么,我们需要明白它内部运作的3个层次:
- 概念层次 -- 我们从要被解析的源代码获取的信息是什么,并且它是如何储存的;
- libclang层次 -- libclang 的正式C API,要比Python绑定代码更容易阅读,虽然只在代码中有一些备注;
- Python绑定层次,这是我们直接调用的。
创建索引并解析代码
我们需要在最开始时添加如下代码:
index = clang.cindex.Index.create()
tu = index.parse(sys.argv[1])
一个"index"代表一组编译和链接在一起的翻译单元。我们需要一些分组翻译单元的方法,如果试图理解它们的话。举例来说,我们可能想要找到一些定义在头文件中的引用类型,以及在一些其他的源文件中。 Index.create() 调用C API函数 clang_createIndex。
接下来,我们使用 Index's parse 方法来解析一个文件中单独的翻译单元。调用 clang_parseTranslationUnit,一个在C API中的关键函数。它的注释提到:
这个程序是Clang C API中的主要入口,提供将一份源文件解析成一个翻译单元的功能,并且能查询API中其他的函数。
这是一个强大的函数 -- 它能可选择性地正常接受全套标识并传到命令行编译器。它返回一个封装了的 CXTranslationUnit 对象,作为一个封装了Python绑定的翻译单元。这个翻译单元能够被查询,例如翻译单元的名字可以通过 spelling 属性表示:
print 'Translation unit:', tu.spelling
然而,它最重要的属性是 -- cursor。一个 cursor 是libclang中的关键抽象,它表示一个被解析翻译单元的抽象语法树中的一些节点。在一个单一抽象下的程序中,cursor整合不同类型的实体,提供一组常见的操作,如获取它的位置和子cursor。 TranslationUnit.cursor 返回一个翻译单元最高层级的cursor,作为索引它的抽象语法树的声明点。
使用curosrs进行工作
Python绑定将 libclang 中的cursor封装成 Cursor 对象。它有许多属性,其中最有趣的包括:
- kind -- 一个枚举指定了这个curosr指向的抽象语法树节点的种类;
- spelling -- 节点的源代码名称
- location -- 被解析节点的源代码位置
- get_children -- 它的子节点
get_children 需要专门说明一下,因为这是一个在C和Python的API接口分歧上的特殊点。
libclang C API 基于访问者想法。根据给定的cursor走到抽象语法树,用户代码提供了一个针对 clang_visitChildren 的回调函数。然后在一个给定的抽象语法树的所有子节点上来调用这个函数。
而另一方面的Python绑定,会封装在内部访问,通过 Cursor.get_children 提供一个Python化的迭代API,返回一个指定cursor的子节点。它仍然可以通过Python来直接访问最原始的API接口,但是使用 get_children 更加地方便。在我们的例子中,我们使用 get_children 来递归访问一个节点的所有子节点:
for c in node.get_children():
find_typerefs(c, typename)
Python绑定的一些局限性
不幸的是,Python绑定还不够成熟并存在一些缺陷,因为它是一项正在进行中的工作。举一个例子,假设我们想要找到和记录这个文件中的所有函数调用:
bool foo()
{
return true;
}
void bar()
{
foo();
for (int i = 0; i < 10; ++i)
foo();
}
int main()
{
bar();
if (foo())
bar();
}
让我们写下这段代码:
import sys
import clang.cindex
def callexpr_visitor(node, parent, userdata):
if node.kind == clang.cindex.CursorKind.CALL_EXPR:
print 'Found %s [line=%s, col=%s]' % (
node.spelling, node.location.line, node.location.column)
return 2 # means continue visiting recursively
index = clang.cindex.Index.create()
tu = index.parse(sys.argv[1])
clang.cindex.Cursor_visit(
tu.cursor,
clang.cindex.Cursor_visit_callback(callexpr_visitor),
None)
这次直接使用 libclang 的访问API,结果如下:
Found None [line=8, col=5]
Found None [line=10, col=9]
Found None [line=15, col=5]
Found None [line=16, col=9]
Found None [line=17, col=9]
既然被记录的位置是正确的,那为什么节点的名字会是 Node 呢?仔细研究过libclang的代码后发现,我们不应该来打印 spelling ,而应该是 display 。在C API中它意味着 clang_getCursorDisplayName 而不是 clang_getCursorSpelling 。但是,Python绑定并没有暴露 clang_getCursorDisplayName 接口!
然而,我们不应该就此放弃。Python绑定的相关源代码十分地直截了当,并且很容易通过 ctypes 来在C API中开放额外的函数。只要在 bindings/python/clang/cindex.py 中添加如下几行:
Cursor_displayname = lib.clang_getCursorDisplayName
Cursor_displayname.argtypes = [Cursor]
Cursor_displayname.restype = _CXString
Cursor_displayname.errcheck = _CXString.from_result
现在我们可以使用 Cursor_displayname,在脚本中通过 clang.cindex.Cursor_displayname(node) 来代替 node.spelling 。这样可以得到我们想要的输出结果:
Found foo [line=8, col=5]
Found foo [line=10, col=9]
Found bar [line=15, col=5]
Found foo [line=16, col=9]
Found bar [line=17, col=9]
更新提示(06.07.2011):来自这篇文章的灵感,我向Clang项目中提交了关于开放 Cursor_displayname 接口的代码,也修复了一些在Python绑定中的其他问题。它已经在Clang的核心开发版本134460中被提交,并且现在应该在主干trunk上可用。
libclang的一些局限性
综上所述,Python绑定的一些局限性相对容易客服。自从 libclang 提供了一个简单易行的C API,这仅仅就是适当使用 ctypes 结构来开放暴露附加函数的问题而已。对于任何有些Python经验的人来说,这都不是一个大问题。
然而一些 libclang 本身的局限性,例如,假设我们想要找到一段代码中的所有返回语句,结果发现通过当前 libclang 的API是办不到的。大致浏览 Index.h 就会发现其原因。
enum CXCursorKind 枚举了我们在 libclang 中可能遇到的cursor节点的种类。下面是这部分相关的代码:
/* Statements */
CXCursor_FirstStmt = 200,
/**
* \brief A statement whose specific kind is not exposed via this
* interface.
*
* Unexposed statements have the same operations as any other kind of
* statement; one can extract their location information, spelling,
* children, etc. However, the specific kind of the statement is not
* reported.
*/
CXCursor_UnexposedStmt = 200,
/** \brief A labelled statement in a function.
*
* This cursor kind is used to describe the "start_over:" label statement in
* the following example:
*
* \code
* start_over:
* ++counter;
* \endcode
*
*/
CXCursor_LabelStmt = 201,
CXCursor_LastStmt = CXCursor_LabelStmt,
忽略用于正确性测试的占位符 CXCursor_FirstStmt 和 CXCursor_LastStmt ,这唯一需要注意的是lable语句。所有其他的声明语句将会被 CXCursor_UnexposedStmt 所替代。
去理解这个缺陷的原理,有建设性的思考是 libclang 的主要目标。目前,API主要用于集成开发环境当中,我们想要知道一切类型和引用符号,但不用明确关心我们看到的声明或是语句的种类。
值得庆幸的是,从Clang的邮件开发列表讨论组中可以收集到这种局限性并非蓄意而为之。在 libclang 中根据需要添加的东西,显然没有一个需要 libclang 来辨别声明种类的不同,因此没有人增加这个特性。如果它对某人来说足够重要,他可以随时在邮件列表中提出一个修补补丁。特别是,这个具体的缺陷(缺少声明种类)是很容易被解决的。看看在 libclang/CXCursor.cpp 中的 cxcursor::MakeCXCursor ,很明显找到这些"种类"是如何生成的:
CXCursor cxcursor::MakeCXCursor(Stmt *S, Decl *Parent,
CXTranslationUnit TU) {
assert(S && TU && "Invalid arguments!");
CXCursorKind K = CXCursor_NotImplemented;
switch (S->getStmtClass()) {
case Stmt::NoStmtClass:
break;
case Stmt::NullStmtClass:
case Stmt::CompoundStmtClass:
case Stmt::CaseStmtClass:
... // many other statement classes
case Stmt::MaterializeTemporaryExprClass:
K = CXCursor_UnexposedStmt;
break;
case Stmt::LabelStmtClass:
K = CXCursor_LabelStmt;
break;
case Stmt::PredefinedExprClass:
.. // many other statement classes
case Stmt::AsTypeExprClass:
K = CXCursor_UnexposedExpr;
break;
.. // more code
}
这是在 Stmt.getStmtClass() 中的一个简单而巨大的switch语句,并且仅对 Stmt::LabelStmtClass 有一种非 CXCursor_UnexposedStmt 类型。所以建议添加的"类型"不要是特别重要的:
- 向 CXCursorKind 添加另一个枚举值,在 CXCursor_FirstStmt 和 CXCurosr_LastStmt 之间;
- 在 cxcursor::MakeCXCursor 中的switch语句中添加另一份条件case以识别适合的类和返回值种类;
- 在Python绑定中暴露相关枚举值。
结论
希望这篇文章介绍 libclang 的Python绑定的文章能够起到帮助。尽管这些组件缺乏外部文档,但它们被很好的编写和注释,并且源代码足够的浅显易懂。
记住这些积极发展、并包装在十分强大的C/C++/ObjC的解析引擎里的API,显得十分重要。仅代表个人观点,Clang是当下最新的C++解析库中最好的选择,没有之一。
在 libclang 本身以及Python绑定中存在着一些美中不足的小限制。这些都是最近相对除了Clang之外, libclang 的附带结果,毕竟它还是一个非常年轻的项目。
幸运的是,希望我这篇文章能够告诉你们这些限制并非极度难以解决的。仅需要少量Python和C的专业知识,就能够扩展Python绑定,并且一点对Clang的理解也可以增强奠定 libclang 本身。另外, libclang 仍然在积极地发展当中。我十分确信这些API将会随着时间的推移而持续改进,并且限制也会越来越少。