1. 配置LLVM和Clang
在這篇文章里慧瘤,我們會基于上一篇所述的方案進(jìn)行展開诲泌,詳細(xì)講解如何從0開始創(chuàng)建一個基于Clang LibTooling的編譯器前端工具仗哨。在開始之前盗迟,我們假設(shè)你已經(jīng)基本了解何為抽象語法樹AST娩缰,我們后面的所有內(nèi)容都是基于對AST的解析完成的脂新。如果不了解AST挪捕,請移步官方文檔Introduction to the Clang AST補(bǔ)全基礎(chǔ)知識,或者這篇中文文章争便。
此外我們還需要下載并配置好LLVM和Clang的源碼環(huán)境级零。LLVM和Clang的源碼都可從llvm.org上面下載,官方提供的代碼庫http://llvm.org/git/llvm以及http://llvm.org/git/clang速度很慢滞乙,當(dāng)然github上面的官方鏡像https://github.com/llvm-mirror/llvm也并不比官方代碼庫好到哪里去奏纪,你可以自己嘗試看哪個更快即可。
因?yàn)槲覀冊O(shè)計的CLAS是基于iOS系統(tǒng)的酷宵,我們需要使用與XCode 8.x所使用的Clang盡可能相近的版本來創(chuàng)建CLAS系統(tǒng)亥贸。蘋果開源網(wǎng)站上目前所能下載到的最新的Clang源碼版本800.0.42.1是跟隨XCode 8.2.1一起發(fā)布的,距離現(xiàn)在也已經(jīng)有了半年多時間浇垦。編寫這篇文章的時候XCode 8.3.3所使用的Clang版本是802.0.42炕置,版本號可以通過下面的命令獲取查看:
> clang --version
Apple LLVM version 8.1.0 (clang-802.0.42)
Target: x86_64-apple-darwin16.7.0
Thread model: posix
InstalledDir: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin
80x版本的Clang,都是基于代碼庫release_39這個分支的男韧,蘋果只是在release_39的基礎(chǔ)上進(jìn)行bug修復(fù)朴摊,和release_39分支的代碼相差不大,所以
> git clone -b release_39 http://llvm.org/git/llvm llvm
> cd llvm/tools
> git clone -b release_39 http://llvm.org/git/clang clang
clone完成后此虑,進(jìn)入llvm源碼根目錄甚纲,并創(chuàng)建llvm_build目錄并進(jìn)入:
> cd llvm && mkdir llvm_build && cd llvm_build
LLVM使用了CMake作為Make工具,它是一個跨平臺的類似Cocoapods的系統(tǒng)朦前,可以生成Unix Makefile介杆,Ninja以及XCode,Visual Studio韭寸,KDE在內(nèi)的項(xiàng)目文件春哨。我們這里演示使用XCode,如需生成其他類型的項(xiàng)目文件恩伺,請查詢CMake幫助文件赴背。
cmake -G "XCode" ..
等待cmake完成,正常安裝XCode的情況下不會報錯,首次運(yùn)行Cmake因?yàn)闆]有緩存時間會比較長凰荚,再次運(yùn)行就會快很多燃观。完成后便可以在llvm_build目錄下見到熟悉的LLVM.xcodeproj的文件了,雙擊打開即可便瑟。因?yàn)長LVM和Clang模塊多代碼量巨大缆毁,所以打開工程的時候會很卡。第一次打開工程XCode會提示是否自動創(chuàng)建Schemes胳徽,你可以選擇自動創(chuàng)建积锅,這樣會生成上百個Schemes。因?yàn)槲覀兊哪繕?biāo)是基于Clang的LibTooling的养盗,并不關(guān)心LLVM的組件缚陷,所以我們選擇稍后手動創(chuàng)建Scheme方便管理。到了這里往核,LLVM和Clang的基本配置已經(jīng)完成箫爷,接下來我們關(guān)注如何使用LibTooling開發(fā)我們的CLAS系統(tǒng)。
2. LibTooling vs. Libclang?
在開始動手之前聂儒,我們應(yīng)該先大致了解一下LibTooling虎锚。Clang的LibTooling是一個獨(dú)立的庫,它允許使用者很方便地搭建屬于你自己的編譯器前端工具衩婚。libclang是另外一個不錯的選擇窜护,它提供給使用者基于C的穩(wěn)定的編程接口,隔離了編譯器底層的復(fù)雜設(shè)計非春,擁有更強(qiáng)的Clang版本兼容性柱徙,以及更好的多語言支持能力,對于大多數(shù)分析AST的場景來說奇昙,libclang是一個很好入手的選擇护侮。libTooling的優(yōu)點(diǎn)與缺點(diǎn)一樣明顯,它基于C++接口储耐,讀起來晦澀難懂羊初,但是提供給使用者遠(yuǎn)比libclang強(qiáng)大全面的AST解析和控制能力,同時由于它與Clang的內(nèi)核過于接近導(dǎo)致它的版本兼容能力比libclang差得多什湘,Clang的變動很容易影響到LibTooling长赞。一般來說,如果你只需要語法分析或者做代碼補(bǔ)全這類功能闽撤,libclang將是你避免掉坑的最佳的選擇涧卵。我們之所以選擇libTooling還有一個重要的原因是它提供了完整的參數(shù)解析方案,可以很方便的構(gòu)建一個獨(dú)立的命令行工具腹尖。這是libclang所不具備的能力。
官方對于如何進(jìn)行選擇的解釋請看這里。有興趣了解更多關(guān)于libclang热幔,可以看官方doxygen文檔以及這篇文章libclang: Thinking Beyond the Compiler乐设。
3. 創(chuàng)建CLAS工程
我們假設(shè)你已經(jīng)按照1的指導(dǎo)完成了所有步驟。那么在開始這一節(jié)之前绎巨,我們需要編譯項(xiàng)目內(nèi)的clang和clangTooling這兩個target近尚,因?yàn)榻酉聛砦覀儎?chuàng)建CLAS需要這些依賴項(xiàng)。clang幾乎依賴了所有l(wèi)lvm和clang的模塊场勤,所以耗時很長戈锻,我們只需要啟動clang的編譯就可以附帶編譯所有clang依賴的庫,免去了我們一個一個地尋找并編譯的麻煩和媳,這也是為什么在1里我們并不推薦使用自動創(chuàng)建Schemes的原因格遭,因?yàn)楦緵]有必要,有clang一個基本就夠了留瞳。編譯大概需要幾十分鐘時間拒迅,取決于你的電腦配置,同時大約消耗掉8~10G左右的磁盤空間她倘。
完成后我們開始創(chuàng)建CLAS工程璧微。因?yàn)樾枰贚ibTooling進(jìn)行開發(fā),我們選擇了在Clang項(xiàng)目內(nèi)添加一個Tools的方式來簡化流程硬梁。當(dāng)然你也可以單獨(dú)創(chuàng)建一個工程前硫,然后通過引用相應(yīng)的頭文件和庫文件來使用LibTooling。但是就像上面一節(jié)所說的荧止,LLVM和Clang模塊多屹电,依賴關(guān)系極其復(fù)雜,將工程的依賴項(xiàng)配置完整需要花費(fèi)大量的時間和精力罩息,不如直接在LLVM項(xiàng)目內(nèi)開發(fā)來得方便嗤详,也適合后期調(diào)試,甚至可以直接步進(jìn)LLVM和Clang的源碼逐步加深對編譯過程的理解瓷炮。我們首先在llvm/tools/clang/tools目錄下創(chuàng)建一個新的目錄clang-autostats并進(jìn)入目錄:
> cd llvm/tools/clang/tools && mkdir clang-autostats
> cd clang-autostats
然后為我們的新工具加入第一個源文件ClangAutoStats.cpp葱色,開始解析AST。CLAS的目標(biāo)很直接娘香,找到所有ObjC的Method定義苍狰,并在左大括號后面插入語句。我們第一次嘗試烘绽,將從指定代碼中找到并打印所有ObjC的Method名稱淋昭。先從main函數(shù)寫起,很簡單只有幾行代碼:
#include "clang/Driver/Options.h"
#include "clang/Tooling/CommonOptionsParser.h"
#include "clang/Tooling/Tooling.h"
static cl::OptionCategory OptsCategory("ClangAutoStats");
int main(int argc, const char **argv) {
CommonOptionsParser op(argc, argv, OptsCategory);
ClangTool Tool(op.getCompilations(), op.getSourcePathList());
int result = Tool.run(newFrontendActionFactory<ClangAutoStatsAction>().get());
return result;
}
main的第一行代碼創(chuàng)建了一個參數(shù)解析器op安接,用來處理工具的傳入?yún)?shù)翔忽,創(chuàng)建所需的CompilationDatabase以及文件列表。OptionCategory指定了工具所處的分類,對我們的使用沒有什么影響歇式,隨便定義一個即可驶悟。第二行創(chuàng)建了一個前端工具ClangTool,將參數(shù)解析的結(jié)果傳入材失。第三行是我們完成任務(wù)的主要入口痕鳍,ClangTool的run方法將使用我們指定的ASTFrontEndAction對輸入文件進(jìn)行遍歷。我們接下來會講解ASTFrontEndAction以及后面要提到的ASTConsumer以及RecursiveASTVisitor龙巨。
在開始下一節(jié)前笼呆,我們先將我們的clang-autostats工程加入到LLVM里面。進(jìn)入llvm/tools/clang/tools目錄旨别,在CMakeLists.txt最后加入一行并保存:
cd llvm/tools/clang/tools
echo 'add_subdirectory(clang-autostats)' >> ./CMakeLists.txt
然后進(jìn)入clang-autostats目錄诗赌,創(chuàng)建CMakeLists.txt文件,粘貼下面的內(nèi)容并保存退出:
set(LLVM_LINK_COMPONENTS
Support
)
add_clang_executable(ClangAutoStats
ClangAutoStats.cpp
)
target_link_libraries(ClangAutoStats
clangAST
clangBasic
clangDriver
clangFormat
clangLex
clangParse
clangSema
clangFrontend
clangTooling
clangToolingCore
clangRewrite
clangRewriteFrontend
)
if(UNIX)
set(CLANGXX_LINK_OR_COPY create_symlink)
else()
set(CLANGXX_LINK_OR_COPY copy)
endif()
然后回到llvm_build目錄昼榛,重新執(zhí)行一遍cmake命令生成新的工程文件境肾,打開LLVM.xcodeproj后會看到在Clang executables文件夾下會出現(xiàn)我們新創(chuàng)建的ClangAutoStats目錄:

這個目錄下面有很多官方提供的如何使用Clang的示例,有興趣的同學(xué)可以學(xué)習(xí)研究胆屿。
4. 遍歷AST
這一節(jié)我們會遇到Clang AST里面三個重要的基類奥喻,ASTFrontEndAction、ASTConsumer以及RecursiveASTVisito非迹。
首先從ASTFrontEndAction開始环鲤,它繼承自FrontEndAction這個抽象基類,很多其他類是從這個類繼承出來的憎兽。例如PluginFrontEndAction冷离、PreprocessorFrontendAction,CodeGenAction等纯命。ASTFrontEndAction是用來為前端工具定義標(biāo)準(zhǔn)化的AST操作流程的西剥。一個前端可以注冊多個Action,然后在指定時刻輪詢調(diào)用每一個Action的特定方法亿汞。這是一種抽象工廠的模式瞭空。借用一張圖來描述這個問題:
圖片來源請參閱:Clang之語法抽象語法樹AST
我們繼承ASTFrontEndAction并重寫CreateASTConsumer方法。這個方法由ClangTool在run的時候通過CompilerInstance調(diào)用疗我,創(chuàng)建并返回給前端一個ASTConsumer咆畏。
class ClangAutoStatsAction : public ASTFrontendAction {
public:
virtual std::unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &CI, StringRef file) override {
return llvm::make_unique<ClangAutoStatsASTConsumer>(&CI);
}
};
Action有很多其他方法可以重寫,每個方法代表一個指定事件發(fā)生時的回調(diào)吴裤,例如BeginSourceFileAction旧找,ExecuteAction,EndSourceFileAction等麦牺,可根據(jù)需要重寫指定方法钮蛛。
接下來我們需要從ASTConsumer繼承一個自己的Consumer鞭缭,ClangAutoStatsASTConsumer:
class ClangAutoStatsASTConsumer : public ASTConsumer {
private:
ClangAutoStatsVisitor Visitor;
CompilerInstance *CI;
public:
explicit ClangAutoStatsASTConsumer(CompilerInstance *aCI)
: Visitor(&(aCI->getASTContext())), CI(aCI) {}
void HandleTranslationUnit(ASTContext &context) override {
TranslationUnitDecl *decl = context.getTranslationUnitDecl();
Visitor.TraverseTranslationUnitDecl(decl);
}
};
代碼也很短,這個Consumer提供了一系列HandleXXX方法愿卒,供使用者根據(jù)關(guān)注的類別進(jìn)行重寫缚去。有些教程里面重寫了HandleTopLevelDecl,而我們的例子則重寫了HandleTranslationUnit方法琼开。HandleTopLevelDecl是在遍歷到Decl(即聲明或定義,例如函數(shù)枕荞、ObjC interface等)的時候立即回調(diào)柜候,而HandleTranslationUnit則是在當(dāng)前的TranslationUnit(即目標(biāo)文件或源代碼)的AST被完整解析出來后才會回調(diào)。TopLevel指的是在AST第一層的節(jié)點(diǎn)躏精,對于OC代碼來說渣刷,這一般是interface、implementation矗烛、全局變量等在代碼最外層的聲明或定義辅柴。在我們的場景里使用HandleTranslationUnit更為合適,HandleTopLevelDecl更適合在語法檢查中使用瞭吃,一旦遇到錯誤碌嘀,這個方法返回false則立即中斷解析并指出錯誤位置,避免產(chǎn)生冗余錯誤信息歪架。
接下來到了最重要的RecursiveASTVisitor了股冗。根據(jù)官方的注釋,這是Clang用來以深度優(yōu)先的方式遍歷AST以及訪問所有節(jié)點(diǎn)的工具類和蚪,支持前序遍歷和后序遍歷止状。它使用的是訪問者模式。這個類依次做了3件事:
- 遍歷(Traverse):遍歷AST的每一個節(jié)點(diǎn)
- 回溯(WalkUp):在每一個節(jié)點(diǎn)上攒霹,從節(jié)點(diǎn)類型向上回溯直到節(jié)點(diǎn)的基類怯疤,然后再開始調(diào)用VisitXXX方法,父類型的Visit方法調(diào)用早于子類型的Visit方法調(diào)用催束。比如一個類型為NamespaceDecl的節(jié)點(diǎn)集峦,調(diào)用Visit方法的順序最終會是VisitDecl()->VisitNamedDecl()->VisitNamespaceDecl()。這種回溯機(jī)制保證了對于同一類型的節(jié)點(diǎn)被一起訪問泣崩,防止不同類型節(jié)點(diǎn)的交替訪問少梁。
- 訪問(Visit):對于每一個節(jié)點(diǎn),如果用戶重寫了VisitXXX方法矫付,則調(diào)用這個重寫的Visit實(shí)現(xiàn)凯沪,否則使用基類默認(rèn)的實(shí)現(xiàn)。
這3件事按照Traverse* > WalkUpFrom* > Visit*
的順序分層次執(zhí)行买优,只能訪問同一級的節(jié)點(diǎn)或者子節(jié)點(diǎn)妨马,無法遍歷到上層節(jié)點(diǎn)挺举。比如一個函數(shù)定義:
- (void)func {
...
}
會按照如下順序調(diào)用:
TraverseObjCMethodDecl
∟ WalkUpFromDecl
∟ VisitDecl
∟ VisitObjCMethodDecl
∟ TraverseCompoundStmt
∟ WalkUpFromStmt
∟ VisitStmt
∟ VisitCompoundStmt
∟ TraverseReturnStmt
...
講了這么多,對于CLAS來說烘跺,我們只需要重寫感興趣的VisitObjCImplementationDecl方法即可湘纵。你可能會問,為什么我們關(guān)注的是OC方法滤淳,卻重寫了@implementation的Visit方法呢梧喷?因?yàn)槲覀冊贑LAS的需求中,遍歷方法的時候脖咐,需要根據(jù)當(dāng)前的類名生成一個唯一的TAG標(biāo)識(就是前一篇文章所說的唯一名字)铺敌。如果重寫了VisitObjCMethodDecl方法,會增加我們實(shí)現(xiàn)這個功能的復(fù)雜度屁擅,單獨(dú)記錄每個類的類名偿凭,并不方便。我們在這個初級的例子里派歌,重寫VisitObjCImplementationDecl方法遍歷所有的頂層子節(jié)點(diǎn)弯囊,如果這個子節(jié)點(diǎn)是ObjCMethodDecl類型的,并且有函數(shù)體Body(沒有Body的方法是聲明胶果,而不是定義匾嘱,只會出現(xiàn)在@interface而不是@implementation里面),就打印出方法名:
class ClangAutoStatsVisitor
: public RecursiveASTVisitor<ClangAutoStatsVisitor> {
public:
explicit ClangAutoStatsVisitor(ASTContext *Ctx) {}
bool VisitObjCImplementationDecl(ObjCImplementationDecl *ID) {
for (auto D : ID->decls()) {
if (ObjCMethodDecl *MD = dyn_cast<ObjCMethodDecl>(D)) {
handleObjcMethDecl(MD);
}
}
return true;
}
bool handleObjcMethDecl(ObjCMethodDecl *MD) {
if (!MD->hasBody()) return true;
errs() << MD->getNameAsString() << "\n";
return true;
}
};
好了稽物,現(xiàn)在一個初具雛形的基于libTooling的工具誕生了奄毡,雖然它僅僅只能打印OC的方法名稱。我們還需要一個測試的HelloViewController.m文件:
#import <UIKit/UIKit.h>
@interface HelloViewController : UIViewController
- (void)sayHi;
@end
@implementation HelloViewController
- (void)sayHi {
NSLog(@"Hello world!");
}
@end
讓我們在XCode里編譯一下ClangAutoStats贝或,你需要創(chuàng)建一個基于ClangAutoStats的Scheme:

直接在XCode里運(yùn)行ClangAutoStats什么也不會發(fā)生吼过,因?yàn)槲覀円獋魅胄枰治龅脑次募窂揭约熬幾g選項(xiàng)等參數(shù),編輯你剛剛創(chuàng)建的ClangAutoStats的Scheme咪奖,切換到Arguments盗忱,將下面的參數(shù)一行一行地加入進(jìn)去:
/Users/test/workspace/HelloViewController.m
--
-mios-simulator-version-min=8.0
-isysroot
/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk
-isystem
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/8.1.0/include
-I/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include/c++/v1
-I/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk/usr/include
-F/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk/System/Library/Frameworks
如果你的XCode安裝在非標(biāo)準(zhǔn)路徑,請自行修改羊赵。添加完成后應(yīng)該是這個樣子的:

好了趟佃,運(yùn)行之后你會看到XCode的console中打印出了sayHi。那個參數(shù)列表里奇怪違和的"--"到底是個什么呢昧捷?我們查閱了一下官方文檔發(fā)現(xiàn)這個參數(shù)是被這么輕描淡寫一句帶過的:
the two dashes after we specify the source file. The additional options for the compiler are passed after the dashes rather than loading them from a compilation database - there just aren’t any options needed right now.
大概意思是說闲昭,在“--”后面的參數(shù),是傳遞給CI的Compilation DataBase的靡挥,而不是這個命令行工具本身的序矩。比如我們的HelloViewController.m,因?yàn)橛?code>#import <UIKit/UIKit.h>這么一條語句跋破,以及繼承了UIViewController簸淀,那么語法分析器(Sema)讀到這里的時候就需要知道UIViewController的定義是從哪里來的瓶蝴,換句話說就是它需要找到定義UIViewController的地方。怎么找呢租幕?通過指定的-I舷手、-F這些參數(shù)指定的目錄來尋找【⑿鳎“--”后面的參數(shù)男窟,可以理解為如果你要編譯HelloViewController.m需要什么參數(shù),那么這個后面就要傳遞什么參數(shù)給我們的 CLAS珠叔,否則就會看到Console里打出找不到xxx定義或者xxx.h文件的錯誤蝎宇。當(dāng)然因?yàn)橐话愕木幾g指令,會有"-c"參數(shù)指定源文件祷安,但是“--”后面并不需要,因?yàn)槲覀冊凇?-”前面就指定了兔乞』惚蓿“--”這種傳參的方式還有另外一種方法,使用-extra-arg="xxxx"的方式指定編譯參數(shù)庸追,這樣就不需要“--”了:
-extra-arg="-Ixxxxxx"
-extra-arg="-Fxxxxxx"
-extra-arg="-isysroot xxxxxx"
“--”有一個更正式的名字叫做Fixed Compilation Database霍骄,詳細(xì)內(nèi)容請參見Compilation databases for Clang-based tools
這一章我們搭建了CLAS的基本結(jié)構(gòu),寫出了我們第一個使用Clang LibTooling的編譯前端工具淡溯,雖然它現(xiàn)在還只能打印出OC的方法名稱读整,而且還不能處理特殊情況,例如Category咱娶,但是它是我們最初的原型米间,只要理解了如何使用Visitor我們可以在此基礎(chǔ)上擴(kuò)展出很多有趣的功能。
待續(xù)
下面一章我們會完善Visitor的功能膘侮,使用Rewriter進(jìn)行源碼轉(zhuǎn)換(Source-Source Transformation)屈糊。敬請期待...