【clang】高效開發(fā)一個clang plugin

最近提了個技術任務包蓝,做一個基于clang的代碼檢查plugin,正好因為前段時間有看一些編譯原理方面的知識想著結合實際場景再了解一下碍拆。首先官網是學習相關知識的不二之選性芬,但還是有些部分是一句帶過,and中間也遇到過不少坑瘦黑,所以在此總結一下.

一京革、簡介

  • llvm(Low Level Virtual Machine):構架編譯器(compiler)的框架系統(tǒng)奇唤,由美國UIUC大學的Chris Lattner博士發(fā)起的開源項目,以C++編寫而成
    匹摇,LLVM 核心庫提供了與編譯器相關的支持咬扇,可以作為多種語言編譯器的后臺來使用。能夠進行程序語言的編譯期優(yōu)化廊勃、鏈接優(yōu)化懈贺、在線編譯優(yōu)化、代碼生成坡垫。LLVM的項目是一個模塊化和可重復使用的編譯器和工具鏈技術的集合梭灿。其中語言/目標設備無關的llvm IR 可以將多種不同語言連接起來。

  • clang:llvm的編譯器前端葛虐,是一個C語言胎源、C++、Objective-C屿脐、Objective-C++語言的輕量級編譯器涕蚤,以快速編譯和較少的內存占用著稱,其目標之一是超越GCC編譯器的诵,并且提供良好的插件支持万栅,容許用戶在編譯時,運行額外的自定義動作西疤。

二烦粒、搭建

首先下載下llvm&clang的源碼,推薦看getting started guide代赁,下面是主要步驟:

  • step 0:Obtaining source code
 //* Checkout LLVM:
cd where-you-want-llvm-to-live
svn co http://llvm.org/svn/llvm-project/llvm/trunk llvm

//* Checkout Clang:
cd where-you-want-llvm-to-live
cd llvm/tools
svn co http://llvm.org/svn/llvm-project/cfe/trunk clang

//* 主要上面兩個project扰她,其他[optional]按需要安裝,譬如需要使用更多的clang tools:
//* Checkout Extra Clang Tools [Optional]:
cd where-you-want-llvm-to-live
cd llvm/tools/clang/tools
svn co http://llvm.org/svn/llvm-project/clang-tools-extra/trunk extra

最新:
Check out the LLVM project:
Change directory to where you want the llvm directory placed.
git clone https://github.com/llvm/llvm-project.git
  • step 1: Configure and build LLVM and Clang:
cd where you want to build llvm
mkdir build
cd build
cmake -G <generator> [options] <path to llvm sources>
*   Some common generators are:
    *   Unix Makefiles — for generating make-compatible parallel makefiles.
    *   Ninja — for generating Ninja build files. Most llvm developers use Ninja.
    *   Visual Studio — for generating Visual Studio projects and solutions.
    *   Xcode — for generating Xcode projects.

*   Some Common options:
    *   -DCMAKE_INSTALL_PREFIX=directory— Specify for *directory* the full pathname of where you want the LLVM tools and libraries to be installed (default /usr/local).
    *   -DCMAKE_BUILD_TYPE=type — Valid options for *type* are Debug, Release, RelWithDebInfo, and MinSizeRel. Default is Debug.
    *   -DLLVM_ENABLE_ASSERTIONS=On — Compile with assertion checks enabled (default is Yes for Debug builds, No for all other build types).

*   Run your build tool of choice!
    *   The default target (i.e. make) will build all of LLVM
    *   The make check-all) will run the regression tests to ensure everything is in working order.
    *   CMake will generate build targets for each tool and library, and most LLVM sub-projects generate their own check-<project> target.
    *   Running a serial build will be *slow*. Make sure you run a parallel build; for make, use make -j.

注:快速構建的話使用-G Ninja ,需要IDE編程的話使用-G Xcode芭碍,想要并行構建的話使用make -j 徒役。

三、前奏

1.首先clang 提供了三種不同方式來編寫相應工具:

  • LibClang:穩(wěn)定的高級C語言抽象接口窖壕。
優(yōu)點:
1.可以使用C++ 之外的語言與clang交互.
2.有穩(wěn)定的交互接口 & 向后兼容.
3.提供強大的高級抽象 例如通過cursor 迭代AST忧勿,&不用學習Clang‘s AST  詳細知識.
缺點:
不能完全控制clang AST

注:官方提供c&python形式API,這里有一個OC形式的Clangkit

  • Clang Plugins:Clang插件允許您在AST上添加運行其他操作作為編譯的一部分瞻讽。插件是由編譯器在運行時加載的動態(tài)庫鸳吸,它們很容易集成到構建環(huán)境中。
使用Clang插件:
    1.如果任何依賴關系發(fā)生變化速勇,則需要您的工具重新運行
    2.希望您的工具能夠制作或打破構建
    3.需要完全控制Clang AST

不使用Clang插件:
    1.想要在構建環(huán)境之外運行工具
    2.想要完全控制Clang的設置晌砾,包括內存虛擬文件的映射
    3.需要在項目中運行特定的文件子集,而這些文件與觸發(fā)重建的任何更改無關

注:當你需要針對您的項目的特殊格式的警告或錯誤快集,或者從一個編譯步驟創(chuàng)建額外的構建工件時贡羔,clang plugins 是你的不二之選廉白。

  • LibTooling是一個C ++接口,旨在編寫獨立工具乖寒,以及集成到運行clang工具的服務中猴蹂。
使用LibTooling:
    1.希望獨立于構建系統(tǒng),在單個文件或特定文件子集上運行工具
    2.想要完全控制Clang AST
    3.想與Clang插件分享代碼

不使用LibTooling:
    1.想要作為由依賴性更改觸發(fā)的構建的一部分運行
    2.想要一個穩(wěn)定的接口楣嘁,以便在AST API更改時不需要更改代碼
    3.希望使用像cursor這樣的高級抽象
    4.不想用C ++編寫你的工具

注:當你需要寫一個簡單的語法檢查器或者一個重構工具時磅轻,選擇libTooling

2.由上可見我們的最佳選擇是clang plugin,那么我們先來看一下一個clang plugin 是如何執(zhí)行的逐虚,借張圖:

clang plugin 執(zhí)行過程

具體是在動態(tài)庫裝載進來后聋溜,可以拿到我們自定義的pluginAction(FrontendAction的子類),然后在CompileInstance初始化之后叭爱,依次調用pluginAction的幾個成員函數(BeginSourceFile撮躁、Excute、EndSourceFile)买雾,其中CreateConsumer創(chuàng)建我們自定義的consumer來獲取語法樹信息把曼,執(zhí)行ExecuteAction 函數進入ParseAST分析流程,調用我們自定義的ASTConsumer 去handle漓穿,通過RecursiveASTVisitor 或 ASTMatcher 來匹配想檢查操作的AST Notes嗤军,如果不符合規(guī)范的話,創(chuàng)建一個diagnosis 來警告或報錯晃危,并且可以創(chuàng)建一個FixHint來提供修復能力叙赚。期間通過ASTContext及其關聯(lián)的 SourceManager 獲取源碼位置&全局標識符等信息。

上述的ParseAST階段僚饭,推薦使用ASTMatcher震叮,可以簡單、精準鳍鸵、高效的匹配到AST Notes冤荆。那么接著需要了解的是上面提及多次的AST:

3. AST:Abstract Syntax Tree(抽象語法樹),編譯時期根據相關文法進行語法分析(&語義分析)后的產物权纤,用于后續(xù)中間代碼生成。

Clang的AST與其他一些編譯器生成的AST不同乌妒,它與編寫的C ++代碼和C ++標準非常相似(AST元素名與clang源碼對象變量名非常相似)汹想。例如,括號表達式和編譯時間常量在AST中以未縮減的形式可用撤蚊。這使得Clang的AST非常適合重構工具古掏。

首先看個示例:

$clang \
-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator11.2.sdk \
-fmodules \
-fsyntax-only \
-Xclang \
-ast-dump \ 
path/to/Testclang/ViewController.m

eg:
clang -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator12.2.sdk -fmodules -fsyntax-only -Xclang -ast-dump /Users/yaso/Desktop/Y/TestClang/TestClang/ViewController.m

結果如下:


ViewController AST

上圖中出現(xiàn)的各種AST Notes主要繼承于Decl己单,Stmt節(jié)點荆针,此外還有Type,DeclContext節(jié)點突颊,Expr表達式節(jié)點是stmt的一種之斯,關于AST Notes詳細知識看這里,清晰的語法樹結構是我們后續(xù)寫Recursive visitor或matcher的重要參考庞萍。另一個重點是ASTContext拧烦,其包含語法樹的全部信息,是ParseAST所需的必要參數钝计。

四恋博、編寫

綜上述,編寫一個plugin主要步驟為:

  • Creating a PluginAction
  • Creating an ASTConsumer
  • Using the RecursiveASTVisitor or ASTMatcher
  • Accessing the SourceManager and ASTContext

1.首先自定義繼承于pluginAction的action:

class CodingStyleCheckASTAction: public PluginASTAction
  {
  public:
    //如其名 創(chuàng)建自定義的ASTConsumer
    unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &Compiler, StringRef InFile);
    //解析-plugin-arg-<plugin-name> 傳入的參數
    bool ParseArgs(const CompilerInstance &CI, const std::vector<std::string> &args);
  };

2.然后是我們的CodingStyleCheckASTConsumer:

  class CodingStyleCheckASTConsumer: public ASTConsumer
  {
  public:
    CodingStyleCheckASTConsumer(CompilerInstance &Instance);
    
  private:
    // 使用ASTMatcher匹配節(jié)點私恬,聲明MatchFinder
    MatchFinder matcher;
    // MatchCallBack object 可以直接訪問匹配器的綁定節(jié)點
    CodingStyleCheckHandler handlerForMatchResult;
    //覆寫HandleTranslationUnit()债沮,當整個翻譯單元的AST已被解析出來的時候調用此方法
    void HandleTranslationUnit(ASTContext &context);
  };

3.使用ASTMatcher高效、精準匹配節(jié)點本鸣,不用像visitor那樣逐層遍歷寫大量代碼疫衩,但此處難點在于Matcher的選用,需要結合-ast-dump出的AST和AST Matcher Reference
選用合適的Matcher荣德,選用過程中可以使用clang-query對matcher進行檢驗闷煤,后續(xù)著重介紹下此部分。

    //just match Main File, up match speed
    matcher.addMatcher(objcInterfaceDecl(isExpansionInMainFile()).bind("objcInterfaceDecl"), &handlerForMatchResult);
    matcher.addMatcher(objcPropertyDecl(isExpansionInMainFile()).bind("objcPropertyDecl"), &handlerForMatchResult);
    matcher.addMatcher(binaryOperator(hasDescendant(opaqueValueExpr(hasSourceExpression(objcMessageExpr(hasSelector("modelOfClass:"))))),isExpansionInMainFile()).bind("binaryOperator_modelOfClass"), &handlerForMatchResult);
    //match ifStmt
    matcher.addMatcher(ifStmt(isExpansionInMainFile(),hasThen(compoundStmt(statementCountIs(0)))).bind("ifStmt_empty_then_body"), &handlerForMatchResult);

4.接著在MatchCallBack 對象里實現(xiàn)run方法對綁定的節(jié)點進行處理命爬,生成相應Diagnostic&FixHint:

  void CodingStyleCheckHandler::run(const MatchFinder::MatchResult &Result)
  {
    if (const ObjCPropertyDecl *propertyDecl = Result.Nodes.getNodeAs<ObjCPropertyDecl>("objcPropertyDecl")) {
      // 存儲 Objective-C 類屬性
      checkPropertyDecl(propertyDecl);
    } else if (const ObjCInterfaceDecl *interfaceDecl = Result.Nodes.getNodeAs<ObjCInterfaceDecl>("objcInterfaceDecl")) {
      checkInterfaceDecl(interfaceDecl);
    } else if (const BinaryOperator *binaryOperator = Result.Nodes.getNodeAs<BinaryOperator>("binaryOperator_modelOfClass")) {
      checkAppointedMethod(binaryOperator);
    } else if (const IfStmt *stmtIf = Result.Nodes.getNodeAs<IfStmt>("ifStmt_empty_then_body")) {
      SourceLocation location = stmtIf->getIfLoc();
      diagWaringReport(location, "Don't use empty body in IfStmt", NULL);
    } else if (const IfStmt *stmtIf = Result.Nodes.getNodeAs<IfStmt>("condition_always_true")) {
      SourceLocation location = stmtIf->getIfLoc();
      diagWaringReport(location, "Body will certainly be executed when condition true", NULL);
    } else if (const IfStmt *stmtIf = Result.Nodes.getNodeAs<IfStmt>("condition_always_false")) {
      SourceLocation location = stmtIf->getIfLoc();
      diagWaringReport(location, "Body will never be executed when condition false.", NULL);
    }
  }
// 提示語向Kyle Wong看齊 ^_^

最后不要忘了注冊插件曹傀,使用FrontendPluginRegistry::Add<>:

static clang::FrontendPluginRegistry::Add<CodingStyleCheck::CodingStyleCheckASTAction>
X("coding-style-check", "check code style");

相關源碼&.dylib已上傳github:CodingStyleCheck

五、使用

  1. 編譯生成plugin.dylib饲宛,首先在plugin.cpp同級目錄下添加CMakeLists.txt文件皆愉,指定加載依賴和所需鏈接庫:
//CMakeLists.txt
add_llvm_loadable_module(CodingStyleCheck 
CodingStyleCheck.cpp
CodingStyleCheck.hpp
CustomPluginUtil.hpp
PLUGIN_TOOL clang
)

if(LLVM_ENABLE_PLUGINS AND (WIN32 OR CYGWIN))
  target_link_libraries(CodingStyleCheck PRIVATE
    clangAST
    clangBasic
    clangFrontend
    clangLex
    LLVMSupport
    )
endif()

如果是-G Unix Makefiles 構建的話,直接在build目錄 make CodingStyleCheck艇抠,然后去./lib目錄找到.dylib
如果是-G Xcode的話幕庐,直接選中你plugin scheme Run,依據你的構建的類型家淤,去相應目錄(Debug/Release)下找到.dylib

  1. 命令行使用
clang \
-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator11.2.sdk \
/path/to/test/*.m \
-fsyntax-only \
-v \
-Xclang -load \
-Xclang /path/to/CodingStyleCheck.dylib \
-Xclang -plugin \
-Xclang coding-style-check \
-Xclang \
-plugin-arg-coding-style-check \
-Xclang \
/path/to/test_dir

注:
 /path/to/test:需要check的文件目錄异剥,可以是單個文件.
 /path/to/CodingStyleCheck.dylib:plugin.dylib 路徑.
 此處clang使用自己編譯出來的(非系統(tǒng)自帶),否則各種symbol not find 

效果圖如下:


coding-style-check result
  1. 集成到Xcode中使用
    首先 hack Xcode絮重,才能使用指定的clang編譯器&plugin:
    下載 XcodeHacking.zip 并解壓冤寿,修改一下 HackedClang.xcplugin/Contents/Resources/HackedClang.xcspec 文件,將 ExecPath 的值修改為你剛編譯的clang編譯器路徑 (沒有使用DCMAKE_INSTALL_PREFIX特殊指定的話青伤,默認為/usr/local/bin/clang):

cd 到XcodeHacking目錄督怜,執(zhí)行移動指令

sudo mv HackedClang.xcplugin `xcode-select -print-path`/../PlugIns/Xcode3Core.ideplugin/Contents/SharedSupport/Developer/Library/Xcode/Plug-ins
sudo mv HackedBuildSystem.xcspec `xcode-select -print-path`/Platforms/iPhoneSimulator.platform/Developer/Library/Xcode/Specifications

重啟Xcode 更改編譯器,添加OTHER_CFLAGS

替換編譯器
//添加OTHER_CFLAGS:
-Xclang -load -Xclang /path/to/CodingStyleCheck.dylib -Xclang -add-plugin -Xclang CodingStyleCheck -v -Xclang

編譯執(zhí)行效果如下:

coding-style-check result

六狠角、結語

綜上大體的介紹的從搭建到使用一個plugin的過程号杠,中間的有些描述可能過于簡潔,如有紕漏或者疑問歡迎留言指出。

學習過程中參考了很多文檔&大佬的文章姨蟋,依次如下:
The LLVM Compiler Infrastructure
Clang 7 documentation
CLANG技術分享系列一:編寫你的第一個CLANG插件
Clang 之旅--使用 Xcode 開發(fā) Clang 插件
AST matchers and Clang refactoring tools
[原創(chuàng)]關于clang插件的實現(xiàn)原理及實踐

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末屉凯,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子眼溶,更是在濱河造成了極大的恐慌悠砚,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件偷仿,死亡現(xiàn)場離奇詭異哩簿,居然都是意外死亡,警方通過查閱死者的電腦和手機酝静,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進店門节榜,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人别智,你說我怎么就攤上這事宗苍。” “怎么了薄榛?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵讳窟,是天一觀的道長。 經常有香客問我敞恋,道長丽啡,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任硬猫,我火速辦了婚禮补箍,結果婚禮上,老公的妹妹穿的比我還像新娘啸蜜。我一直安慰自己坑雅,他們只是感情好,可當我...
    茶點故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布衬横。 她就那樣靜靜地躺著裹粤,像睡著了一般。 火紅的嫁衣襯著肌膚如雪蜂林。 梳的紋絲不亂的頭發(fā)上遥诉,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天,我揣著相機與錄音噪叙,去河邊找鬼突那。 笑死,一個胖子當著我的面吹牛构眯,可吹牛的內容都是我干的。 我是一名探鬼主播早龟,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼惫霸,長吁一口氣:“原來是場噩夢啊……” “哼猫缭!你這毒婦竟也來了?” 一聲冷哼從身側響起壹店,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤猜丹,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后硅卢,有當地人在樹林里發(fā)現(xiàn)了一具尸體射窒,經...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年将塑,在試婚紗的時候發(fā)現(xiàn)自己被綠了脉顿。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,690評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡点寥,死狀恐怖艾疟,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情敢辩,我是刑警寧澤蔽莱,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布戚长,位于F島的核電站盗冷,受9級特大地震影響,放射性物質發(fā)生泄漏同廉。R本人自食惡果不足惜仪糖,卻給世界環(huán)境...
    茶點故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望恤溶。 院中可真熱鬧乓诽,春花似錦、人聲如沸咒程。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽帐姻。三九已至稠集,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間饥瓷,已是汗流浹背剥纷。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留呢铆,地道東北人晦鞋。 一個月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親悠垛。 傳聞我的和親對象是個殘疾皇子线定,可洞房花燭夜當晚...
    茶點故事閱讀 44,577評論 2 353

推薦閱讀更多精彩內容