LLVM & Clang 入門

本文主要從下面幾個方面簡單介紹了一下 LLVM & Clang。html

概述前端

快速入門ios

Clang 三大件git

Xcode 編譯過程github

建立插件swift

編寫插件(實戰)後端

Xcode 集成 Pluginxcode

概述

LLVM包含三部分,分別是LLVM suiteClangTest Suitebash

  1. LLVM suite,LLVM 套件,它包含了 LLVM 所須要的全部工具、庫和頭文件,一個彙編器、解釋器、位碼分析器和位碼優化器,還包含了可用於測試 LLVM 的工具和 clang 前端的基本回歸測試。架構

  2. Clang,俗稱爲 Clang 前端,該組件將CC++Objective C,和 Objective C++代碼編譯到 LLVM 的位碼中。一旦編譯到 LLVM 位代碼中,就可使用 LLVM 套件中的工具來操做程序。

  3. Test Suite,測試套件,這是一個可選的工具,它是一套帶有測試工具的程序,可用於進一步測試 LLVM 的功能和性能。

快速入門

官方建議查看 Clang 的入門文檔,由於 LLVM 的文檔可能已通過期。

Checkout LLVM:

  • $ cd 到放 LLVM 的路徑下

  • $ git clone https://git.llvm.org/git/llvm.git/

Checkout Clang:

  • $ cd llvm/tools

  • $ git clone https://git.llvm.org/git/clang.git/

配置和構建 LLVM 和 Clang:

這裏有Xcodeninja兩種編譯方式。

須要使用到的編譯工具是CMakeCMake的最低版本要求爲3.4.3,不瞭解CMake的同窗能夠戳我進行入門瞭解。 安裝CMake須要用到brew,請確認brew已經安裝。 使用$ brew install cmake命令便可安裝CMake

方式一:使用 ninja 進行編譯

使用ninja進行編譯則還須要安裝ninja。 使用$ brew install ninja命令便可安裝ninja

  1. llvm源碼根目錄下新建一個llvm_build目錄,最終會在llvm_build目錄下生成build.ninja

  2. llvm源碼根目錄下新建一個llvm_release目錄,最終編譯文件會在llvm_release文件夾路徑下。

    • $ cd llvm_build

    • $ cmake -G Ninja ../llvm -DCMAKE_INSTALL_PREFIX= 安裝路徑(本機爲/Users/xxx/xxx/LLVM/llvm_release,注意DCMAKE_INSTALL_PREFIX後面不能有空格。

  3. 依次執行編譯、安裝指令。

    • $ ninja

    • $ ninja install

方式二:使用 Xcode 進行編譯
  1. llvm源碼根目錄的同級下建立一個名爲llvm_xcode的目錄,並$cd llvm_xcode進入到llvm_xcode

  2. 編譯命令: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, RelWithDebInfoMinSizeRel,默認Debug

    • -DLLVM_ENABLE_ASSERTIONS="On" — 在啓用斷言檢查的狀況下編譯,默認爲Yes

  3. 這裏咱們使用$ cmake -G Xcode ../llvm命令生成一個Xcode項目。

  4. 編譯,選擇ALL_BUILD Secheme 進行編譯,預計1+小時。

    All_BUILD

Clang 三大件

Clang 三大件分別是LibClangClang PluginsLibTooling

LibClang:

libclang 供了一個相對較小的 API,它將用於解析源代碼的工具暴露給抽象語法樹(AST),加載已經解析的 AST,遍歷 AST,將物理源位置與 AST 內的元素相關聯。

libclang 是一個穩定的高級 C 語言接口,隔離了編譯器底層的複雜設計,擁有更強的 Clang 版本兼容性,以及更好的多語言支持能力,對於大多數分析 AST 的場景來講,libclang 是一個很好入手的選擇。

優勢
  1. 可使用 C++ 以外的語言與 Clang 交互。
  2. 穩定的交互接口和向後兼容。
  3. 強大的高級抽象,好比用光標迭代 AST,而且不用學習 Clang AST 的全部細節。
缺點
  1. 不能徹底控制 Clang AST。

Clang Plugins:

Clang Plugin 容許你在編譯過程當中對 AST 執行其餘操做。Clang Plugin 是動態庫,由編譯器在運行時加載,而且它們很容易集成到構建環境中。

LibTooling:

LibTooling 是一個獨立的庫,它容許使用者很方便地搭建屬於你本身的編譯器前端工具,它的優勢與缺點同樣明顯,它基於 C++ 接口,讀起來晦澀難懂,可是提供給使用者遠比 libclang 強大全面的 AST 解析和控制能力,同時因爲它與 Clang 的內核過於接近致使它的版本兼容能力比 libclang 差得多,Clang 的變更很容易影響到 LibTooling。libTooling 還提供了完整的參數解析方案,能夠很方便的構建一個獨立的命令行工具。這是 libclang 所不具有的能力。通常來講,若是你只須要語法分析或者作代碼補全這類功能,libclang 將是你避免掉坑的最佳的選擇。

Xcode 編譯過程

LLVM

Objective-Cswift都採用Clang做爲編譯器前端,編譯器前端主要進行語法分析、語義分析、生成中間代碼,在這個過程當中,會進行類型檢查,若是發現錯誤或者警告會標註出來在哪一行。

LLVM

編譯器後端會進行機器無關的代碼優化,生成機器語言,而且進行機器相關的代碼優化,根據不一樣的系統架構生成不一樣的機器碼。

C++Objective-C都是編譯語言。編譯語言在執行的時候,必須先經過編譯器生成機器碼。

LLVM

如上圖所示,在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編譯

建立插件

  1. /llvm/tools/clang/tools目錄下新建插件。

    create clang plugin

  2. 修改/llvm/tools/clang/tools目錄下的CMakeLists.txt文件,新增add_clang_subdirectory(xxPlugin)

    create clang plugin

  3. QTPlugin目錄下新建一個名爲xxPlugin.cpp的文件。

  4. 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內容進行編寫。

  5. 目錄文件建立完成以後,利用cmake從新生成一下Xcode項目。在llvm_xcode目錄下執行$ cmake -G Xcode ../llvm

  6. 插件源代碼在 Xcode 項目中的Loadable modules目錄下能夠找到,這樣就能夠直接在 Xcode 裏編寫插件代碼。

編寫插件(實戰)

宗旨:重載Clang編譯過程的函數,實現自定義需求(分析),大多數狀況都是對源代碼分析。

插件文件(.cpp)結構(組成)

上圖是Clang Plugin執行的過程,分別有CompilerInstanceFrontendActionASTConsumer

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():在整個文件都解析完後會被調用。

除了上面提到的這幾個類,還有兩個比較重要的類,分別是RecursiveASTVisitorMatchFinder

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");
複製代碼

如何編寫插件相關代碼?

對源代碼(本身寫的)進行代碼分析的,好比Objcproperty修飾關鍵字,咱們就可使用 clang 命令,打印出全部的 AST Nodes 來進行分析。 咱們的源文件內容以下:

#import<UIKit/UIKit.h>

@interface ViewController : UIViewController

@property (nonatomic, strong) NSString *string;
@property (nonatomic, strong) NSArray *array;

@end

@implementation ViewController
@end
複製代碼

會發現NSStringNSArray咱們都使用了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 集成 Plugin

加載插件:

打開須要加載插件的Xcode項目,在Build Settings欄目中的OTHER_CFLAGS添加上以下內容:

-Xclang -load -Xclang (.dylib)動態庫路徑 -Xclang -add-plugin -Xclang 插件名字(namespace 的名字,名字不對則沒法使用插件)
複製代碼

設置編譯器:

因爲Clang插件須要使用對應的版本去加載,若是版本不一致則會致使編譯錯誤,會出現以下圖所示:

Build Settings欄目中新增兩項用戶定義的設置

分別是CCCXX

CC對應的是本身編譯的clang的絕對路徑,CXX對應的是本身編譯的clang++的絕對路徑。

若是👆的步驟都確認無誤以後,在編譯的時候若是遇到了下圖這種錯誤

則能夠在Build Settings欄目中搜索index,將Enable Index-Wihle-Building FunctionalityDefault改成NO

最終效果:

參考文章

推薦文章

若有內容錯誤,歡迎 issue 指正。

Example

轉載請註明出處!

相關文章
相關標籤/搜索