Clang 之旅--使用 Xcode 開發 Clang 插件

Clang 之旅系列文章:javascript

Clang 之旅--使用 Xcode 開發 Clang 插件
Clang 之旅--[翻譯]添加自定義的 attribute
Clang 之旅--實現一個自定義檢查規範的 Clang 插件
java


前言

最近在跟老大的聊天中聊到了一個比較特殊的需求:是否有辦法在編譯階段檢查某個方法的參數與返回值的類型相同,若是類型不一致的話能拋出編譯錯誤的提示。這彷佛已經不是 Objective-C 或者 Swift 的語言語法自己所能解決的了,老大還指點了能夠從編譯器等底層中進行研究。因而,我踏進了 Clang 和 LLVM 的大門。ios

我打算將 Clang 的研究心得分爲幾篇文章來寫,這是 Clang 之旅的第一篇,主要講如何用 Xcode 編譯 Clang,以及實現一個簡單的 Clang 插件並掛載到 Xcode 中參與編譯流程,算是進入 Clang 的門檻。只是,這門檻就狠狠地讓我吃了苦頭,Google 找到好幾篇博客講怎麼編譯 Clang 的,可是也有一些年頭了,版本比較舊,編譯出來的 Clang 不能運行在如今的系統上;還有一些寫的比較含糊,漏了某些關鍵步驟,致使花了好幾個小時跟着教程作下來最後仍是一堆 error;並且試錯的成本仍是比較高的,下載的源碼有1G多(考慮從 Github 下載的速度🙄,須要掛個代理),完整編譯出來有20G左右,個人15款 Macbook Pro 大概須要瘋狂編譯2個小時…...若是不能接受這些的話,仍是別嘗試了,很遺憾,你連見到 Clang 真容的機會都沒有┑( ̄Д  ̄)┍git


編譯源碼

準備工做

Clang 須要用 CMake 來編譯,CMake 的安裝方法能夠參考這篇文章:Mac 安裝 CMake & CMake Command Line Tools,建議對 CMake 徹底不瞭解的同窗能夠先補充一點 CMake 的基本知識,這樣能更容易理解接下來要作的事情,CMake 的入門知識能夠參考:CMake 入門實戰github


下載源碼

首先建立 LLVM 的源碼路徑及編譯路徑:xcode

cd /opt
sudo mkdir llvm
sudo chown `whoami` llvm    // 將 llvm 目錄的全部者指定爲當前用戶
cd llvm
export LLVM_HOME=`pwd`      // 設置當前目錄(/opt/llvm)爲 LLVM_HOME 目錄
複製代碼

接下來從 Github clone 源代碼(注意這幾條語句中的 release_60,在當前時間2018.3.18時,我試過了 release_3三、release_39,編譯出來的 Clang 插件在運行的時候都會報 NSUUID 的 Nullability 錯誤,應該是這些版本不支持 Objective-C 後來加的 Nullability 特性,因此我下載了當前最新的 release_60 分支。通常來講,最新分支是兼容已有特性的,因此優先下載最新分支,分支查看能夠參照下圖):bash

git clone -b release_60 git@github.com:llvm-mirror/llvm.git llvm
git clone -b release_60 git@github.com:llvm-mirror/clang.git llvm/tools/clang
git clone -b release_60 git@github.com:llvm-mirror/clang-tools-extra.git llvm/tools/clang/tools/extra
git clone -b release_60 git@github.com:llvm-mirror/compiler-rt.git llvm/projects/compiler-rt
複製代碼

編譯源碼

生成 Xcode 工程(也能夠直接用命令行編譯,不過你們平時可能看習慣了 Xcode 工程,因此用 Xcode 編譯比較習慣)app

mkdir llvm_build; cd llvm_build
cmake -G Xcode ../llvm -DCMAKE_BUILD_TYPE:STRING=MinSizeRel
複製代碼

生成的文件以下:ide

打開 Xcode 工程,選擇自動建立 Schemes:測試

而後編譯 Clang 和 libClang(能夠隨時終止編譯,再次點擊編譯會從上次中止的地方繼續進行):


這裏可能須要1個多小時才能完成編譯,如無心外,編譯成功!


編寫你的第一個插件

這個插件實現的功能就是打印語法樹上全部節點的類名以及父類名,建立 Clang 插件的總體步驟以下圖:


首先修改源代碼目錄 /opt/llvm/llvm/tools/clang/tools 下的 CMakeLists.txt 文件,添加一個新的編譯目標,直接在 CMakeLists.txt 的最後面添加上一行,以下圖:



而後在 tools 目錄下添加 MyPlugin 文件夾,文件夾裏面新增兩個文件 CMakeLists.txt 和 MyPlugin.cpp,這裏先不講解具體文件中的內容,目的是想讓插件跑起來,看到運行效果。

CMakeLists.txt 文件以下:

add_llvm_loadable_module(MyPlugin 
MyPlugin.cpp
PLUGIN_TOOL clang
)

if(LLVM_ENABLE_PLUGINS AND (WIN32 OR CYGWIN))
  target_link_libraries(MyPlugin PRIVATE
    clangAST
    clangBasic
    clangFrontend
    clangLex
    LLVMSupport
    )
endif()複製代碼
  1. MyPlugin.cpp 文件以下:

#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 MyPlugin
{
    class MyASTVisitor: public
    RecursiveASTVisitor < MyASTVisitor >
    {
private:
        ASTContext *context;
public:
        void setContext(ASTContext &context)
        {
            this->context = &context;
        }

        bool VisitDecl(Decl *decl)
        {
            if (isa < ObjCInterfaceDecl > (decl)) {
                ObjCInterfaceDecl *interDecl = (ObjCInterfaceDecl *)decl;
                if (interDecl->getSuperClass()) {
                    string interName = interDecl->getNameAsString();
                    string superClassName = interDecl->getSuperClass()->getNameAsString();

                    cout << "-------- ClassName:" << interName << " superClassName:" << superClassName << endl;
                }
            }

            return true;
        }
    };
    
    class MyASTConsumer: public ASTConsumer
    {
private:
        MyASTVisitor visitor;
        void HandleTranslationUnit(ASTContext &context)
        {
            visitor.setContext(context);
            visitor.TraverseDecl(context.getTranslationUnitDecl());
        }
    };
    class MyASTAction: public PluginASTAction
    {
public:
        unique_ptr < ASTConsumer > CreateASTConsumer(CompilerInstance & Compiler, StringRef InFile) {
            return unique_ptr < MyASTConsumer > (new MyASTConsumer);
        }
        bool ParseArgs(const CompilerInstance &CI, const std::vector < std::string >& args)
        {
            return true;
        }
    };
}
static clang::FrontendPluginRegistry::Add
< MyPlugin::MyASTAction > X("MyPlugin",
                            "MyPlugin desc");複製代碼

再次在 llvm_build 目錄下 CMake 一下

cmake -G Xcode ../llvm -DCMAKE_BUILD_TYPE:STRING=MinSizeRel
複製代碼

而後從新打開 LLVM.xcodeproj 工程,會發現多了一個 MyPlugin 的編譯目標,選中進行編譯。


編譯成功以後,就能夠獲得一個 MyPlugin.dylib 的 Clang 插件了~爲了方便,我將 MyPlugin.dylib 放在桌面上:


使用插件

命令行中使用插件

首先用命令行對單文件測試一下剛剛生成的 Clang 插件是否正確,新建一個測試用文件 test.m 放在桌面,test.m 以下:

#import<UIKit/UIKit.h>
@interface ViewController : UIViewController
@end
@implementation ViewController
- (instancetype)init
{
    if(self = [super init]){
    }
    return self;
}
@end
複製代碼

如今個人 test.m 和 MyPlugin.dylib 都在桌面上了(固然也能夠放在不一樣的目錄下,只要在待會用到這兩個文件的地方指定各自的絕對路徑就行,這裏是爲了方便敘述)


接着命令行 cd 到桌面,而後執行如下命令就能夠看到結果了:

/opt/llvm/llvm_build/Debug/bin/clang -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator11.2.sdk -Xclang -load -Xclang ./MyPlugin.dylib -Xclang -add-plugin -Xclang MyPlugin -c ./test.m
複製代碼

注意:

  1. 我編譯出來的 clang 在 /opt/llvm/llvm_build/Debug/bin/clang 目錄中,若是你與個人路徑不同則指定爲你對應的路徑

  2. 在我寫這篇文章時 Xcode 版本是9.2,對應的是 iPhoneSimulator11.2.sdk,你須要進入該目錄查看你的 sdk 版本

如無心外,命令行中會出現一大堆輸出:


Xcode 中使用插件

接下來說怎麼樣在 Xcode 使用咱們剛剛編譯出來的插件(隨着 Xcode 變得封閉,插件掛載到 Xcode 上運行在將來的版本中可能會被禁止)。


首先 hack Xcode,才能使 Xcode 指向咱們本身編譯的 Clang:下載 XcodeHacking.zip 並解壓,裏面有 HackedBuildSystem.xcspec 和 HackedClang.xcplugin 兩個文件,這裏可能須要修改一下 HackedClang.xcplugin/Contents/Resources/HackedClang.xcspec 文件,將 ExecPath 的值修改成你編譯出來的 Clang 的目錄:

而後 cd 到解壓的 XcodeHacking 目錄,將這兩個文件用命令行移動到對應的目錄下:

sudo mv HackedClang.xcplugin `xcode-select -print-path`/../PlugIns/Xcode3Core.ideplugin/Contents/SharedSupport/Developer/Library/Xcode/Plug-ins
sudo mv HackedBuildSystem.xcspec `xcode-select -print-path`/Platforms/iPhoneSimulator.platform/Developer/Library/Xcode/Specifications
複製代碼

而後重啓 Xcode,點擊 Target 的 Build Settings,修改 Compiler for C/C++/Objective-C 項爲 Clang LLVM Trunk(不進行第1步中 hack Xcode 操做的話是不會有這個選項的)

修改 OTHER_CFLAGS 選項:

-Xclang -load -Xclang /Users/Vernon/Desktop/MyPlugin.dylib -Xclang -add-plugin -Xclang MyPlugin
複製代碼

注意

  1. 將 /Users/Vernon/Desktop/MyPlugin.dylib 修改成你生成的插件對應的目錄
  2. 若是編譯中出現一大堆系統庫的 symbol not found 錯誤的話,能夠在上述命令的最後手動指定你的 SDK 目錄:-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator11.2.sdk


最後編譯你的項目,而後快捷鍵 Command+9 跳到 Show the Report navigator,選中剛剛的編譯報告,注意下圖中每一個文件右上角都有能夠點擊展開的按鈕,展開後就能看到咱們插件的輸出了(下圖4爲對應輸出)。Nice~



結語

文章不長,只是這看似簡單的過程也花了我一個多星期的業餘時間,寫下這個系列文章一是爲了記錄本身這鑽研的過程,之後也可查詢,二是但願若是有人能看到這篇拙文能夠省下一點時間,更快的踏進 LLVM 和 Clang 的世界探索。

接下來會根據個人我的需求嘗試給 Clang 添加自定義的 attribute,若是有所心得,會撰文分享,敬請期待~

相關文章
相關標籤/搜索