結構化編譯器前端 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)

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. 抽象語法樹

imadvegfewge001

將程序代碼表示爲抽象語法樹的一個好處是能極大方便編譯,分析和優化。比如,對於一個單純的 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 和訪問節點的類。對於一個已經構造好的語法樹,它將完成以下三方面的工作:

  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 訪問函數體內的變量聲明和每一條語句。

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 進行插件開發;
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章