最近提了個技術任務包蓝,做一個基于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í)行的逐虚,借張圖:
具體是在動態(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
結果如下:
上圖中出現(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
五、使用
- 編譯生成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
- 命令行使用
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
效果圖如下:
- 集成到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í)行效果如下:
六狠角、結語
綜上大體的介紹的從搭建到使用一個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)原理及實踐