如何运用 Clang 进行插件开发
Low Level Virtual Machine (LLVM) 是一个开源的编译器架构,Clang 是 LLVM 的一个编译器前端。由于优秀的设计,使得 clang 非常适合用于开发源代码级别的分析工具。本文将首先介绍 Clang 的背景知识,让读者对 Clang 的背景知识和功能特性有一个基本的了解。我们还将通过一个实际的例子,介绍如何在最新的 Clang(3.0) 发行版本上,编写一个 Clang 的插件。
Low Level Virtual Machine (LLVM) 是一个开源的编译器架构,它已经被成功应用到多个应用领域。Clang ( 发音为 /klæŋ/) 是 LLVM 的一个编译器前端,它目前支持 C, C++, Objective-C 以及 Objective-C++ 等编程语言。Clang 对源程序进行词法分析和语义分析,并将分析结果转换为 Abstract Syntax Tree ( 抽象语法树 ) ,最后使用 LLVM 作为后端代码的生成器。
Clang 的开发目标是提供一个可以替代 GCC 的前端编译器。与 GCC 相比,Clang 是一个重新设计的编译器前端,具有一系列优点,例如模块化,代码简单易懂,占用内存小以及容易扩展和重用等。由于 Clang 在设计上的优异性,使得 Clang 非常适合用于设计源代码级别的分析和转化工具。Clang 也已经被应用到一些重要的开发领域,如 Static Analysis 是一个基于 Clang 的静态代码分析工具。
本文将简单介绍 Clang 的背景知识和功能特性,并通过一个小例子介绍如何使用 Clang 的库来编写一个小程序来统计源代码中的函数。
由于 GNU 编译器套装 (GCC) 系统庞大,而且 Apple 大量使用的 Objective-C 在 GCC 中优先级较低,同时 GCC 作为一个纯粹的编译系统,与 IDE 配合并不优秀,Apple 决定从零开始写 C family 的前端,也就是基于 LLVM 的 Clang 了。Clang 由 Apple 公司开发,源代码授权使用 BSD 的开源授权。
相比于 GCC,Clang 具有如下优点:
当前 Clang 还处在不断完善过程中,相比于 GCC, Clang 在以下方面还需要加强:
回页首
在这一节,我们将介绍如何获取 Clang 源码,编译和安装 Clang。编译 Clang 要求您的系统中安装有 C++ 编译器 ( 如 GCC)。如果您还要编译 Clang 的测试集,那么您还需要事先安装 python。
由于 Clang 是 LLVM 的一部分,并且 Clang 也用到 LLVM 的库,我们需要先下载 LLVM,然后下载 Clang 作为 LLVM 工具的一部分。下面的例子示意了如何 svn 在 Linux 下获取最新的 LLVM 和 Clang。
1 .创建 LLVM 源代码存放目录 (llvm_source)
$mkdir – p llvm_source
2 .进入创建的目录
$cd llvm_source
3 .获取 LLVM
$svn co http://llvm.org/svn/llvm-project/llvm/trunk llvm
4 .获取 Clang
$cd llvm/tools $svn co http://llvm.org/svn/llvm-project/cfe/trunk clang
$cd ../../ ( 返回 llvm_source) $mkdir build ( 建立编译的工作目录 ) $cd build $../llvm/configure – prefix=$HOME/llvm ( 配置 LLVM,将目标安装目录设定为 $HOME/llvm) $make ( 以 DEBUG 模式来编译 LLVM 和 Clang)
您可以像使用普通的编译器一样使用 Clang。首先你需要把 Clang 的安装路径加入 PATH 环境变量中。以下例子假定您使用的是 Linux 的 bash:
$ export PATH=$HOME/llvm/bin:$PATH $ export LD_LIBRARY_PATH=$HOME/llvm/lib/:$LD_LIBRARY_PATH 在本文中,我们使用一个常见的 hello world 程序来演示 Clang。在这里我们把这个文件命名为 test.c。它的内容如下: #includeint main(int argc, char **argv) { printf("hello world\n"); return 0; }
您可以使用任何编辑器输入并生成这个文件。
有了这个文件以后,您可以试试以下命令行的命令:
$ clang --help ( 查看帮助信息 ) $ clang test.c -fsyntax-only ( 检查语法和词法正确性 ) $ clang test.c -S -emit-llvm -o test.bc ( 生成优化前的 llvm bitcode) $ clang test.c -S -emit-llvm -o test.bc -O3 ( 生成优化的 llvm bitcode) $ clang test.c -S -O3 -o test ( 生成可执行代码 )
与 GCC 相比,Clang 的一大优点是更加清晰明确的错误提示。
您不妨尝试着删除"printf("hello world\n");"语句后面的分号。编译这个程序,GCC 给出的错误信息将是:
test.c: In function 'main': test.c:6:2: error: expected ';' before 'return'
而 Clang 给出的错误信息则是:
test.c:5:26: error: expected ';' after expression printf("hello world.\n") ^ ;
相比之下,是不是 Clang 的错误信息更加明晰和用户友好呢?关于 Clang 出错处理更详细的信息,可以参见 http://clang.llvm.org/features.html#expressivediags。
回页首
前面我们提到过 Clang 是一个编译器前端,这也就是说:Clang 将目标程序进行分析,然后生成结构化的,树状的语法表示 , 即抽象语法树 AST(Abstract Syntax Tree)。
例如,下面简单的语句可以表示为语法树(如图 1):
while(x <= 5) { fun(x); }
将程序代码表示为抽象语法树的一个好处是能极大方便编译,分析和优化。比如,对于一个单纯的 C++ 程序来说,重命名一个变量就比较困难,我们不能简单地搜索变量名称替换成一个新的名称,因为这可能会改变太多。例如:不同的类或者名字空间里也有相同名称的变量名。将程序表示为抽象语法树以后,替换变量名称就会变得很简单:我们只需要改变对应的变量声明抽象语法树节点的名称字段,然后把抽象语法树转换回源代码,这是因为每个变量的定义都会对应一个独一无二的抽象语法树节点。
回页首
与许多编译器前端相比,Clang 的最大特点就是良好的结构化设计,每部分都是一个单独的库 (library)。也就是说您可以单独的使用其中的一部分,来编写自己的程序。在这里,我们将通过一个小例子来介绍如何使用 Clang 的库来编写一个小程序来统计源代码中的函数。通过这个例子,您将对于如何遍历抽象语法树有一个基本的概念。
我们的这个小程序将基于 Clang 的一个叫做 BoostConASTConsumer 的例子,这个例子的源代码位于:"tools/clang/lib/Frontend/BoostConAction.cpp"。
BoostConASTConsumer 的代码如下:
#include "clang/Frontend/FrontendActions.h" #include "clang/AST/ASTConsumer.h" #include "clang/AST/RecursiveASTVisitor.h" #include#include using namespace clang; namespace { class BoostConASTConsumer : public ASTConsumer, public RecursiveASTVisitor { public: /// HandleTranslationUnit - This method is called when the /// ASTsfor entire translation unit have been parsed. virtual void HandleTranslationUnit(ASTContext &Ctx); bool VisitCXXRecordDecl(CXXRecordDecl *D) { std::cout << D->getNameAsString() << std::endl; return true; } }; } ASTConsumer *BoostConAction::CreateASTConsumer (CompilerInstance &CI, llvm::StringRef InFile) { return new BoostConASTConsumer(); } void BoostConASTConsumer::HandleTranslationUnit(ASTContext &Ctx) { fprintf(stderr, "Welcome to BoostCon!\n"); TraverseDecl(Ctx.getTranslationUnitDecl()); }
BoostConASTConsumer 是一个遍历抽象语法树 (AST) 的例子。当 Clang 读入源文件,Clang 将根据源程序的信息构造一棵抽象语法树。BoostConASTConsumer 就是一个 AST Consumer,提供了访问抽象语法树的接口。
我们可以看到:BoostConASTConsumer 是 ASTConsumer 的子类,同时也是 RecursiveASTVisitor 这个 C++ 模板的声明。其中对 ASTConsumer 的继承主要是重载了 HandleTranslationUnit 这个函数,这是 BoostConASTConsumer 的入口。当整个抽象语法树 (AST) 构造完成以后,HandleTranslationUnit 这个函数将会被 Clang 的驱动程序调用。
在本小节中,我们将着重介绍 RecursiveASTVisitor,这是一个重要的函数模板。通过介绍这个模板,我们将向您简单介绍遍历抽象语法树的一些基本概念。
RecursiveASTVisitor 是一个深度优先遍历 AST 和访问节点的类。对于一个已经构造好的语法树,它将完成以下三方面的工作:
上述工作由下面三组方法完成,分别是:
上述三组方法是分层次的 (Traverse* > WalkUpFrom * > Visit*)。一个方法 ( 如 Traverse*) 可以调用同一层次的方法 ( 例如其他 Traverse*) 或低一层次的方法 ( 如 WalkUpFrom*),它不能调用更高层次的方法。这个结构确保同样类型的 AST 节点会被同时访问,也就是说不会出现交替访问不同节点的情况。
下面的伪代码,简单描述了 TraverseDecl 的工作情况。假设我们有一个 AST 节点叫做 x。在入口处,TraverseDecl 将根据 x 的类型来调用相应的访问函数。比如,如果节点类型是一个函数声明,那么将调用 TraverseFunctionDecl。在 TraverseFunctionDecl 中则会递归调用函数的内容,如首先访问函数入口参数,然后访问函数体,在访问函数体的过程中又会调用 TraverseDecl 和 TraverseStmt 访问函数体内的变量声明和每一条语句。
switch(x->type()) { case FunctionDeclType: TraverseFunctionDecl(dyn_cast(x)); break; case DeclType: TraverseDecl(dyn_cast (x)); break; ... }
明白了 BoostConASTConsumer 以后,我们就可以着手编写一个用来统计源代码中函数的例子了。我们所要做的工作其实很简单,只需要重载 VisitFunctionDecl。这样每当遇见一个函数的定义 (FunctionDecl) 的节点时,VisitFunctionDecl 将会被调用。
为了统计函数个数和信息,我们加入两个类数据成员:
int fc; // 用于统计函数个数 std::vectorfuncs; // 用于记录已经遍历的函数 然后我们在 VisitFunctionDecl 里统计函数信息: bool VisitFunctionDecl(FunctionDecl *D) { if (D->isThisDeclarationADefinition()) // 只关心提供了定义的函数 //(忽略只有声明而没有在源代 // 码中出现定义的函数) { fc++; funcs.push_back(D->getNameAsString());// 获取函数名并保存到 funcs } return true; }
由于 HandleTranslationUnit 是函数的入口,因此我们在 HandleTranslationUnit 对变量 fc 进行初始化:
fc = 0;
最后,我们在析构函数里打印统计信息:
~BoostConASTConsumer() { std::cout << "I have seen " << fc << " functions. \ They are: " <
回到编译的工作目录 (build),重新编译以及安装 Clang: make install
回到 test.c 所在的目录,输入: $clang -cc1 -boostcon test.c 我们将得到以下信息: I have seen 1 functions. They are: main
回页首
在这篇文章里,我们简单介绍了 Clang 的背景知识和功能特性,并通过一个小例子介绍了如何使用 Clang 的库来编写一个小程序来统计源代码中的函数。
阅读完本文,读者应该能够: