iOS 編繹生成 clang 編繹器 + clang 插件開發(fā)

最近在研究 LLVM,網上看了很多這方面的教程,照著做總出現這樣那樣的問題,估計是時間隔太久,部分更新導致之前的東西出問題了,于是自己重新整理了一下,基本把坑都踩完了。希望能幫到有需要的童鞋,讓有興趣的童鞋少踩點坑。

先看最終效果,如圖所示:


image.png

為了達到這樣的效果,無論步驟多么繁瑣,都是激勵自己實現效果的最好動力!

ps:
(1)生成的 clang 版本是 15.0 。
(2)部分步驟重復的,會用步驟前面的序號代替詳細說明。如 3.1 即是下載 LLVM。
(3)插件的代碼都是親測過,可正常運行??芍苯訌椭剖褂?。

一、附上官網的鏈接:
https://llvm.org/docs/GettingStarted.html#getting-started-with-llvm

二、步驟總覽:
2.1、下載 LLVM 工程;
2.2、安裝 cmake工具;
2.3、把 llvm-project 目錄下的 clang 文件夾拷貝到 llvm 目錄下;在llvm目錄下找到CMakeLists.txt,然后搜索 add_subdirectory(projects),并在其后面添加 add_subdirectory(clang);
2.4、用cmake命令生成我們的 llvm 項目(包含clang);
2.5、編繹我們的 clang 項目,生成clang編繹器;
2.6、編寫自己的插件
2.7、測試插件
2.8、根據需求,修改插件代碼
2.9、將clang插件集成到Xcode中

三、下面分步驟詳細解說:
3.1、下載 LLVM
mac 直接通過下面官方鏈接在終端用 git clone 命令將項目克隆下來即可。項目還挺大的,整個項目克隆下來大概 3.5G 左右。(克隆之后的項目已包含clang)
官方鏈接:git clone https://github.com/llvm/llvm-project.git
目錄結構如下圖所示:

image.png

3.2、安裝 cmake工具。(如已安裝,可直接跳過這一步)
(1)先檢查mac是否已安裝cmake工具:
打開終端輸入cmake,如下圖所示:


image.png

如果提示command not found,則說明未安裝cmake

(2)進入cmake官方下載頁面:https://cmake.org/download/,完成下載安裝,雙擊打開后界面如下圖所示:

image.png

為了能在終端使用cmake命令,點擊上方菜單欄Tools,選擇"How to install For Command Line Use"
image.png

這里cmake提供三種方式,如下圖所示:
image.png

這里可以選擇其中一種方式。以第一種方式為例,拷貝第一種方式提供的路徑,在前面加export,在mac電腦的 Home 目錄的.bash_profile文件底部追加(類似于配置環(huán)境變量):

export PATH="/private/var/folders/4w/vyrtq4g54p16r733bx9cr79r0000gn/T/AppTranslocation/F6102686-D9D7-4E93-9034-2E77D6E07DF9/d/CMake.app/Contents/bin":"$PATH"

如果沒有該文件,可以直接創(chuàng)建.bash_profile文件并追加該環(huán)境變量。如下圖所示


image.png

接著,打開我們的終端Terminal(默認已經是在家目錄的路徑下,如果沒有,切換到家目錄下即可)執(zhí)行下面的命令,讓我們剛才配置的環(huán)境變量生效:

source .bash_profile

最后,嘗試一下cmake命令是否有效:

cmake --version

可以看到,我們的cmake已經能正常使用了,如下圖所示:


image.png

3.3、為了能在llvm工程中包含 clang scheme,我們需要做兩步操作:
(1)把 llvm-project 目錄下的clang文件夾拷貝到llvm目錄下。
(2)在llvm目錄下找到CMakeLists.txt,然后搜索 add_subdirectory(projects),并在其后面添加 add_subdirectory(clang)。如下圖所示:


image.png

(ps:如果沒有執(zhí)行這一步,我們生成的 llvm 項目是沒有包含 clang scheme 的。這一點要注意。)

3.4、在終端依次執(zhí)行以下命令,生成我們的 llvm 項目:
(1)cd llvm-project
(2)mkdir build
(3)cd build
(4)cmake -G Xcode ../llvm
第 4 個命令執(zhí)行完之后,cmake工具會幫我們在llvm-project目錄下的 build 目錄下生成包含 clang 和 clangTooling scheme 的llvm Xcode工程。

3.5、在 build 目錄下雙擊打開 llvm 工程,會有如下圖所示的提示:


image.png

直接選默認藍色的第一個:自動創(chuàng)建 schemes即可。

3.6、點擊Xcode選擇要編繹的項目的位置,會彈出所有的子項目。我們滾動到最后,選擇管理我們的schemes。找到 clang scheme 將并它放在比較靠前的位置,這里是為了方便后續(xù)可以快速找到它并對它進行編繹。如下圖所示:


image.png

image.png

image.png

3.7、編繹我們的 clang 項目。這里要花的時間比較漫長,時間的長短取決于機器的性能。編繹完成后,會生成 clang 可執(zhí)行文件,我們可以在 llvm-project 目錄下的 build 目錄下的 Debug 目錄下的 bin 目錄下找到它。如下圖所示:


image.png

到這里,我們已經知道如何編繹生成 clang 文件了。接下來,我們可以開始編寫我們的插件,讓編譯好的 clang 和我們插件結合一起,發(fā)揮出一些獨特的功能。

四、編寫插件代碼的準備工作。
傳統(tǒng)的編繹流程分為:前端 + 優(yōu)化器 + 后端。
前端負責源碼的解析、詞義分析、語法分析(構建抽象語法樹),LLVM的前端還會生成中間代碼。
優(yōu)化器負責進行各種優(yōu)化、改善代碼運行時間等。
后端負責將代碼映射到各種目標指令集。生成機器語言,并對機器語言進行優(yōu)化。

4.1、首先,我們在 llvm-project/llvm/clang/tools/ 新建目錄WXPlugin,然后在WXPlugin目錄下創(chuàng)建兩個文件:CMakeLists.txt 和 WXPlugin.cpp。
4.2、在 CMakeLists.txt 文件中添加下面的代碼:

add_llvm_library( WXPlugin MODULE BUILDTREE_ONLY WXPlugin.cpp )

4.3、在與 WXPlugin 同一個目錄中找到 CMakeLists.txt 文件,并在該文件中添加下下代碼:

add_clang_subdirectory(WXPlugin)

如下圖所示:


image.png

4.4、參考步驟 3.4,重新在build目錄下執(zhí)行cmake命令。
4.5、參考步驟 3.5,雙擊打開Xcode 工程,提示是否自動創(chuàng)建 scheme,選自動創(chuàng)建。
4.6、于是,我們可以在Xcode工程中的 Loadable modules 中找到我們添加的插件。


image.png

4.7、參考步驟 3.6,將 WXPlugin scheme 移動到靠前的位置,方便后續(xù)快速找到它并對它進行編繹。
4.8、展開該目錄,如下圖所示,我們就可以在 .cpp 文件中編寫我們的插件代碼了。


image.png

五、編寫插件代碼。
5.1、將下面的代碼直接拷貝到 WXPlugin.cpp 文件中

#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 WXPlugin {

    class WXMatchCallback: 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;
    }

    //判斷是否應該用copy修飾
    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:
        WXMatchCallback(CompilerInstance &CI):CI(CI){}
        //真正的回調
        void run(const MatchFinder::MatchResult &Result) {
        //通過result拿到節(jié)點
        const ObjCPropertyDecl * propertyDecl = Result.Nodes.getNodeAs<ObjCPropertyDecl>("objcPropertyDecl");
            if (propertyDecl) {
                string typeStr = propertyDecl->getType().getAsString();
                cout<<"-------拿到了:"<<typeStr<<"-------"<<endl;
            }
    };
};


//自定義WXConsumer
class WXConsumer: public ASTConsumer{
private:
    //AST節(jié)點的查找過程
    MatchFinder matcher;
    WXMatchCallback callback;
public:
    
    WXConsumer(CompilerInstance &CI):callback(CI){
        //添加一個MatchFinder去匹配objcPropertyDecl節(jié)點
        //回調在WXMatchCallback里面run方法!
        matcher.addMatcher(objcPropertyDecl().bind("objcPropertyDecl"), &callback);
    }
    
    
    //解析完一個頂級的聲明就回調一次
    bool HandleTopLevelDecl(DeclGroupRef D) {
//        cout<<"正在解析……"<<endl;
        return true;
    }
    
    //整個文件都會解析完成的回調
    void HandleTranslationUnit(ASTContext &Ctx) {
//        cout<<"文件解析完畢!"<<endl;
        matcher.matchAST(Ctx);
    }
};


//繼承PluginASTAction 實現我們自定義的Action
class WXASTACtion:public PluginASTAction{
public:
    bool ParseArgs(const CompilerInstance &CI,const std::vector<std::string> &arg){
        
        return true;
    }
    
    unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &CI,StringRef InFile){
        return unique_ptr<WXConsumer>(new WXConsumer(CI));
    }
};

}


//注冊插件
static FrontendPluginRegistry::Add<WXPlugin::WXASTACtion>WX("WXPlugin","this is WXPlugin");

5.2、編繹我們的 WXPlugin scheme。編繹后生成的 clang 可執(zhí)行文件和 WXPlugin 插件可以通過 Xcode 工程中的 Product 目錄下找到對應的文件 Show In Finder自動跳轉到文件所在的目錄,如下圖所示:


image.png

image.png

image.png

也可以在build 目錄下中的Debug子目錄 bin 和 lib兩個目錄中找到。


image.png

image.png

當然,每次我們更新了 插件的代碼,就需要重新編繹生成我們的新的插件。

六、測試插件
(1)我們先用終端來測試
命令如下:

自己編繹的 clang 路徑 -isysroot  Xcode_sdk的路徑 -Xclang -load -Xclang  自己編繹的插件生成的插件路徑 -Xclang -add-plugin -Xclang 插件的名字 -c 源碼路徑

例子如下:

/Users/pilipala/Downloads/0404/llvm-project/build/Debug/bin/clang -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator15.0.sdk -Xclang -load -Xclang /Users/pilipala/Downloads/0404/llvm-project/build/Debug/lib/WXPlugin.dylib  -Xclang -add-plugin -Xclang WXPlugin -c /Users/pilipala/Downloads/0406/Test/Test/ViewController.m

當鍵盤敲下回車的那一瞬間,我們能看到激動人心的效果,如下所示,這說明我們的插件測試是ok的:

build % /Users/pilipala/Downloads/0404/llvm-project/build/Debug/bin/clang -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator15.0.sdk -Xclang -load -Xclang /Users/pilipala/Downloads/0404/llvm-project/build/Debug/lib/WXPlugin.dylib  -Xclang -add-plugin -Xclang WXPlugin -c /Users/pilipala/Downloads/0406/Test/Test/ViewController.m
-------拿到了:NSUInteger-------
-------拿到了:Class-------
-------拿到了:NSString *-------
-------拿到了:NSString *-------
-------拿到了:BOOL-------
-------拿到了:Class _Nonnull-------
-------拿到了:id _Nonnull-------
-------拿到了:NSArray<ObjectType> * _Nonnull-------
-------拿到了:NS_RETURNS_INNER_POINTER const char *-------
-------拿到了:id _Nullable-------
-------拿到了:void * _Nullable-------
-------拿到了:char-------
-------拿到了:unsigned char-------
-------拿到了:short-------
-------拿到了:unsigned short-------
-------拿到了:int-------
-------拿到了:unsigned int-------
-------拿到了:long-------
-------拿到了:unsigned long-------
-------拿到了:long long-------
-------拿到了:unsigned long long-------
-------拿到了:float-------
-------拿到了:double-------
-------拿到了:BOOL-------
……
……

七、根據需求修改我們的插件代碼,過濾一些系統(tǒng)節(jié)點。這里我們以屬性 NSString 不能用 strong 修飾,如果用了strong 修飾,我們給以警告提示為例。插件的完整的代碼如下:


#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 WXPlugin {

    class WXMatchCallback: 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;
    }

    //判斷是否應該用copy修飾
    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:
        WXMatchCallback(CompilerInstance &CI):CI(CI){}
        //真正的回調
        void run(const MatchFinder::MatchResult &Result) {
        //通過result拿到節(jié)點
            const ObjCPropertyDecl * propertyDecl = Result.Nodes.getNodeAs<ObjCPropertyDecl>("objcPropertyDecl");
            string fileName = CI.getSourceManager().getFilename(propertyDecl->getSourceRange().getBegin()).str();
            if (propertyDecl && isUserSourceCode(fileName)) {
                
                string typeStr = propertyDecl->getType().getAsString();
                
                ObjCPropertyAttribute::Kind attrKind = propertyDecl->getPropertyAttributes();
                
                if (isShouldUseCopy(typeStr) && !(attrKind & ObjCPropertyDecl::Copy)) {
                    DiagnosticsEngine &diag = CI.getDiagnostics();
                    diag.Report(propertyDecl->getBeginLoc(), diag.getCustomDiagID(DiagnosticsEngine::Warning, "%0這個地方推薦使用copy!!"))<<typeStr;
                }
                
                cout<<"----獲取到了:"<<typeStr<<"------"<<"屬于----"<<fileName<<"------"<<endl;
            }
            
        };
};


//自定義WXConsumer
class WXConsumer: public ASTConsumer{
private:
    //AST節(jié)點的查找過程
    MatchFinder matcher;
    WXMatchCallback callback;
public:
    
    WXConsumer(CompilerInstance &CI):callback(CI){
        //添加一個MatchFinder去匹配objcPropertyDecl節(jié)點
        //回調在WXMatchCallback里面run方法!
        matcher.addMatcher(objcPropertyDecl().bind("objcPropertyDecl"), &callback);
    }
    
    
    //解析完一個頂級的聲明就回調一次
    bool HandleTopLevelDecl(DeclGroupRef D) {
//        cout<<"正在解析……"<<endl;
        return true;
    }
    
    //整個文件都會解析完成的回調
    void HandleTranslationUnit(ASTContext &Ctx) {
//        cout<<"文件解析完畢!"<<endl;
        matcher.matchAST(Ctx);
    }
};


//繼承PluginASTAction 實現我們自定義的Action
class WXASTACtion:public PluginASTAction{
public:
    bool ParseArgs(const CompilerInstance &CI,const std::vector<std::string> &arg){
        
        return true;
    }
    
    unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &CI,StringRef InFile){
        return unique_ptr<WXConsumer>(new WXConsumer(CI));
    }
};

}


//注冊插件
static FrontendPluginRegistry::Add<WXPlugin::WXASTACtion>WX("WXPlugin","this is WXPlugin");

重新編譯生成插件之后,我們還是先用終端來測試,測試成功如下圖所示:


image.png

八、將Clang編繹器集成到Xcode中
8.1、在Xcode項目中,做以下配置:
(1)在BuildSettings 中搜索Other C Flags,將下面的內容配置到 other C Flags:

-Xclang -load -Xclang 插件的路徑 -Xclang -add-plugin -Xclang 插件名

舉個例子,如下所示:

-Xclang -load -Xclang /Users/pilipala/Downloads/0404/llvm-project/build/Debug/lib/WXPlugin.dylib  -Xclang -add-plugin -Xclang WXPlugin

如下圖所示:


image.png

(2)在BuildSettings 中添加兩項用戶自定義,如下圖所示:


image.png

其中 CC 對應自己編譯后的 clang 的絕對路徑;CXX對應自己編繹后的 clang++ 的絕對路徑。
(3)在BuildSettings 中搜索 index,將Enable Index-While-Building Functionality 選項默認的 Default 改成 NO ,如下圖所示:
image.png

完成這三步的配置,即可完成 clang 在 Xcode 中的集成。重新編繹項目,即可看到文中開頭提到的效果。恭喜,你已經了解了 clang 插件開發(fā)的整個流程!

九、你可能會遇到的問題:
9.1、 編繹clang項目的提示 如下圖所示:


image.png

這時需要重新走一遍第四步,用 cmake 重新編繹出我們 llvm 項目即可。因為屬于增量編繹,所以不會像我們第一次編繹生成 llvm 項目那么久,會很快執(zhí)行完。

9.2、4.5步驟執(zhí)行完之后,在工程 Loadable modules 中找不到我們添加的插件。
解決方案參考如下:
(1)檢查以下拼寫是否有錯,建議直接復制,不要手敲:

add_clang_subdirectory(WXPlugin)
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容