iOS 编绎生成 clang 编绎器 + clang 插件开发

最近在研究 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文件底部追加(类似于配置环境变量):

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

如果没有该文件,可以直接创建.bash_profile文件并追加该环境变量。如下图所示


image.png

接着,打开我们的终端Terminal(默认已经是在家目录的路径下,如果没有,切换到家目录下即可)执行下面的命令,让我们刚才配置的环境变量生效:

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:如果没有执行这一步,我们生成的 llvm 项目是没有包含 clang scheme 的。这一点要注意。)

3.4、在终端依次执行以下命令,生成我们的 llvm 项目:
(1)cd llvm-project
(2)mkdir build
(3)cd build
(4)cmake -G Xcode ../llvm
第 4 个命令执行完之后,cmake工具会帮我们在llvm-project目录下的 build 目录下生成包含 clang 和 clangTooling scheme 的llvm Xcode工程。

3.5、在 build 目录下双击打开 llvm 工程,会有如下图所示的提示:


image.png

直接选默认蓝色的第一个:自动创建 schemes即可。

3.6、点击Xcode选择要编绎的项目的位置,会弹出所有的子项目。我们滚动到最后,选择管理我们的schemes。找到 clang scheme 将并它放在比较靠前的位置,这里是为了方便后续可以快速找到它并对它进行编绎。如下图所示:


image.png

image.png

image.png

3.7、编绎我们的 clang 项目。这里要花的时间比较漫长,时间的长短取决于机器的性能。编绎完成后,会生成 clang 可执行文件,我们可以在 llvm-project 目录下的 build 目录下的 Debug 目录下的 bin 目录下找到它。如下图所示:


image.png

到这里,我们已经知道如何编绎生成 clang 文件了。接下来,我们可以开始编写我们的插件,让编译好的 clang 和我们插件结合一起,发挥出一些独特的功能。

四、编写插件代码的准备工作。
传统的编绎流程分为:前端 + 优化器 + 后端。
前端负责源码的解析、词义分析、语法分析(构建抽象语法树),LLVM的前端还会生成中间代码。
优化器负责进行各种优化、改善代码运行时间等。
后端负责将代码映射到各种目标指令集。生成机器语言,并对机器语言进行优化。

4.1、首先,我们在 llvm-project/llvm/clang/tools/ 新建目录WXPlugin,然后在WXPlugin目录下创建两个文件: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目录下执行cmake命令。
4.5、参考步骤 3.5,双击打开Xcode 工程,提示是否自动创建 scheme,选自动创建。
4.6、于是,我们可以在Xcode工程中的 Loadable modules 中找到我们添加的插件。


image.png

4.7、参考步骤 3.6,将 WXPlugin scheme 移动到靠前的位置,方便后续快速找到它并对它进行编绎。
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拿到节点
        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节点的查找过程
    MatchFinder matcher;
    WXMatchCallback callback;
public:
    
    WXConsumer(CompilerInstance &CI):callback(CI){
        //添加一个MatchFinder去匹配objcPropertyDecl节点
        //回调在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 可执行文件和 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-------
……
……

七、根据需求修改我们的插件代码,过滤一些系统节点。这里我们以属性 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拿到节点
            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节点的查找过程
    MatchFinder matcher;
    WXMatchCallback callback;
public:
    
    WXConsumer(CompilerInstance &CI):callback(CI){
        //添加一个MatchFinder去匹配objcPropertyDecl节点
        //回调在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 插件开发的整个流程!

九、你可能会遇到的问题:
9.1、 编绎clang项目的提示 如下图所示:


image.png

这时需要重新走一遍第四步,用 cmake 重新编绎出我们 llvm 项目即可。因为属于增量编绎,所以不会像我们第一次编绎生成 llvm 项目那么久,会很快执行完。

9.2、4.5步骤执行完之后,在工程 Loadable modules 中找不到我们添加的插件。
解决方案参考如下:
(1)检查以下拼写是否有错,建议直接复制,不要手敲:

add_clang_subdirectory(WXPlugin)
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,125评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,293评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,054评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,077评论 1 291
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,096评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,062评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,988评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,817评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,266评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,486评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,646评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,375评论 5 342
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,974评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,621评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,642评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,538评论 2 352

推荐阅读更多精彩内容