1. 程式人生 > >Clang 之旅--使用 Xcode 開發 Clang 外掛

Clang 之旅--使用 Xcode 開發 Clang 外掛

前言

最近在跟老大的聊天中聊到了一個比較特殊的需求:是否有辦法在編譯階段檢查某個方法的引數與返回值的型別相同,如果型別不一致的話能丟擲編譯錯誤的提示。這似乎已經不是 Objective-C 或者 Swift 的語言語法本身所能解決的了,老大還指點了可以從編譯器等底層中進行研究。於是,我踏進了 Clang 和 LLVM 的大門。

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

llvm大小

編譯原始碼

準備工作

Clang 需要用 CMake 來編譯,CMake 的安裝方法可以參考這篇文章:Mac 安裝 CMake & CMake Command Line Tools,建議對 CMake 完全不瞭解的同學可以先補充一點 CMake 的基本知識,這樣能更容易理解接下來要做的事情,CMake 的入門知識可以參考:CMake 入門實戰

下載原始碼

首先建立 LLVM 的原始碼路徑及編譯路徑:

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_33、release_39,編譯出來的 Clang 外掛在執行的時候都會報 NSUUID 的 Nullability 錯誤,應該是這些版本不支援 Objective-C 後來加的 Nullability 特性,所以我下載了當前最新的 release_60 分支。一般來說,最新分支是相容已有特性的,所以優先下載最新分支,分支檢視可以參照下圖):

git clone -b
release_60 [email protected].com:llvm-mirror/llvm.git llvm git clone -b release_60 [email protected].com:llvm-mirror/clang.git llvm/tools/clang git clone -b release_60 [email protected].com:llvm-mirror/clang-tools-extra.git llvm/tools/clang/tools/extra git clone -b release_60 [email protected].com:llvm-mirror/compiler-rt.git llvm/projects/compiler-rt

llvm最新分支.png

編譯原始碼

生成 Xcode 工程(也可以直接用命令列編譯,不過大家平時可能看習慣了 Xcode 工程,所以用 Xcode 編譯比較習慣)

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

生成的檔案如下:

Xcode工程.png

開啟 Xcode 工程,選擇自動建立 Schemes:

自動建立Schemes.png

然後編譯 Clang 和 libClang(可以隨時終止編譯,再次點選編譯會從上次停止的地方繼續進行):

編譯Clang和libClang

這裡可能需要1個多小時才能完成編譯,如無意外,編譯成功!

編寫你的第一個外掛

這個外掛實現的功能就是列印語法樹上所有節點的類名以及父類名,建立 Clang 外掛的整體步驟如下圖:

建立外掛.png

首先修改原始碼目錄 /opt/llvm/llvm/tools/clang/tools 下的 CMakeLists.txt 檔案,新增一個新的編譯目標,直接在 CMakeLists.txt 的最後面新增上一行,如下圖:
新增新的編譯目標.png

然後在 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()

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.png

編譯成功之後,就可以得到一個 MyPlugin.dylib 的 Clang 外掛了~為了方便,我將 MyPlugin.dylib 放在桌面上:

MyPlugin外掛.png

使用外掛

命令列中使用外掛

首先用命令列對單檔案測試一下剛剛生成的 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 的目錄:
修改HackedClang.xcspec

然後 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 操作的話是不會有這個選項的)

Complier.png

然後修改 OTHER_CFLAGS 選項:
OTHER_CFLAGS.png

-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,如果有所心得,會撰文分享,敬請期待~