本文主要從下面幾個方面簡單介紹了一下 LLVM & Clang。html
概述前端
快速入門ios
Clang 三大件git
Xcode 編譯過程github
建立插件swift
編寫插件(實戰)後端
Xcode 集成 Pluginxcode
LLVM
包含三部分,分別是LLVM suite
、Clang
和Test Suite
。bash
LLVM suite
,LLVM 套件,它包含了 LLVM 所須要的全部工具、庫和頭文件,一個彙編器、解釋器、位碼分析器和位碼優化器,還包含了可用於測試 LLVM 的工具和 clang 前端的基本回歸測試。架構
Clang
,俗稱爲 Clang 前端,該組件將C
,C++
,Objective C
,和 Objective C++
代碼編譯到 LLVM 的位碼中。一旦編譯到 LLVM 位代碼中,就可使用 LLVM 套件中的工具來操做程序。
Test Suite
,測試套件,這是一個可選的工具,它是一套帶有測試工具的程序,可用於進一步測試 LLVM 的功能和性能。
官方建議查看 Clang 的入門文檔,由於 LLVM 的文檔可能已通過期。
$ cd 到放 LLVM 的路徑下
$ git clone https://git.llvm.org/git/llvm.git/
$ cd llvm/tools
$ git clone https://git.llvm.org/git/clang.git/
這裏有Xcode
和ninja
兩種編譯方式。
須要使用到的編譯工具是CMake
,CMake
的最低版本要求爲3.4.3
,不瞭解CMake
的同窗能夠戳我進行入門瞭解。 安裝CMake
須要用到brew
,請確認brew
已經安裝。 使用$ brew install cmake
命令便可安裝CMake
。
使用ninja
進行編譯則還須要安裝ninja
。 使用$ brew install ninja
命令便可安裝ninja
。
在llvm
源碼根目錄下新建一個llvm_build
目錄,最終會在llvm_build
目錄下生成build.ninja
。
在llvm
源碼根目錄下新建一個llvm_release
目錄,最終編譯文件會在llvm_release
文件夾路徑下。
$ cd llvm_build
$ cmake -G Ninja ../llvm -DCMAKE_INSTALL_PREFIX= 安裝路徑(本機爲/Users/xxx/xxx/LLVM/llvm_release
,注意DCMAKE_INSTALL_PREFIX
後面不能有空格。
依次執行編譯、安裝指令。
$ ninja
$ ninja install
在llvm
源碼根目錄的同級下建立一個名爲llvm_xcode
的目錄,並$cd llvm_xcode
進入到llvm_xcode
。
編譯命令:cmake -G <generator> [options] <path to llvm sources>
generator commands:
Unix Makefiles
— 生成和 make 兼容的並行的 makefile。
Ninja
— 生成一個 Ninja 編譯文件,大多數 LLVM 開發者使用 Ninja。
Visual Studio
— 生成一個 Visual Studio 項目。
Xcode
— 生成一個 Xcode 項目。
options commands
-DCMAKE_INSTALL_PREFIX=
"directory" — 安裝 LLVM 工具和庫的完整路徑,默認/usr/local
。
-DCMAKE_BUILD_TYPE=
"type" — type 的值爲Debug
,Release
, RelWithDebInfo
和MinSizeRel
,默認Debug
。
-DLLVM_ENABLE_ASSERTIONS=
"On" — 在啓用斷言檢查的狀況下編譯,默認爲Yes
。
這裏咱們使用$ cmake -G Xcode ../llvm
命令生成一個Xcode
項目。
編譯,選擇ALL_BUILD
Secheme 進行編譯,預計1+
小時。
Clang 三大件分別是LibClang
、Clang Plugins
和LibTooling
。
libclang 供了一個相對較小的 API,它將用於解析源代碼的工具暴露給抽象語法樹(AST),加載已經解析的 AST,遍歷 AST,將物理源位置與 AST 內的元素相關聯。
libclang 是一個穩定的高級 C 語言接口,隔離了編譯器底層的複雜設計,擁有更強的 Clang 版本兼容性,以及更好的多語言支持能力,對於大多數分析 AST 的場景來講,libclang 是一個很好入手的選擇。
Clang Plugin 容許你在編譯過程當中對 AST 執行其餘操做。Clang Plugin 是動態庫,由編譯器在運行時加載,而且它們很容易集成到構建環境中。
LibTooling 是一個獨立的庫,它容許使用者很方便地搭建屬於你本身的編譯器前端工具,它的優勢與缺點同樣明顯,它基於 C++ 接口,讀起來晦澀難懂,可是提供給使用者遠比 libclang 強大全面的 AST 解析和控制能力,同時因爲它與 Clang 的內核過於接近致使它的版本兼容能力比 libclang 差得多,Clang 的變更很容易影響到 LibTooling。libTooling 還提供了完整的參數解析方案,能夠很方便的構建一個獨立的命令行工具。這是 libclang 所不具有的能力。通常來講,若是你只須要語法分析或者作代碼補全這類功能,libclang 將是你避免掉坑的最佳的選擇。
Objective-C
與swift
都採用Clang
做爲編譯器前端,編譯器前端主要進行語法分析、語義分析、生成中間代碼,在這個過程當中,會進行類型檢查,若是發現錯誤或者警告會標註出來在哪一行。
編譯器後端會進行機器無關的代碼優化,生成機器語言,而且進行機器相關的代碼優化,根據不一樣的系統架構生成不一樣的機器碼。
C++
,Objective-C
都是編譯語言。編譯語言在執行的時候,必須先經過編譯器生成機器碼。
如上圖所示,在Xcode
按下CMD+B
以後的工做流程。
預處理(Pre-process):他的主要工做就是將宏替換,刪除註釋展開頭文件,生成.i
文件。
詞法分析(Lexical Analysis):將代碼切成一個個 token,好比大小括號,等於號還有字符串等。是計算機科學中將字符序列轉換爲標記序列的過程。
語法分析(Semantic Analysis):驗證語法是否正確,而後將全部節點組成抽象語法樹 AST 。由 Clang 中 Parser 和 Sema 配合完成。
靜態分析(Static Analysis):使用它來表示用於分析源代碼以便自動發現錯誤。
中間代碼生成(Code Generation):生成中間代碼 IR,CodeGen 會負責將語法樹自頂向下遍歷逐步翻譯成 LLVM IR,IR 是編譯過程的前端的輸出,後端的輸入。
優化(Optimize):LLVM 會去作些優化工做,在 Xcode 的編譯設置裏也能夠設置優化級別-O1
、-O3
、-Os
...還能夠寫些本身的 Pass,官方有比較完整的 Pass 教程: Writing an LLVM Pass 。若是開啓了Bitcode
蘋果會作進一步的優化,有新的後端架構仍是能夠用這份優化過的Bitcode
去生成。
生成目標文件(Assemble):生成Target
相關Object
(Mach-o)。
連接(Link):生成Executable
可執行文件。
通過這一步步,咱們用各類高級語言編寫的代碼就轉換成了機器能夠看懂能夠執行的目標代碼了。
這裏只是做了一個Xcode
編譯過程的一個簡單的介紹,須要深刻了解的同窗能夠查看 深刻淺出iOS編譯 。
在/llvm/tools/clang/tools
目錄下新建插件。
修改/llvm/tools/clang/tools
目錄下的CMakeLists.txt
文件,新增add_clang_subdirectory(xxPlugin)
。
在QTPlugin
目錄下新建一個名爲xxPlugin.cpp
的文件。
在QTPlugin
目錄下新建一個名爲CMakeLists.txt
的文件,內容爲
add_llvm_library(xxPlugin MODULE xxPlugin.cpp PLUGIN_TOOL clang)
if(LLVM_ENABLE_PLUGINS AND (WIN32 OR CYGWIN))
target_link_libraries(xxPlugin PRIVATE
clangAST
clangBasic
clangFrontend
LLVMSupport
)
endif()
複製代碼
有可能會隨着版本的變化致使上面的內容在編譯的時候使用cmake
命令會編譯不經過。建議參照LLVM.xcodeproj
工程下的Loadable modules
裏面的CMakeLists.txt
內容進行編寫。
目錄文件建立完成以後,利用cmake
從新生成一下Xcode
項目。在llvm_xcode
目錄下執行$ cmake -G Xcode ../llvm
。
插件源代碼在 Xcode 項目中的Loadable modules
目錄下能夠找到,這樣就能夠直接在 Xcode 裏編寫插件代碼。
宗旨:重載Clang
編譯過程的函數,實現自定義需求(分析),大多數狀況都是對源代碼分析。
上圖是Clang Plugin
執行的過程,分別有CompilerInstance
、FrontendAction
和ASTConsumer
。
CompilerInstance:是一個編譯器實例,綜合了一個 Compiler 須要的 objects,如 Preprocessor,ASTContext(真正保存 AST 內容的類),DiagnosticsEngine,TargetInfo 等。
FrontendAction:是一個基於 Consumer 的抽象語法樹(Abstract Syntax Tree/AST)前端 Action 抽象基類,對於 Plugin,咱們能夠繼承至系統專門提供的PluginASTAction
來實現咱們自定義的 Action,咱們重載CreateASTConsumer()
函數返回自定義的Consumer
,來讀取 AST Nodes。
unique_ptr <ASTConsumer> CreateASTConsumer(CompilerInstance &CI, StringRef InFile) {
return unique_ptr <QTASTConsumer> (new QTASTConsumer);
}
複製代碼
ASTConsumer:是一個讀取抽象語法樹的抽象基類,咱們能夠重載下面兩個函數:
HandleTopLevelDecl()
:解析頂級的聲明(像全局變量,函數定義等)的時候被調用。
HandleTranslationUnit()
:在整個文件都解析完後會被調用。
除了上面提到的這幾個類,還有兩個比較重要的類,分別是RecursiveASTVisitor
和MatchFinder
。
RecursiveASTVisitor:是一個特別有用的類,使用它能夠訪問任意類型的 AST 節點。
VisitStmt()
:分析表達式。
VisitDecl()
:分析全部聲明。
MatchFinder:是一個 AST 節點的查找過濾匹配器,可使用addMatcher
函數去匹配本身關注的 AST 節點。
基礎結構如👇所示:其中的QTASTVisitor
不是必須的,若是你不須要訪問 AST 節點,則能夠根據本身對應的業務場景進行調整,這裏只是舉例!!!。
#include <iostream>
#include "clang/AST/AST.h"
#include "clang/AST/ASTConsumer.h"
#include "clang/AST/RecursiveASTVisitor.h"
#include "clang/Frontend/CompilerInstance.h"
#include "clang/Frontend/FrontendPluginRegistry.h"
using namespace clang;
using namespace std;
using namespace llvm;
namespace QTPlugin {
// ...other
class QTASTVisitor : public RecursiveASTVisitor <QTASTVisitor> {
private:
ASTContext *context;
public:
void setContext(ASTContext &context) {
this->context = &context;
}
// 分析全部聲明
bool VisitDecl(Decl *decl) {
return true;// 返回true以繼續遍歷AST,返回false以終止遍歷,退出Clang
}
// 分析表達式
bool VisitStmt(Stmt *S) {
return true;// 返回true以繼續遍歷AST,返回false以終止遍歷,退出Clang
}
};
class QTASTConsumer: public ASTConsumer {
private:
QTASTVisitor visitor;
// 解析完頂級的聲明(像全局變量,函數定義等)後被調用
bool HandleTopLevelDecl(DeclGroupRef D) {
return true;
}
// 在整個文件都解析完後被調用
void HandleTranslationUnit(ASTContext &context) {
visitor.setContext(context);
visitor.TraverseDecl(context.getTranslationUnitDecl());
}
};
class QTASTAction: public PluginASTAction {
public:
unique_ptr <ASTConsumer> CreateASTConsumer(CompilerInstance &CI, StringRef InFile) {
return unique_ptr <QTASTConsumer> (new QTASTConsumer);
}
bool ParseArgs(const CompilerInstance &CI, const std::vector < std::string >& args) {
return true;
}
};
}
// 註冊插件
static clang::FrontendPluginRegistry::Add < QTPlugin::QTASTAction > X("QTPlugin", "QTPlugin desc");
複製代碼
對源代碼(本身寫的)進行代碼分析的,好比Objc
的property
修飾關鍵字,咱們就可使用 clang 命令,打印出全部的 AST Nodes 來進行分析。 咱們的源文件內容以下:
#import<UIKit/UIKit.h>
@interface ViewController : UIViewController
@property (nonatomic, strong) NSString *string;
@property (nonatomic, strong) NSArray *array;
@end
@implementation ViewController
@end
複製代碼
會發現NSString
和NSArray
咱們都使用了strong
進行修飾。
使用clang -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator12.1.sdk -fmodules -fsyntax-only -Xclang -ast-dump <dump file>
命令,打印出全部的 AST Nodes 以下圖。
會發如今圈中的內容中ObjCPropertyDecl
,表示的是一個Objc
類的屬性聲明。其中包含了類名、變量名以及修飾關鍵字。 咱們可使用MatchFinder
匹配ObjCPropertyDecl
節點。
class QTASTConsumer: public ASTConsumer {
private:
MatchFinder matcher;
QTMatchHandler handler;
public:
QTASTConsumer(CompilerInstance &CI) :handler(CI) {
matcher.addMatcher(objcPropertyDecl().bind("objcPropertyDecl"), &handler);
}
void HandleTranslationUnit(ASTContext &context) {
matcher.matchAST(context);
}
};
複製代碼
這裏的QTMatchHandler
是咱們繼承至的MatchFinder::MatchCallback
的一個類,咱們能夠在run()
函數裏面去判斷哪些應該使用copy
關鍵字修飾的,而沒有使用 copy 修飾的 property。
class QTMatchHandler: public MatchFinder::MatchCallback {
private:
CompilerInstance &CI;
bool isUserSourceCode(const string filename) {
if (filename.empty()) return false;
// 非Xcode中的源碼都認爲是用戶源碼
if (filename.find("/Applications/Xcode.app/") == 0) return false;
return true;
}
bool isShouldUseCopy(const string typeStr) {
if (typeStr.find("NSString") != string::npos ||
typeStr.find("NSArray") != string::npos ||
typeStr.find("NSDictionary") != string::npos/*...*/) {
return true;
}
return false;
}
public:
QTMatchHandler(CompilerInstance &CI) :CI(CI) {}
void run(const MatchFinder::MatchResult &Result) {
const ObjCPropertyDecl *propertyDecl = Result.Nodes.getNodeAs<ObjCPropertyDecl>("objcPropertyDecl");
if (propertyDecl && isUserSourceCode(CI.getSourceManager().getFilename(propertyDecl->getSourceRange().getBegin()).str()) ) {
ObjCPropertyDecl::PropertyAttributeKind attrKind = propertyDecl->getPropertyAttributes();
string typeStr = propertyDecl->getType().getAsString();
if (propertyDecl->getTypeSourceInfo() && isShouldUseCopy(typeStr) && !(attrKind & ObjCPropertyDecl::OBJC_PR_copy)) {
cout<<"--------- "<<typeStr<<": 不是使用的 copy 修飾--------"<<endl;
DiagnosticsEngine &diag = CI.getDiagnostics();
diag.Report(propertyDecl->getBeginLoc(), diag.getCustomDiagID(DiagnosticsEngine::Warning, "--------- %0 不是使用的 copy 修飾--------")) << typeStr;
}
}
}
};
複製代碼
最後整個文件的內容能夠在 QTPlugin.cpp 看到。
最後CMD+B
編譯生成.dylib
文件,找到插件對應的.dylib
,右鍵show in finder
。
驗證:咱們能夠在終端中使用命令的方式進行驗證
本身編譯的clang文件路徑 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator12.1.sdk/ -Xclang -load -Xclang 插件(.dylib)路徑 -Xclang -add-plugin -Xclang 插件名 -c 資源文件(.h或者.m)
複製代碼
舉一個🌰,我當前是在ViewController.m
目錄下。
/Users/laiyoung_/Documents/LLVM/llvm_xcode/Debug/bin/clang -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator12.1.sdk/ -Xclang -load -Xclang /Users/laiyoung_/Documents/LLVM/llvm_xcode/Debug/lib/QTPropertyCheckPlugin.dylib -Xclang -add-plugin -Xclang QTPlugin -c ./ViewController.m
複製代碼
輸出結果:
打開須要加載插件的Xcode
項目,在Build Settings
欄目中的OTHER_CFLAGS
添加上以下內容:
-Xclang -load -Xclang (.dylib)動態庫路徑 -Xclang -add-plugin -Xclang 插件名字(namespace 的名字,名字不對則沒法使用插件)
複製代碼
因爲Clang
插件須要使用對應的版本去加載,若是版本不一致則會致使編譯錯誤,會出現以下圖所示:
在Build Settings
欄目中新增兩項用戶定義的設置
分別是CC
和CXX
。
CC
對應的是本身編譯的clang
的絕對路徑,CXX
對應的是本身編譯的clang++
的絕對路徑。
若是👆的步驟都確認無誤以後,在編譯的時候若是遇到了下圖這種錯誤
則能夠在Build Settings
欄目中搜索index
,將Enable Index-Wihle-Building Functionality
的Default
改成NO
。
參考文章:
推薦文章:
若有內容錯誤,歡迎 issue 指正。
轉載請註明出處!