结构化编译器前端 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 的库来编写一个小程序来统计源代码中的函数。

Clang 的开发背景

由于 GNU 编译器套装 (GCC) 系统庞大,而且 Apple 大量使用的 Objective-C 在 GCC 中优先级较低,同时 GCC 作为一个纯粹的编译系统,与 IDE 配合并不优秀,Apple 决定从零开始写 C family 的前端,也就是基于 LLVM 的 Clang 了。Clang 由 Apple 公司开发,源代码授权使用 BSD 的开源授权。

Clang 的特性

相比于 GCC,Clang 具有如下优点:

  • 编译速度快:在某些平台上,Clang 的编译速度显著的快过 GCC。
  • 占用内存小:Clang 生成的 AST 所占用的内存是 GCC 的五分之一左右。
  • 模块化设计:Clang 采用基于库的模块化设计,易于 IDE 集成及其他用途的重用。
  • 诊断信息可读性强:在编译过程中,Clang 创建并保留了大量详细的元数据 (metadata),有利于调试和错误报告。
  • 设计清晰简单,容易理解,易于扩展增强。与代码基础古老的 GCC 相比,学习曲线平缓。

当前 Clang 还处在不断完善过程中,相比于 GCC, Clang 在以下方面还需要加强:

  • 支持更多语言:GCC 除了支持 C/C++/Objective-C, 还支持 Fortran/Pascal/Java/Ada/Go 和其他语言。Clang 目前支持的语言有 C/C++/Objective-C/Objective-C++。
  • 加强对 C++ 的支持:Clang 对 C++ 的支持依然落后于 GCC,Clang 还需要加强对 C++ 提供全方位支持。
  • 支持更多平台:GCC 流行的时间比较长,已经被广泛使用,对各种平台的支持也很完备。Clang 目前支持的平台有 Linux/Windows/Mac OS。

Clang 安装

在这一节,我们将介绍如何获取 Clang 源码,编译和安装 Clang。编译 Clang 要求您的系统中安装有 C++ 编译器 ( 如 GCC)。如果您还要编译 Clang 的测试集,那么您还需要事先安装 python。

获取源码

由于 Clang 是 LLVM 的一部分,并且 Clang 也用到 LLVM 的库,我们需要先下载 LLVM,然后下载 Clang 作为 LLVM 工具的一部分。下面的例子示意了如何 svn 在 Linux 下获取最新的 LLVM 和 Clang。

1 .创建 LLVM 源代码存放目录 (llvm_source)

2 .进入创建的目录

3 .获取 LLVM

4 .获取 Clang

编译 Clang

开始使用 Clang

您可以像使用普通的编译器一样使用 Clang。首先你需要把 Clang 的安装路径加入 PATH 环境变量中。以下例子假定您使用的是 Linux 的 bash:

您可以使用任何编辑器输入并生成这个文件。

有了这个文件以后,您可以试试以下命令行的命令:

与 GCC 相比,Clang 的一大优点是更加清晰明确的错误提示。

您不妨尝试着删除”printf(“hello world\n”);”语句后面的分号。编译这个程序,GCC 给出的错误信息将是:

而 Clang 给出的错误信息则是:

相比之下,是不是 Clang 的错误信息更加明晰和用户友好呢?关于 Clang 出错处理更详细的信息,可以参见http://clang.llvm.org/features.html#expressivediags

抽象语法树

前面我们提到过 Clang 是一个编译器前端,这也就是说:Clang 将目标程序进行分析,然后生成结构化的,树状的语法表示 , 即抽象语法树 AST(Abstract Syntax Tree)。

例如,下面简单的语句可以表示为语法树(如图 1):

图 1. 抽象语法树

imadvegfewge001

将程序代码表示为抽象语法树的一个好处是能极大方便编译,分析和优化。比如,对于一个单纯的 C++ 程序来说,重命名一个变量就比较困难,我们不能简单地搜索变量名称替换成一个新的名称,因为这可能会改变太多。例如:不同的类或者名字空间里也有相同名称的变量名。将程序表示为抽象语法树以后,替换变量名称就会变得很简单:我们只需要改变对应的变量声明抽象语法树节点的名称字段,然后把抽象语法树转换回源代码,这是因为每个变量的定义都会对应一个独一无二的抽象语法树节点。

基于 Clang 的编程

与许多编译器前端相比,Clang 的最大特点就是良好的结构化设计,每部分都是一个单独的库 (library)。也就是说您可以单独的使用其中的一部分,来编写自己的程序。在这里,我们将通过一个小例子来介绍如何使用 Clang 的库来编写一个小程序来统计源代码中的函数。通过这个例子,您将对于如何遍历抽象语法树有一个基本的概念。

BoostConASTConsumer

我们的这个小程序将基于 Clang 的一个叫做 BoostConASTConsumer 的例子,这个例子的源代码位于:”tools/clang/lib/Frontend/BoostConAction.cpp”。

BoostConASTConsumer 的代码如下:

BoostConASTConsumer 是一个遍历抽象语法树 (AST) 的例子。当 Clang 读入源文件,Clang 将根据源程序的信息构造一棵抽象语法树。BoostConASTConsumer 就是一个 AST Consumer,提供了访问抽象语法树的接口。

我们可以看到:BoostConASTConsumer 是 ASTConsumer 的子类,同时也是 RecursiveASTVisitor 这个 C++ 模板的声明。其中对 ASTConsumer 的继承主要是重载了 HandleTranslationUnit 这个函数,这是 BoostConASTConsumer 的入口。当整个抽象语法树 (AST) 构造完成以后,HandleTranslationUnit 这个函数将会被 Clang 的驱动程序调用。

在本小节中,我们将着重介绍 RecursiveASTVisitor,这是一个重要的函数模板。通过介绍这个模板,我们将向您简单介绍遍历抽象语法树的一些基本概念。

RecursiveASTVisitor 是一个深度优先遍历 AST 和访问节点的类。对于一个已经构造好的语法树,它将完成以下三方面的工作:

  1. 遍历 AST 的每个节点;
  2. 在某一个节点,访问这个节点的层次结构 ( 每个节点也是一个树 );
  3. 如果某一个节点是一种类型的动态类别 ( 比如是一个子类等 ),调用一个用户重载的函数来访问这个节点;

上述工作由下面三组方法完成,分别是:

  1. TraverseDecl(Decl *x) 完成工作 1,它是遍历 AST 的入口。这个方法是用来访问有关变量和函数的声明。TraverseDecl 只是简单的根据节点的类型来调用相应的 TraverseFoo(Foo *x),然后递归访问 x 的子节点。TraverseStmt(Stmt *x) 和 TraverseType(QualType x) 则是用来访问一条语句和一个类型的(如结构体),它们的工作方式和 TraverseDecl 类似。
  2. WalkUpFromFoo(Foo *x) 完成工作 2。它不会尝试访问 x 的任何子节点,而是先调用 WalkUpFromBar(x),其中 Bar 是 Foo 的直接父类(除非 Foo 没有父类), 然后调用 VisitFoo(x)。
  3. VisitFoo(Foo *x)完成工作 3。

上述三组方法是分层次的 (Traverse* > WalkUpFrom * > Visit*)。一个方法 ( 如 Traverse*) 可以调用同一层次的方法 ( 例如其他 Traverse*) 或低一层次的方法 ( 如 WalkUpFrom*),它不能调用更高层次的方法。这个结构确保同样类型的 AST 节点会被同时访问,也就是说不会出现交替访问不同节点的情况。

下面的伪代码,简单描述了 TraverseDecl 的工作情况。假设我们有一个 AST 节点叫做 x。在入口处,TraverseDecl 将根据 x 的类型来调用相应的访问函数。比如,如果节点类型是一个函数声明,那么将调用 TraverseFunctionDecl。在 TraverseFunctionDecl 中则会递归调用函数的内容,如首先访问函数入口参数,然后访问函数体,在访问函数体的过程中又会调用 TraverseDecl 和 TraverseStmt 访问函数体内的变量声明和每一条语句。

统计源代码中的函数

明白了 BoostConASTConsumer 以后,我们就可以着手编写一个用来统计源代码中函数的例子了。我们所要做的工作其实很简单,只需要重载 VisitFunctionDecl。这样每当遇见一个函数的定义 (FunctionDecl) 的节点时,VisitFunctionDecl 将会被调用。

为了统计函数个数和信息,我们加入两个类数据成员:

由于 HandleTranslationUnit 是函数的入口,因此我们在 HandleTranslationUnit 对变量 fc 进行初始化:

最后,我们在析构函数里打印统计信息:

编译

测试 BoostConASTConsumer

小结

在这篇文章里,我们简单介绍了 Clang 的背景知识和功能特性,并通过一个小例子介绍了如何使用 Clang 的库来编写一个小程序来统计源代码中的函数。

阅读完本文,读者应该能够:

  • 理解 Clang 的背景知识和功能特性;
  • 了解如何运用 Clang 进行插件开发;
1 收藏 评论

相关文章

可能感兴趣的话题



直接登录
跳到底部
返回顶部