背景與概覽
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)
1
|
$mkdir
– p llvm_source |
2 .進入創建的目錄
1
|
$cd
llvm_source |
3 .獲取 LLVM
1
|
$svn
co http://llvm.org/svn/llvm-project/llvm/trunk llvm |
4 .獲取 Clang
1
2
|
$cd
llvm/tools $svn
co http://llvm.org/svn/llvm-project/cfe/trunk clang |
編譯 Clang
1
2
3
4
5
|
$cd
../../ ( 返回 llvm_source) $mkdir
build ( 建立編譯的工作目錄 ) $cd
build $../llvm/configure
– prefix=$HOME/llvm ( 配置 LLVM,將目標安裝目錄設定爲 $HOME/llvm) $make
( 以 DEBUG 模式來編譯 LLVM 和 Clang) |
開始使用 Clang
您可以像使用普通的編譯器一樣使用 Clang。首先你需要把 Clang 的安裝路徑加入 PATH 環境變量中。以下例子假定您使用的是 Linux 的 bash:
1
2
3
4
5
6
7
8
9
10
|
$
export PATH=$HOME/llvm/bin:$PATH $
export LD_LIBRARY_PATH=$HOME/llvm/lib/:$LD_LIBRARY_PATH 在本文中,我們使用一個常見的
hello world 程序來演示 Clang。在這裏我們把這個文件命名爲 test.c。它的內容如下: #include
<stdio.h> int
main(int argc, char **argv) {
printf("hello
world\n"); return
0; } |
您可以使用任何編輯器輸入並生成這個文件。
有了這個文件以後,您可以試試以下命令行的命令:
1
2
3
4
5
|
$
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 給出的錯誤信息將是:
1
2
|
test.c:
In function 'main': test.c:6:2:
error: expected ';' before 'return' |
而 Clang 給出的錯誤信息則是:
1
2
3
4
|
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):
1
2
3
4
|
while(x
<= 5) {
fun(x); } |
圖 1. 抽象語法樹
將程序代碼表示爲抽象語法樹的一個好處是能極大方便編譯,分析和優化。比如,對於一個單純的 C++ 程序來說,重命名一個變量就比較困難,我們不能簡單地搜索變量名稱替換成一個新的名稱,因爲這可能會改變太多。例如:不同的類或者名字空間裏也有相同名稱的變量名。將程序表示爲抽象語法樹以後,替換變量名稱就會變得很簡單:我們只需要改變對應的變量聲明抽象語法樹節點的名稱字段,然後把抽象語法樹轉換回源代碼,這是因爲每個變量的定義都會對應一個獨一無二的抽象語法樹節點。
基於 Clang 的編程
與許多編譯器前端相比,Clang 的最大特點就是良好的結構化設計,每部分都是一個單獨的庫 (library)。也就是說您可以單獨的使用其中的一部分,來編寫自己的程序。在這裏,我們將通過一個小例子來介紹如何使用 Clang 的庫來編寫一個小程序來統計源代碼中的函數。通過這個例子,您將對於如何遍歷抽象語法樹有一個基本的概念。
BoostConASTConsumer
我們的這個小程序將基於 Clang 的一個叫做 BoostConASTConsumer 的例子,這個例子的源代碼位於:”tools/clang/lib/Frontend/BoostConAction.cpp”。
BoostConASTConsumer 的代碼如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
#include
"clang/Frontend/FrontendActions.h" #include
"clang/AST/ASTConsumer.h" #include
"clang/AST/RecursiveASTVisitor.h" #include
<cstdio> #include
<iostream> using
namespace clang; namespace
{ class
BoostConASTConsumer : public ASTConsumer, public
RecursiveASTVisitor<BoostConASTConsumer> {
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 和訪問節點的類。對於一個已經構造好的語法樹,它將完成以下三方面的工作:
- 遍歷 AST 的每個節點;
- 在某一個節點,訪問這個節點的層次結構 ( 每個節點也是一個樹 );
- 如果某一個節點是一種類型的動態類別 ( 比如是一個子類等 ),調用一個用戶重載的函數來訪問這個節點;
上述工作由下面三組方法完成,分別是:
- TraverseDecl(Decl *x) 完成工作 1,它是遍歷 AST 的入口。這個方法是用來訪問有關變量和函數的聲明。TraverseDecl 只是簡單的根據節點的類型來調用相應的 TraverseFoo(Foo *x),然後遞歸訪問 x 的子節點。TraverseStmt(Stmt *x) 和 TraverseType(QualType x) 則是用來訪問一條語句和一個類型的(如結構體),它們的工作方式和 TraverseDecl 類似。
- WalkUpFromFoo(Foo *x) 完成工作 2。它不會嘗試訪問 x 的任何子節點,而是先調用 WalkUpFromBar(x),其中 Bar 是 Foo 的直接父類(除非 Foo 沒有父類), 然後調用 VisitFoo(x)。
- VisitFoo(Foo *x)完成工作 3。
上述三組方法是分層次的 (Traverse* > WalkUpFrom * > Visit*)。一個方法 ( 如 Traverse*) 可以調用同一層次的方法 ( 例如其他 Traverse*) 或低一層次的方法 ( 如 WalkUpFrom*),它不能調用更高層次的方法。這個結構確保同樣類型的 AST 節點會被同時訪問,也就是說不會出現交替訪問不同節點的情況。
下面的僞代碼,簡單描述了 TraverseDecl 的工作情況。假設我們有一個 AST 節點叫做 x。在入口處,TraverseDecl 將根據 x 的類型來調用相應的訪問函數。比如,如果節點類型是一個函數聲明,那麼將調用 TraverseFunctionDecl。在 TraverseFunctionDecl 中則會遞歸調用函數的內容,如首先訪問函數入口參數,然後訪問函數體,在訪問函數體的過程中又會調用 TraverseDecl 和 TraverseStmt 訪問函數體內的變量聲明和每一條語句。
1
2
3
4
5
6
7
8
9
10
|
switch(x->type())
{
case
FunctionDeclType: TraverseFunctionDecl(dyn_cast<FunctionDecl>(x));
break;
case
DeclType: TraverseDecl(dyn_cast<Decl>(x));
break;
...
} |
統計源代碼中的函數
明白了 BoostConASTConsumer 以後,我們就可以着手編寫一個用來統計源代碼中函數的例子了。我們所要做的工作其實很簡單,只需要重載 VisitFunctionDecl。這樣每當遇見一個函數的定義 (FunctionDecl) 的節點時,VisitFunctionDecl 將會被調用。
爲了統計函數個數和信息,我們加入兩個類數據成員:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
int
fc; // 用於統計函數個數 std::vector<FunctionDecl*>
funcs; // 用於記錄已經遍歷的函數 然後我們在
VisitFunctionDecl 裏統計函數信息: bool
VisitFunctionDecl(FunctionDecl *D) {
if
(D->isThisDeclarationADefinition()) // 只關心提供了定義的函數 //(忽略只有聲明而沒有在源代
//
碼中出現定義的函數) {
fc++;
funcs.push_back(D->getNameAsString());//
獲取函數名並保存到 funcs }
return
true; } |
由於 HandleTranslationUnit 是函數的入口,因此我們在 HandleTranslationUnit 對變量 fc 進行初始化:
1
|
fc
= 0; |
最後,我們在析構函數裏打印統計信息:
1
2
3
4
5
6
7
8
|
~BoostConASTConsumer()
{
std::cout
<< "I have seen " << fc << " functions. \ They are: " <<endl; for
(unsigned i=0; i<funcs.size(); i++) {
std::cout
<< funcs[i] << endl; }
} |
編譯
1
2
|
回到編譯的工作目錄
(build),重新編譯以及安裝 Clang: make
install |
測試 BoostConASTConsumer
1
2
3
4
5
6
|
回到
test.c 所在的目錄,輸入: $clang
-cc1 -boostcon test.c 我們將得到以下信息: I
have seen 1 functions. They are: main |
小結
在這篇文章裏,我們簡單介紹了 Clang 的背景知識和功能特性,並通過一個小例子介紹瞭如何使用 Clang 的庫來編寫一個小程序來統計源代碼中的函數。
閱讀完本文,讀者應該能夠:
- 理解 Clang 的背景知識和功能特性;
- 瞭解如何運用 Clang 進行插件開發;