Clang 入門教程 - 解析 C 語言程式

在前幾天的文章中有稍微提到 LLVM ,今天則是要介紹從 LLVM 延伸出來的子計畫(subproject):Clang。

這個教程其實是參考"How to parse C programs with clang: A tutorial in 9 parts.“,那你可能會好奇說我這樣做不就是把它翻譯過來而已?不是的,因為 Clang API 快速的更動,兩年多前的程式碼早已不敷使用,所以範例程式的參考價值已經大不如前,於是我決定把範例程式重寫,這也就是此篇文章誕生的原因。

開始之前

你必須先編譯好所需要的 LLVM & Clang 函式庫,步驟可以參考這裡。因為此篇文章所用到了 LLVM 以及 Clang 版本為 r124522,如果讓程式保證可以執行,請在所有 svn checkout 的網址後面加上"@124522″,如:

svn co http://llvm.org/svn/llvm-project/llvm/trunk@124522 llvm

當編譯好之後,請到這裡下載此篇文章會用到的所有範例程式以及輸入檔案。

Clang 是什麼?

從官方網站簡短的一句話來看:「The goal of the Clang project is to create a new C, C++, Objective C and Objective C++ front-end for the LLVM compiler.」,Clang 其實就是 LLVM 的 front-end,但同時也具有一般編譯器(如:gcc)的功能。

至於它可以做些什麼?當你想要自動化重構你的程式、寫個比 ctags 還強大的工具並且支援 Obj-C 、分析並取得程式統計資料、或是 grep 無法滿足你?這樣都是 Clang 所可以為你而做的!

而這篇教學主要會涵蓋到 Preprocessor, Parser, and AST libraries。基本上分為九個部分,而在這篇我會先提到前六篇,主要的目的是可以分析出程式中所有的全域變數,後三篇則有待日後補完了。

一、初始化前處理器

因為效能關係,所以在 Clang 中並沒有把 Preprocessor 獨立出來,而且 Preprocessor 也是存取 Lexer 的主要介面。因此在你大部分用到 libclang 的程式中,前處理器幾乎是一定會用到的類別。接下來就教你如何初始化這個類別:

int main() { const DiagnosticOptions diagOptions; TextDiagnosticPrinter *tdp = new TextDiagnosticPrinter(outs(), diagOptions, true); LangOptions lang; DiagnosticIDs *diagID; const llvm::IntrusiveRefCntPtr< DiagnosticIDs > Diags; Diagnostic diag(Diags, tdp, false); FileSystemOptions fsOptions; FileManager fm(fsOptions); SourceManager sm(diag, fm); TargetOptions tarOptions; tarOptions.Triple = LLVM_HOSTTRIPLE; TargetInfo *ti = TargetInfo::CreateTargetInfo(diag, tarOptions); HeaderSearch headers(fm); Preprocessor pp(diag, lang, *ti, sm, headers); }

這樣就完成了初始化的動作了!接著簡單解說一下初始化過程所用到的類別:

  1. Diagnostic:負責處理錯誤與警告訊息,每個 SourceManager 以及 Translation Unit 都會對應一個 Diagnostic。
  2. DiagnosticOptions:提供一些設定 Diagnostic Engine 的參數,如:IgnoreWarnings。
  3. TextDiagnosticPrinter:負責列印錯誤與警告訊息。
  4. LangOptions:提供一些程式語言上的設定,如:CPlusPlus0x,開啟 C++0x 的支援。
  5. FileSystemOptions:目前只有負責儲存 WorkingDir 的功用。
  6. FileManager:提供檔案系統的支援,如:取得檔案、目錄、檔案系統快取…等等。會在第二部分實際的練習。
  7. SourceManager:負責原始碼的快取與載入。
  8. TargetOptions:設定 Target Triple, ABI..等等。
  9. TargetInfo:負責提供 target-specific 資訊,如:各種資料型態的大小。
  10. HeaderSearch:處理標頭檔路徑,會在第三部分的教學中加入更多參數。

二、處理檔案輸入

既然是要解析程式,那不可能沒有輸入,所以我們需要取得檔案內容給前處理器處理:

const FileEntry *file = fm.getFile("input01.c"); sm.createMainFileID(file); pp.EnterMainSourceFile(); std::cout << "------------------------- The output of tutorial 2 "; std::cout << "-------------------------" << std::endl; Token Tok; do { pp.Lex(Tok); if(diag.hasErrorOccurred()) break; pp.DumpToken(Tok); std::cerr << std::endl; } while(Tok.isNot(tok::eof));

首先我們順利拿到了 input01.c 這個檔案,並且在第二行把這個檔案當做我們的程式進入點(也就是有宣告 main() 的該檔案),最後進入該檔案進行處理 #define 以及 lexing 的動作。

三、處理標頭檔

有時候 Clang 內建的路徑可能沒有辦法順利找到我們的標頭檔,這時候我們就得手動加入新的路徑,如下,我加入了兩個路徑:

HeaderSearchOptions HSOpts; HSOpts.AddPath("/usr/include/linux/", frontend::System, false, false, true); HSOpts.AddPath("/usr/lib/gcc/i686-pc-linux-gnu/4.3.4/include/", frontend::System, false, false, true); ApplyHeaderSearchOptions(headers, HSOpts, lang, ti->getTriple());

再重新執行一次程式,可以發現輸出會多了這些標頭檔的解析結果。

四、解析程式 I

接下來的教學就比較複雜一點了,前面三個部分都還沒真正做到 Parsing 的動作。而且這部份也是跟原先的教學網頁差異最大的部份,最主要的原因是 MinimalAction 這個類別已經被官方移除了,而這個類別其實就是輕量化的 Sema 類別,少了這個類別,我們只能選擇用 Sema 類別來處理,即使有點用牛刀殺雞的感覺…(如果有人知道更好的方法,歡迎告訴我!)

這部份開始之前,請先把輸入檔案換成 input04.c

IdentifierTable tab(lang); tab.PrintStats();

程式的輸出:

*** Identifier Table Stats: # Identifiers: 80 # Empty Buckets: 8112 Hash density (#identifiers per bucket): 0.009766 Ave identifier length: 8.437500 Max identifier length: 28 Number of memory regions: 1 Bytes used: 2355 Bytes allocated: 4096 Bytes wasted: 1741 (includes alignment, etc)

不過這邊的 API 用法跟以前已經有點不同,所以印出來的結果似乎不完全正確?!可能是用法不對。

五、解析程式 II

第四部分只能算是個熱身,接著才是真正的開始:

Decl *ActOnDeclarator(Scope *S, Declarator &D) { // Print names of global variables. Differentiating between // global variables and global functions is Hard in C, so this // is only an approximation. const DeclSpec& DS = D.getDeclSpec(); SourceLocation loc = D.getIdentifierLoc(); if ( // Only global declarations... D.getContext() == Declarator::FileContext // ...that aren't typedefs or extern declarations... && DS.getStorageClassSpec() != DeclSpec::SCS_extern && DS.getStorageClassSpec() != DeclSpec::SCS_typedef // ...and no functions... && !D.isFunctionDeclarator() // ...and in a user header && !pp.getSourceManager().isInSystemHeader(loc) ) { IdentifierInfo *II = D.getIdentifier(); std::cout << "Found global user declarator " << II->getName().str() << std::endl; //raw_ostream& Out(); //Out << "Found global user declarator " << II->getName(); } return Sema::ActOnDeclarator(S, D); }

其中 ActOnDeclarator 是當 p.ParseTranslationUnit() 解析到 Declarator 會呼叫的 Callback,但是因為現在的程式碼中 ActOnDeclarator 已經不是虛擬函式了,這代表我們沒辦法覆寫它,讓它在執行階段轉呼叫我們自定義的 ActOnDeclarator,所以這邊的作法有點暴力:把 clang/Sema/Sema.h 的 704 行,更改為:

virtual Decl *ActOnDeclarator(Scope *S, Declarator &D);

中間的 if 判別式則是為了把不符合全域變數的 declarator 都濾掉。

但是我們看一下輸出結果,可以發現有誤判的現象,這是因為

typedef int x(); x z; __typeof(z) r;

這類的宣告是沒辦法不透過語意分析就可以知道是不是全域變數的宣告的!

六、語意分析

要使用語意分析的方式跟上個步驟有點像,都是透過 Callback,所以我們依舊要覆寫函式:

virtual void HandleTopLevelDecl(DeclGroupRef D) { static int count = 0; DeclGroupRef::iterator it; for(it = D.begin(); it != D.end(); it++) { count++; VarDecl *VD = dyn_cast(*it); if(!VD) continue; std::cout << VD << std::endl; if(VD->isFileVarDecl() && VD->getStorageClass() != SC_Extern) { std::cout << "Read top-level variable decl: '" << VD->getDeclName().getAsString() << "'\n"; } } }

最後在主程式中,創建所需要的 ASTContext:

SelectorTable sels; Builtin::Context builtins(*ti); unsigned size = 0; ASTContext ctxt(lang, sm, *ti, tab, sels, builtins, size); MyASTConsumer consumer; CodeCompleteConsumer *codeCompleter; MySemaAction sema(pp, ctxt, consumer, false, codeCompleter); ParseAST(pp, &consumer, ctxt, false, true, codeCompleter);

一樣簡單的說明一下這些類別:

  1. SelectorTable:用來隱藏底層 multi-keyword caching 的實作。
  2. Context:存放 target-independent 以及 target-specific 的內建函式資訊,方便使用者存取。
  3. ASTContext:存放 AST nodes。
  4. ASTConsumer:抽象類別,提供使用者繼承以使用客製化的函數來存取 AST。
  5. CodeCompleteConsumer:抽象類別,提供 Code-completion 資訊(當編譯出錯原因是因為不完整,可以透過 Code-completion 自動補完)給 Consumer 。
  6. Sema:負責 C 語言的語意分析(Semantic Analysis)以及建構 AST。
  7. ParseAST(…):解析整個指令的程式,當完成時通知 ASTConsumer。函式中會透過 ASTContext 把解析過的宣告插入到 Translation Unit 之中。