iOS底層探索之LLVM(三)——自定義Clang插件(下)

1. 寫在前面

上篇博客中已經(jīng)介紹了LLVM下載流程和LLVM的編譯流程爷光,也對(duì)編譯完成的LLVM工程進(jìn)行了 ClangclangTooling的編譯。

LLVM.png

iOS底層探索之LLVM(二)——自定義Clang插件(上)

iOS底層探索之LLVM(一)——LLVM初體驗(yàn)

本篇博客將手把手教大家幔戏,進(jìn)行代碼編寫而线,自定義一個(gè) Clang插件因宇,最終實(shí)現(xiàn)的功能是對(duì)不正確使用屬性修飾會(huì)進(jìn)行報(bào)錯(cuò)恤筛,并提示正確的用詞刹淌,實(shí)現(xiàn)效果如下饶氏。

最終實(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)

修改CMakeLists.txt

  • JPPlugins目錄下新建一個(gè)名為JPPlugins.cpp的文件和CMakeLists.txt的文件柠衅, 在CMakeLists.txt中寫上如下代碼

add_llvm_library( JPPlugins MODULE BUILDTREE_ONLY
JPPlugins.cpp
)

2.3 編譯插件

新建JPPlugins.cpp和CMakeLists.txt文件
  • 接下來再次使用cmake命令重新生成一下xcode項(xiàng)目势誊,還是在llvm_build目錄中 使用cmake -G Xcode ../llvm命令挤忙。
  • 最后可以在LLVMXcode項(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去掉了苇侵,就是這么奇怪,這回成功了企锌。

添加插件然后進(jìn)行編譯

我也不去下什么結(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
自己編譯的 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è)試效果如下
終端測(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)如下圖所示:

編譯錯(cuò)誤
  • Build Settings欄目中新增兩項(xiàng)用戶定義的設(shè)置

在這里插入圖片描述

分別是CC 和CXX

  • CC對(duì)應(yīng)的是自己編譯的clang的絕對(duì)路徑
  • CXX對(duì)應(yīng)的是自己編譯的clang++的絕對(duì)路徑
CC 和CXX
  • 設(shè)置分別是CC 和CXX
在這里插入圖片描述
  • 接下來在Build Settings欄目中搜索index运提,將Enable Index-Wihle-Building FunctionalityDefault改為NO蝗柔,我的 Xcode有毒,搜索不到民泵,我是硬找的癣丧,直接往下翻,好一番查找栈妆,終于找到了胁编。
在這里插入圖片描述

3.3 編譯測(cè)試插件

  • 最后command + B 編譯一下

編譯測(cè)試工作

從圖中的結(jié)果,可以看出鳞尔,clang的插件完美的運(yùn)行了嬉橙。

低調(diào)低調(diào).png

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)注明出處伟件,謝謝支持硼啤!??

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市斧账,隨后出現(xiàn)的幾起案子谴返,更是在濱河造成了極大的恐慌,老刑警劉巖咧织,帶你破解...
    沈念sama閱讀 211,743評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件嗓袱,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡习绢,警方通過查閱死者的電腦和手機(jī)渠抹,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,296評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來毯炮,“玉大人逼肯,你說我怎么就攤上這事√壹澹” “怎么了?”我有些...
    開封第一講書人閱讀 157,285評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵大刊,是天一觀的道長(zhǎng)为迈。 經(jīng)常有香客問我,道長(zhǎng)缺菌,這世上最難降的妖魔是什么葫辐? 我笑而不...
    開封第一講書人閱讀 56,485評(píng)論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮伴郁,結(jié)果婚禮上耿战,老公的妹妹穿的比我還像新娘。我一直安慰自己焊傅,他們只是感情好剂陡,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,581評(píng)論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著狐胎,像睡著了一般鸭栖。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上握巢,一...
    開封第一講書人閱讀 49,821評(píng)論 1 290
  • 那天晕鹊,我揣著相機(jī)與錄音,去河邊找鬼。 笑死溅话,一個(gè)胖子當(dāng)著我的面吹牛晓锻,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播飞几,決...
    沈念sama閱讀 38,960評(píng)論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼砚哆,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了循狰?” 一聲冷哼從身側(cè)響起窟社,我...
    開封第一講書人閱讀 37,719評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎绪钥,沒想到半個(gè)月后灿里,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,186評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡程腹,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,516評(píng)論 2 327
  • 正文 我和宋清朗相戀三年匣吊,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片寸潦。...
    茶點(diǎn)故事閱讀 38,650評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡色鸳,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出见转,到底是詐尸還是另有隱情命雀,我是刑警寧澤,帶...
    沈念sama閱讀 34,329評(píng)論 4 330
  • 正文 年R本政府宣布斩箫,位于F島的核電站吏砂,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏乘客。R本人自食惡果不足惜狐血,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,936評(píng)論 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望易核。 院中可真熱鬧匈织,春花似錦、人聲如沸牡直。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,757評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽井氢。三九已至弦追,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間花竞,已是汗流浹背劲件。 一陣腳步聲響...
    開封第一講書人閱讀 31,991評(píng)論 1 266
  • 我被黑心中介騙來泰國(guó)打工掸哑, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人零远。 一個(gè)月前我還...
    沈念sama閱讀 46,370評(píng)論 2 360
  • 正文 我出身青樓苗分,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親牵辣。 傳聞我的和親對(duì)象是個(gè)殘疾皇子摔癣,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,527評(píng)論 2 349

推薦閱讀更多精彩內(nèi)容