1. 寫在前面
在上篇博客中已經(jīng)介紹了LLVM
下載流程和LLVM
的編譯流程爷光,也對(duì)編譯完成的LLVM
工程進(jìn)行了 Clang
和clangTooling
的編譯。
iOS底層探索之LLVM(二)——自定義Clang插件(上)
本篇博客將手把手教大家幔戏,進(jìn)行代碼編寫而线,自定義一個(gè)
Clang
插件因宇,最終實(shí)現(xiàn)的功能是對(duì)不正確使用屬性修飾會(huì)進(jìn)行報(bào)錯(cuò)恤筛,并提示正確的用詞刹淌,實(shí)現(xiàn)效果如下饶氏。
2. 前期準(zhǔn)備
2.1 新建插件
在/llvm/tools/clang/tools
目錄下新建插件JPPlugins
(這個(gè)是你自己建的讥耗,名字隨便都可以,你自己知道就可以)
2.2 修改CMakeLists.txt
修改/llvm/tools/clang/tools
目錄下的文件CMakeLists.txt
嚷往,新增加一句add_clang_subdirectory(JPPlugins)
- 在
JPPlugins
目錄下新建一個(gè)名為JPPlugins.cpp
的文件和CMakeLists.txt
的文件柠衅, 在CMakeLists.txt
中寫上如下代碼
add_llvm_library( JPPlugins MODULE BUILDTREE_ONLY
JPPlugins.cpp
)
2.3 編譯插件
- 接下來再次使用
cmake
命令重新生成一下xcode
項(xiàng)目势誊,還是在llvm_build
目錄中 使用cmake -G Xcode ../llvm
命令挤忙。 - 最后可以在
LLVM
的Xcode
項(xiàng)目中可以看到Loadable modules
目錄下有自己的Plugin
目錄了赏僧,我興致勃勃的打開工程一看。。侄柔。纵苛。瞬浓。
什么?居然失敗了屑咳,什么都沒有叭蕖!這是在和我開玩笑嗎乔宿?我仔細(xì)一看位迂,是否是
JPPlugins.cpp
文件和JPPlugins
插件同名導(dǎo)致失敗的呢?我于是改了下cpp
的名稱详瑞,同時(shí)把插件名的s
去掉了掂林,還真就成功了!這就讓我百思不得解了坝橡,就很奇怪泻帮!
- 我這倔脾氣,我就不信這個(gè)邪了计寇,我又改回原來最初的名稱锣杂,
JPPlugins.cpp
文件和JPPlugins
插件依然是同名,我又編譯了一次番宁,發(fā)現(xiàn)還是失敗了元莫。 - 我又改成
JP.cpp
文件和插件名為JPPlugins
,這回不一樣了蝶押,該成功了吧踱蠢!結(jié)果還是失敗了。 - 我又進(jìn)行了第三次嘗試棋电,
JPPlugin.cpp
文件和插件名為JPPlugin
茎截,這回我把s
去掉了苇侵,就是這么奇怪,這回成功了企锌。
我也不去下什么結(jié)論榆浓, 反正同名的是可以編譯成功的,具體是不是有
s
的后綴導(dǎo)致的撕攒,我也不知道陡鹃,反正我這兩次的成功是和這個(gè)有關(guān),時(shí)間比較多的老鐵可以去驗(yàn)證一下打却,這里就不再折騰去驗(yàn)證了杨何。
那么我們現(xiàn)在就可以去
cpp
里面編寫Clang
插件的代碼了峦睡。
3. 編寫插件代碼
廢話不多寫蝴韭,直接上代碼天通,步驟就不一一列出來了岔留,都寫在代碼里面了懂鸵。
#include <iostream>
#include "clang/AST/AST.h"
#include "clang/AST/DeclObjC.h"
#include "clang/AST/ASTConsumer.h"
#include "clang/ASTMatchers/ASTMatchers.h"
#include "clang/Frontend/CompilerInstance.h"
#include "clang/ASTMatchers/ASTMatchFinder.h"
#include "clang/Frontend/FrontendPluginRegistry.h"
// 聲明使用命名空間
using namespace clang;
using namespace std;
using namespace llvm;
using namespace clang::ast_matchers;
// 插件命名空間
namespace JPPlugin {
// 第三步:掃描完畢回調(diào)
// 4式矫、自定義回調(diào)類捺檬,繼承自MatchCallback
class JPMatchCallback : public MatchFinder::MatchCallback {
private:
// CI傳遞路徑:JPASTAction類中的CreateASTConsumer方法參數(shù) -> JPASTConsumer的構(gòu)造函數(shù) -> JPMatchCallback的私有屬性藕咏,通過構(gòu)造函數(shù)從JPASTConsumer構(gòu)造函數(shù)中獲取
CompilerInstance &CI;
// 判斷是否是自己的文件
bool isUserSourceCode(const string fileName) {
// 文件名不為空
if (fileName.empty()) return false;
// 非Xcode中的代碼都認(rèn)為是用戶的
if (0 == fileName.find("/Applications/Xcode.app/")) return false;
return true;
}
// 判斷是否應(yīng)該用copy修飾
bool isShouldUseCopy(const string typeStr) {
// 判斷類型是否是 NSString / NSArray / NSDictionary
if (typeStr.find("NSString") != string::npos ||
typeStr.find("NSArray") != string::npos ||
typeStr.find("NSDictionary") != string::npos) {
return true;
}
return false;
}
public:
// 構(gòu)造方法
JPMatchCallback(CompilerInstance &CI):CI(CI) {}
// 重載run方法
void run(const MatchFinder::MatchResult &Result) {
// 通過Result獲取節(jié)點(diǎn)對(duì)象状知,根據(jù)節(jié)點(diǎn)id("objcPropertyDecl")獲取(此id需要與JPASTConsumer構(gòu)造方法中bind的id一致)
const ObjCPropertyDecl *propertyDecl = Result.Nodes.getNodeAs<ObjCPropertyDecl>("objcPropertyDecl");
// 獲取文件名稱(包含路徑)
string fileName = CI.getSourceManager().getFilename(propertyDecl->getSourceRange().getBegin()).str();
// 如果節(jié)點(diǎn)有值 && 是用戶文件
if (propertyDecl && isUserSourceCode(fileName)) {
// 獲取節(jié)點(diǎn)的類型,并轉(zhuǎn)成字符串
string typeStr = propertyDecl->getType().getAsString();
// 節(jié)點(diǎn)的描述信息
ObjCPropertyAttribute::Kind attrKind = propertyDecl->getPropertyAttributes();
// 應(yīng)該使用copy孽查,但是沒有使用copy
if (isShouldUseCopy(typeStr) && !(attrKind & ObjCPropertyAttribute::kind_copy)) {
// 通過CI獲取診斷引擎
DiagnosticsEngine &diag = CI.getDiagnostics();
// Report 報(bào)告
/**
錯(cuò)誤位置:getLocation 節(jié)點(diǎn)位置
錯(cuò)誤:getCustomDiagID(等級(jí)饥悴,提示)
*/
diag.Report(propertyDecl->getLocation(), diag.getCustomDiagID(DiagnosticsEngine::Warning, "%0 - 這個(gè)屬性推薦使用copy修飾!!"))<< typeStr;
}
}
}
};
// 第二步:掃描配置完畢
// 3、自定義JPASTConsumer盲再,繼承自抽象類 ASTConsumer西设,用于監(jiān)聽AST節(jié)點(diǎn)的信息 -- 過濾器
class JPASTConsumer : public ASTConsumer {
private:
// AST 節(jié)點(diǎn)查找器(過濾器)
MatchFinder matcher;
// 回調(diào)對(duì)象
JPMatchCallback callback;
public:
// 構(gòu)造方法中創(chuàng)建MatchFinder對(duì)象
JPASTConsumer(CompilerInstance &CI):callback(CI) { // 構(gòu)造即將CI傳遞給callback
// 添加一個(gè)MatchFinder,每個(gè)objcPropertyDecl節(jié)點(diǎn)綁定一個(gè)objcPropertyDecl標(biāo)識(shí)(去匹配objcPropertyDecl節(jié)點(diǎn))
// 回調(diào)callback答朋,其實(shí)是在CJLMatchCallback里面重寫run方法(真正回調(diào)的是回調(diào)run方法)
matcher.addMatcher(objcPropertyDecl().bind("objcPropertyDecl"), &callback);
}
// 重載兩個(gè)方法 HandleTopLevelDecl 和 HandleTranslationUnit
// 解析完畢一個(gè)頂級(jí)的聲明就回調(diào)一次(頂級(jí)節(jié)點(diǎn)贷揽,即全局變量,屬性梦碗,函數(shù)等)
bool HandleTopLevelDecl(DeclGroupRef D) {
// cout<<"正在解析..."<<endl;
return true;
}
// 當(dāng)整個(gè)文件都解析完畢后回調(diào)
void HandleTranslationUnit(ASTContext &Ctx) {
// cout<<"文件解析完畢G菪鳌!洪规!"<<endl;
// 將文件解析完畢后的上下文context(即AST語法樹) 給 matcher
matcher.matchAST(Ctx);
}
};
//2印屁、繼承PluginASTAction,實(shí)現(xiàn)我們自定義的JPASTAction斩例,即自定義AST語法樹行為
class JPASTAction : public PluginASTAction {
public:
// 重載ParseArgs 和 CreateASTConsumer方法
/*
解析給定的插件命令行參數(shù)
- param CI 編譯器實(shí)例雄人,用于報(bào)告診斷。
- return 如果解析成功樱拴,則為true柠衍;否則洋满,插件將被銷毀,并且不執(zhí)行任何操作珍坊。該插件負(fù)責(zé)使用CompilerInstance的Diagnostic對(duì)象報(bào)告錯(cuò)誤牺勾。
*/
bool ParseArgs(const CompilerInstance &CI, const std::vector<std::string> &arg) {
return true;
}
// 返回自定義的JPASTConsumer對(duì)象,抽象類ASTConsumer的子類
unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &CI, StringRef InFile) {
/**
傳遞CI
CI用于:
- 判斷文件是否是用戶的
- 拋出警告
*/
return unique_ptr<JPASTConsumer>(new JPASTConsumer(CI));
}
};
}
// 第一步:注冊(cè)插件阵漏,并自定義JPASTAction類
// 1驻民、注冊(cè)插件
static FrontendPluginRegistry::Add<JPPlugin::JPASTAction> X("JPPlugin", "this is JPPlugin");
3.1 終端測(cè)試插件
新建立一個(gè)工程,在ViewController.m
里面寫入如下代碼
@interface ViewController ()
@property (nonatomic , strong) NSString *name;
@property (nonatomic , strong) NSArray *array;
@end
- 測(cè)試命令 如下
“ 自己編譯的
clang
文件路徑-isysroot
模擬器文件路徑-Xclang -load -Xclang
插件路徑(.dylib
)-Xclang -add-plugin -Xclang
插件名字-c
源碼文件路徑 ”
- 自己編譯的
clang
文件路徑為:llvm-project/llvm_build/Debug/bin/clang
模擬器文件路徑為:
/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk
履怯,根據(jù)自己的電腦去判斷回还。插件名字:就是你建立的插件的名字,我這里是
JPPlugin
源碼文件路徑:就是你需要插件去識(shí)別的文件的路徑
例如下面??這樣
/Users/RENO/Desktop/TEST/JPDemo/llvm-project/llvm_build/Debug/bin/clang -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk -Xclang -load -Xclang /Users/RENO/Desktop/TEST/JPDemo/llvm-project/llvm_build/Debug/lib/JPPlugin.dylib -Xclang -add-plugin -Xclang JPPlugin -c /Users/RENO/Desktop/TEST/JPDemo/PluginTestDemo/PluginTestDemo/ViewController.m
- 測(cè)試效果如下
3.2 Xcode 集成插件
- 加載插件
打開你建立的測(cè)試工程叹洲,在代碼工程的 Build Settings -> Other C Flags
添加上如下的內(nèi)容:
-Xclang -load -Xclang 插件路徑(.dylib) -Xclang -add-plugin -Xclang 插件名字
- 設(shè)置編器
由于Clang
插件需要使用對(duì)應(yīng)的版本去加載柠硕,如果版本不一致則會(huì)導(dǎo)致編譯錯(cuò)誤,會(huì)出現(xiàn)如下圖所示:
- 在
Build Settings
欄目中新增兩項(xiàng)用戶定義的設(shè)置
分別是CC 和CXX
-
CC
對(duì)應(yīng)的是自己編譯的clang
的絕對(duì)路徑 -
CXX
對(duì)應(yīng)的是自己編譯的clang++
的絕對(duì)路徑
- 設(shè)置分別是CC 和CXX
- 接下來在
Build Settings
欄目中搜索index
运提,將Enable Index-Wihle-Building Functionality
將Default
改為NO
蝗柔,我的Xcode
有毒,搜索不到民泵,我是硬找的癣丧,直接往下翻,好一番查找栈妆,終于找到了胁编。
3.3 編譯測(cè)試插件
- 最后
command + B
編譯一下
從圖中的結(jié)果,可以看出鳞尔,
clang
的插件完美的運(yùn)行了嬉橙。
4. 總結(jié)
- 過程是曲折的,結(jié)果是美好的铅檩!??
- 我的
Xcode
版本是Version 12.5
macOS Big Sur 11.4
- 以上內(nèi)容僅供參考憎夷,每個(gè)人的電腦環(huán)境不一樣,可能有差別昧旨。
- 配置的時(shí)候一定要注意路徑不要寫錯(cuò)了拾给。
- 這只是記錄我自己的探索過程,最主要的是一些思路和方法兔沃,踩過的坑蒋得,希望可以幫到大家避雷!
5. 寫在后面
關(guān)注我乒疏,更多內(nèi)容持續(xù)輸出额衙!
敬請(qǐng)期待!
?? 喜歡就點(diǎn)個(gè)贊吧????
?? 覺得有收獲的,可以來一波 收藏+關(guān)注窍侧,以免你下次找不到我????
??歡迎大家留言交流县踢,批評(píng)指正,
轉(zhuǎn)發(fā)
請(qǐng)注明出處伟件,謝謝支持硼啤!??