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 
#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");
            if (propertyDecl) {
                string typeStr = propertyDecl->getType().getAsString();
                cout<<"-------拿到了:"< &arg){
        
        return true;
    }
    
    unique_ptr CreateASTConsumer(CompilerInstance &CI,StringRef InFile){
        return unique_ptr(new WXConsumer(CI));
    }
};

}


//注册插件
static FrontendPluginRegistry::AddWX("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 * _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 
#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");
            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!!"))< &arg){
        
        return true;
    }
    
    unique_ptr CreateASTConsumer(CompilerInstance &CI,StringRef InFile){
        return unique_ptr(new WXConsumer(CI));
    }
};

}


//注册插件
static FrontendPluginRegistry::AddWX("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)

你可能感兴趣的:(iOS 编绎生成 clang 编绎器 + clang 插件开发)