LLVM

解釋性語言和編譯性語言的區(qū)別?
解釋性語言可以通過解釋器直接執(zhí)行相應(yīng)的代碼,比如python語言;而
編譯性語言要經(jīng)過編譯器編譯成相應(yīng)的可執(zhí)行文件,然后才可以執(zhí)行。

傳統(tǒng)的編譯器設(shè)計

傳統(tǒng)的編譯器

編譯器前端(Frontend)

編譯器前端的任務(wù)是解析源代碼。它會進行:詞法分析,語法分析,語義分析,檢查源代碼是否存在錯誤,然后構(gòu)建抽象語法樹(Abstract Syntax Tree,AST),LLVM的前端還會生成中間代碼(IR)。

優(yōu)化器(Optimizer)

優(yōu)化器負責各種優(yōu)化。改善代碼的運行時間,例如消除冗余計算等。

后端(Backend)/代碼生成器(CodeGenerator)

將代碼映射到目標指令集,生成機器語言,并且進行機器相關(guān)的代碼優(yōu)化。

iOS的編譯器架構(gòu)

clang和llvm的聯(lián)系?
Objective C/C/C++使用的編譯器前端是Clang,swift是Swift。后端都是LLVM。LLVM是編譯器的框架系統(tǒng),clang只是編譯器的前端部分。

iOS編譯器架構(gòu)

LLVM的設(shè)計

其他的編譯器如GCC,作為了一個整體的程序設(shè)計的,所以不夠靈活。
LLVM最重要的設(shè)計就是,使用的通用的代碼表示形式IR,它是用來在編譯器中表示代碼的形式。所以LLVM可以靈活的增加前端和后端,只要增加的前端和后端使用同一的規(guī)則IR就可以。


LLVM靈活的前后端

Clang

Clang是LLVM的一個子項目,它的誕生是為了替代GCC。它屬于LLVM架構(gòu)中的前端。
通過下面的命令可以打印源碼的編譯階段。

clang -ccc-print-phases main.m 

打印結(jié)果如下:

0: input, "main.m", objective-c
1: preprocessor, {0}, objective-c-cpp-output
2: compiler, {1}, ir
3: backend, {2}, assembler
4: assembler, {3}, object
5: linker, {4}, image
6: bind-arch, "x86_64", {5}, image

上面過程的解釋:
0:輸入原文件;
1:預(yù)處理過程,包括宏的替換,頭文件的導(dǎo)入等;
2:編譯階段,詞法分析、語法分析,最終生成IR代碼。
3:后端,代碼優(yōu)化處理,最終生成匯編;
4:匯編生成目標文件;
5:連接:鏈接需要的動態(tài)庫和靜態(tài)庫,生成可執(zhí)行文件;
6:通過不同的架構(gòu),生成對象的可執(zhí)行文件;

預(yù)處理階段
clang -E main.m

執(zhí)行完成后可以看到文件的導(dǎo)入和宏的替換。

編譯階段

詞法分析:把代碼切成一個個的Token,比如大小括號,字符串等。

clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m

語法分析:在詞法分析的基礎(chǔ)上將單詞序列組成起來,形成了語法樹,判斷程序代碼在結(jié)構(gòu)上是否正確。

clang -fmodules -fsyntax-only -Xclang -ast-dump main.m

生成中間代碼
通過下面的代碼可以生成.ll的IR代碼文件

clang -S -fobjc-arc -emit-llvm main.m

IR生成時可以通過參數(shù)選擇優(yōu)化,優(yōu)化級別分別為-O0 -O1 -O2 -O3 -Os

clang -Os -S -fobjc-arc -emit-llvm main.m -o main.ll

bitCode:Xcode7以后可通過bitcode開啟進一步的優(yōu)化,生成.bc的中間代碼。

clang -emit-llvm -c main.ll -o main.bc

生成匯編代碼:最終通過.ll或者.bc生成匯編代碼。

clang -S -fobjc-arc main.bc -o main.s
clang -S -fobjc-arc main.ll -o main.s

生成匯編代碼的時候也可以選擇優(yōu)化

clang -Os -S -fobjc-arc main.m -o main.s

生成目標文件(匯編器)

clang -fmodules -c main.s -o main.o

生成可執(zhí)行文件(鏈接)

clang main.o -o main

創(chuàng)建Clang插件

  • 在LLVM的目錄 /llvm/tools/clang/tools 創(chuàng)建新的插件:HKPlugin。


    插件目錄
  • 修改/llvm/tools/clang/tools目錄下的CMakeLists.text文件,新增add_clang_subdirectory(HKPlugin)。


    配置文件
  • 在HKPlugin目錄下新增一個HKPlugin.cpp的文件以及CMakeLists.text配置文件。在CMakeLists.text中填寫配置信息:

插件的配置信息
  • 接下來我們利用cmake重新生成一下Xcode項目。在build_xcode目錄下cmake -G Xcode ../llvm
  • 最后在LLVM的Xcode項目中可以看到Loadable modules 目錄下有自己的Plugin目錄了。下面我們就可以在里面編寫插件代碼了。


    自定義插件

    一個簡單的插件代碼:

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

    class HKConsumer : public ASTConsumer{
    public:
        //解析完一個頂級的申明就會來這里執(zhí)行!
        bool HandleTopLevelDecl(DeclGroupRef D) {
            cout<<"正在解析..."<<endl;
            return true;
        }
        
        //解析完整個文件后被調(diào)用
        void HandleTranslationUnit(ASTContext &Ctx) {
            cout<<"文件解析完畢!"<<endl;
        }
        
    };


    //繼承PluginASTAction實現(xiàn)我們自定義的Action
    class HKASTAction: 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 <HKConsumer>(new HKConsumer);
        }
        
    };


}
//注冊插件
static FrontendPluginRegistry::Add<HKPlugin::HKASTAction> X("HKPlugin", "This is the description of the plugin");

測試插件代碼

自己編寫的clang插件目錄 -isysroot /Applications/Xcode.app/Contents/Deve loper/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulat or12.2.sdk/ -Xclang -load -Xclang 插件(.dylib)路徑 -Xclang -add-plugin
-Xclang 插件名 -c 源碼路徑

測試結(jié)果:


插件測試結(jié)果

自定義clang---屬性修飾符copy

在clang的語法分析階段,判斷屬性類型為NSString,NSArray,NSDictionay的,是否使用copy修飾,如果不是copy修飾報出警告。

@interface ViewController ()
/**  */
@property (nonatomic, strong) NSString *name;
/**  */
@property (nonatomic, strong) NSArray *arrs;

@end

我們使用clang的語法分析命令翻譯該文件

clang -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/ iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator12.2.sdk??????S DK?????? -fmodules -fsyntax-only -Xclang -ast-dump ViewController.m

得到的了編譯階段的語法樹:


語法樹

可以看到屬性在語法樹中被翻譯為ObjCPropertyDecl。我們可以在語法分析的階段監(jiān)聽ObjCPropertyDecl這種類型的節(jié)點,然后進行判斷。想要監(jiān)聽某個類型的節(jié)點,我們可以使用MatchFinder來獲取。

class ZFConsumer : public ASTConsumer{
private:
    MatchFinder matcher;
    ZFMatchCallback callBack;
public:
    ZFConsumer() {
        matcher.addMatcher(objcPropertyDecl().bind("objcPropertyDecl"), &callBack);
    }
    
    //解析完整個文件后被調(diào)用
    void HandleTranslationUnit(ASTContext &Ctx) {
        cout<<"ZF文件解析完畢!"<<endl;
        matcher.matchAST(Ctx);
    }
};

為Consumer增加了成員變量matcher,在Consumer的初始化中通過matcher來添加objcPropertyDecl類型節(jié)點的監(jiān)聽。然后在文件解析完的回調(diào)方法HandleTranslationUnit中,為matcher傳遞ASTContext。另外我們還需要創(chuàng)建回調(diào)的對象ZFMatchCallback。

class ZFMatchCallback: public MatchFinder::MatchCallback {
public:
    virtual void run(const MatchFinder::MatchResult &Result) {
    //通過結(jié)果獲取到節(jié)點。
    const ObjCPropertyDecl *propertyDecl = Result.Nodes.getNodeAs<ObjCPropertyDecl>("objcPropertyDecl");
    
    if (propertyDecl) {//如果節(jié)點有值
        //拿到屬性的類型
        string typeStr = propertyDecl->getType().getAsString();
        cout<<typeStr<<"應(yīng)該用copy修飾"<<endl;
    }
}
};

監(jiān)聽到objcPropertyDecl類型的節(jié)點后,會回調(diào)run方法。這樣我們就可以獲取到節(jié)點以及節(jié)點的類型等信息。
這樣進行監(jiān)聽話,還會把引入的頭文件的信息全部都給攔截監(jiān)聽了,所以我們需要過濾下,如果是路徑包含"/Applications/Xcode.app/"就為系統(tǒng)的頭文件中的信息,需要過濾掉。

//判斷是否是自己的文件
bool isUserSourceCode(const string filename) {
    if (filename.empty()) return false;
    
    // 非Xcode中的源碼都認為是用戶源碼
    if (filename.find("/Applications/Xcode.app/") == 0) return false;
    
    return true;
}
//獲取文件名稱
string filename = CI.getSourceManager().getFilename(propertyDecl->getSourceRange().getBegin()).str();
if (propertyDecl && isUserSourceCode(filename)) {//如果節(jié)點有值,并且是用戶文件
  .......
}

其中的CI為CompilerInstance類型,是通過ZFConsumer傳遞過來的。

然后我們還需要判斷當前的屬性是否正確使用了copy

//判斷是否應(yīng)該用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;
}
//attrKind & ObjCPropertyDecl::OBJC_PR_copy 如果實行修飾符為copy返回true
if (isShouldUseCopy(typeStr) && !(attrKind & ObjCPropertyDecl::OBJC_PR_copy)) {

}

如果該使用copy的地方?jīng)]有使用copy,使用DiagnosticsEngine診斷引擎進行警告處理

//診斷引擎
DiagnosticsEngine &diag = CI.getDiagnostics();
//Report 報告
diag.Report(propertyDecl->getBeginLoc(),diag.getCustomDiagID(DiagnosticsEngine::Warning, "%0這個地方推薦用Copy"))<<typeStr;

這樣我們的clang插件就寫的差不多了,使用自定義的clang和clang插件對ViewController.m文件的屬性就行語法檢測,結(jié)果如下:

NSString *應(yīng)該用copy修飾而沒用Copy,發(fā)出警告!
demo1/ViewController.m:13:1: warning: NSString *這個地方推薦用Copy
@property (nonatomic, strong) NSString *name;
^
NSArray *應(yīng)該用copy修飾而沒用Copy,發(fā)出警告!
demo1/ViewController.m:15:1: warning: 
      NSArray *這個地方推薦用Copy
@property (nonatomic, strong) NSArray *arrs;
^
2 warnings generated.

最后附上我們自定義的clang插件的完成代碼:

#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 ZFPlugin {
    
    class ZFMatchCallback: 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;
        }
        
        //判斷是否應(yīng)該用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:
        ZFMatchCallback(CompilerInstance &CI):CI(CI) {}
        
        virtual void run(const MatchFinder::MatchResult &Result) {
            //通過結(jié)果獲取到節(jié)點。
            const ObjCPropertyDecl *propertyDecl = Result.Nodes.getNodeAs<ObjCPropertyDecl>("objcPropertyDecl");
            //獲取文件名稱
            string filename = CI.getSourceManager().getFilename(propertyDecl->getSourceRange().getBegin()).str();
            
            if (propertyDecl && isUserSourceCode(filename)) {//如果節(jié)點有值,并且是用戶文件
                //拿到屬性的類型
                string typeStr = propertyDecl->getType().getAsString();
                //拿到節(jié)點的描述信息
                ObjCPropertyDecl::PropertyAttributeKind attrKind = propertyDecl->getPropertyAttributes();
                
                //attrKind & ObjCPropertyDecl::OBJC_PR_copy 如果實行修飾符為copy返回true
                if (isShouldUseCopy(typeStr) && !(attrKind & ObjCPropertyDecl::OBJC_PR_copy)) {
                    cout<<typeStr<<"應(yīng)該用copy修飾而沒用Copy,發(fā)出警告!"<<endl;
                    //診斷引擎
                    DiagnosticsEngine &diag = CI.getDiagnostics();
                    //Report 報告
                    diag.Report(propertyDecl->getBeginLoc(),diag.getCustomDiagID(DiagnosticsEngine::Warning, "%0這個地方推薦用Copy"))<<typeStr;
                }
            }
        }
    };
    
    class ZFConsumer : public ASTConsumer{
    private:
        MatchFinder matcher;
        ZFMatchCallback callBack;
    public:
        ZFConsumer(CompilerInstance &CI): callBack(CI) {
            matcher.addMatcher(objcPropertyDecl().bind("objcPropertyDecl"), &callBack);
        }
        
        //解析完整個文件后被調(diào)用
        void HandleTranslationUnit(ASTContext &Ctx) {
            cout<<"ZF文件解析完畢!"<<endl;
            matcher.matchAST(Ctx);
        }
        
    };
    
    
    //繼承PluginASTAction實現(xiàn)我們自定義的Action
    class ZFASTAction: 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 <ZFConsumer>(new ZFConsumer(CI));
        }
        
    };
    
    
}
//注冊插件
static FrontendPluginRegistry::Add<ZFPlugin::ZFASTAction> X("ZFPlugin", "This is the description of the plugin");

Xcode集成自定義插件

前面我們寫好了自定義的功能插件,下面我們把插件集成到Xcode中去。

加載插件

打開測試的xcode項目,在Build Settings -> Other C Flags添加如下內(nèi)容:

-Xclang -load -Xclang (.dylib)插件路徑 -Xclang -add-plugin -Xclang ZFPlugin。


配置自定義插件

設(shè)置編譯器

  • 設(shè)置編譯器clang為自己的clang。
    clang插件需要對應(yīng)版本的clang去加載,如果版本不一致會導(dǎo)致編譯出錯。需要再Build Setting中新增兩項用戶定義的設(shè)置


    image.png
  • 分別為CC和CXX。
    CC對應(yīng)的是自己編譯的clang路徑;
    CXX對應(yīng)的是自己編譯的clang++路徑;


    image.png
  • 接下來在Build Settings欄目中搜索Index,將Enable Index-While-Building Functionality的Default該為NO。


    image.png
  • 然后可以達到我們的最終目的,使用自定義的clang插件檢測我們的代碼的語法了。


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

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容