iOS底层原理之自定义Clang插件

2021-09-07  本文已影响0人  iOS丶lant

前言

前文主要介绍了下LLVMClang相关的概念、设计思想和编译流程,本篇文章将使用LLVMClang实现一个简单的插件。废话不多说,让我们开始今天的内容吧。

一: LLVM下载

编写Clang插件之前,需要先下载和编译LLVM

由于国内的网络限制,我们需要借助镜像下载LLVM的源码。 mirror.tuna.tsinghua.edu.cn/help/llvm-p…

这里提供两种下载方式,一种是下载整个LLVM(包括各个子仓库,比如clang等等),一种是只下载LLVM,然后根据自己需要再去下载子仓库。

1.1: 下载完整LLVM,包含子仓库(2.78G

git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm-project.git

1.2: 只下载LLVM,不包含子仓库(1.52G

git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm-project/llvm.git

1.2.1: 根据需要下载相应的子仓库

自定义插件需要的子仓库

cd llvm/tools
git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm-project/clang.git

如果想研究lldb的话,需要在LLVMtools目录下下载lldb。自定义插件不需要。

cd llvm/tools
git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm-project/lldb.git
复制代码
cd ../projects
git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm-project/compiler-rt.git
git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm-project/libcxx.git
git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm-project/libcxxabi.git
cd ../tools/clang/tools
git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm-project/clang-tools-extra.git

二: LLVM编译

新版macOS默认的shellzsh,所以,首先进入终端执行:

echo 'export OSX_COMMANDLINE_SDKROOT="/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk"' >> ~/.zshrc

然后再执行:

source ~/.zshrc

由于最新的LLVM只支持cmake来编译了,所以我们还需要安装brewcmake。相关安装方法请移步brew和cmake安装

一些常见的构建系统生成器(generator)有:

作为iOS开发人员,当然首选Xcode来进行编译了。

2.1: 使用Xcode构建LLVM项目

首先使用Xcodegenerator,通过cmakeLLVM编译成Xcode项目。

2.1.1: 完整LLVM编译方法

cd llvm-project                // 进入完整llvm文件夹
mkdir build_xcode              // 新建文件夹build_xcode
cd build_xcode                 // 进入build_xcode
cmake -G <generator> [options] ../llvm // 编译成Xcode项目,具体命令看下面

这里generator我们选择Xcode-DLLVM_ENABLE_PROJECTS就是需要编译的子项目,这里我们需要加上clang,compiler-rt,libcxx,libcxxabi,clang-tools-extra

cmake -G Xcode -DLLVM_ENABLE_PROJECTS='libcxx;libcxxabi;clang;clang-tools-extra;compiler-rt' -DLLDB_USE_SYSTEM_DEBUGSERVER=ON -DLLDB_TEST_COMPILER=clang++ -DCMAKE_OSX_SYSROOT=$OSX_COMMANDLINE_SDKROOT ../llvm

2.1.2: 不完整LLVM编译方法

mkdir build_xcode       // 在llvm所在目录新建文件夹build_xcode
cd build_xcode          // 进入build_xcode
cmake -G Xcode ../llvm  // 编译成Xcode项目

不完整LLVM由于我们已经根据自己的情况下载了子仓库,所以不用添加[options]直接编译就可以了。

开始编译(完整包为例):

大概几分钟后后,检测映射完成。

此时build_xcode目录下大概有67M内容(指定不同[options],大小会有所不同):

2.2: 使用ninja构建LLVM项目(不推荐)

cd llvm_build

// 注意DCMAKE_INSTALL_PREFIX后面不能有空格
cmake -G Ninja ../llvm -DCMAKE_INSTALL_PREFIX= 安装路径(本机为/ Users/xxx/xxx/LLVM/llvm_release)
ninja

ninja install

2.3: 使用Xcode编译Clang

⚠️注意:不要选择Automatically Create Schemes,选择Manually Manage Schemes

否则会引入一些不必要的scheme,拖累Xcode速度。

原则:使用哪个scheme,就引入哪个。

⚠️注意:每次运行时要通过Run Without Building运行。这意味着当你编译一次之后,代码没有改变的情况下,不需要重新编译,直接运行现有可执行文件即可。

三: Clang插件

开始创建插件之前先对要实现的功能做一个简单的介绍:

3.1: 创建插件

llvm-project/llvm/tools/clang/tools目录下新建插件目录XJPlugin:

修改llvm-project/llvm/tools/clang/tools目录(即同目录)下的CMakeLists.txt文件,在最下面新增add_clang_subdirectory(XJPlugin)

XJPlugin目录下新建一个名为XJPlugin.cpp的文件和CMakeLists.txt的文件。在CMakeLists.txt中添加如下代码:

// 通过终端在XJPlugin目录下创建这两个文件
touch CJLPlugin.cpp 
touch CMakeLists.txt

// CMakeLists.txt文件中添加如下代码
add_llvm_library( XJPlugin MODULE BUILDTREE_ONLY
  XJPlugin.cpp
)

接下来使用cmake重新构建一下Xcode项目,终端进入build_xcode目录,运行如下命令:

cmake -G Xcode -DLLVM_ENABLE_PROJECTS='libcxx;libcxxabi;clang;clang-tools-extra;compiler-rt' -DLLDB_USE_SYSTEM_DEBUGSERVER=ON -DLLDB_TEST_COMPILER=clang++ -DCMAKE_OSX_SYSROOT=$OSX_COMMANDLINE_SDKROOT ../llvm

重新进入build_xcode目录打开LLVM.xcodeproj,然后添加XJPluginscheme,并进行编译:

现在在LLVMXcode项目的Loadable modules目录下就可以看到我们的XJPlugin目录了。接下来就在里面编写插件代码。

工程目录非常多,可以全选之后按住command键,鼠标左键点击目录左边的箭头全部折叠,这样就方便找到Loadable modules目录了。

3.2: 编写插件代码

XJPlugin.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 XJPlugin {

    // 第三步:扫描完毕回调
    // 4、自定义回调类,继承自MatchCallback
    class XJMatchCallback : public MatchFinder::MatchCallback {

    private:
        // CI传递路径:XJASTAction类中的CreateASTConsumer方法参数 -> XJASTConsumer的构造函数 -> XJMatchCallback的私有属性,通过构造函数从XJASTConsumer构造函数中获取
        CompilerInstance &CI;

        // 判断是否是自己的文件
        bool isUserSourceCode(const string fileName) {
            // 文件名不为空
            if (fileName.empty()) return false;
            // 非Xcode中的代码都认为是用户的
            if (0 == fileName.find("/Applications/Xcode.app/")) return false;
            return true;
        }

        // 判断是否应该用copy修饰
        bool isShouldUseCopy(const string typeStr) {
            // 判断类型是否是 NSString / NSArray / NSDictionary
            if (typeStr.find("NSString") != string::npos ||
                typeStr.find("NSArray") != string::npos ||
                typeStr.find("NSDictionary") != string::npos) {
                return true;
            }
            return false;
        }

    public:
        // 构造方法
        XJMatchCallback(CompilerInstance &CI):CI(CI) {}

        // 重载run方法
        void run(const MatchFinder::MatchResult &Result) {
            // 通过Result获取节点对象,根据节点id("objcPropertyDecl")获取(此id需要与XJASTConsumer构造方法中bind的id一致)
            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();
                // 应该使用copy,但是没有使用copy
                if (isShouldUseCopy(typeStr) && !(attrKind & ObjCPropertyAttribute::kind_copy)) {
                    // 通过CI获取诊断引擎
                    DiagnosticsEngine &diag = CI.getDiagnostics();
                    // Report 报告
                    /**
                     错误位置:getLocation 节点位置
                     错误:getCustomDiagID(等级,提示)
                     */
                    diag.Report(propertyDecl->getLocation(), diag.getCustomDiagID(DiagnosticsEngine::Warning, "%0 - 这个属性推荐使用copy修饰!!"))<< typeStr;
                }
            }
        }
    };

    // 第二步:扫描配置完毕
    // 3、自定义XJASTConsumer,继承自抽象类 ASTConsumer,用于监听AST节点的信息 -- 过滤器
    class XJASTConsumer : public ASTConsumer {
    private:
        // AST 节点查找器(过滤器)
        MatchFinder matcher;
        // 回调对象
        XJMatchCallback callback;

    public:
        // 构造方法中创建MatchFinder对象
        XJASTConsumer(CompilerInstance &CI):callback(CI) { // 构造即将CI传递给callback
            // 添加一个MatchFinder,每个objcPropertyDecl节点绑定一个objcPropertyDecl标识(去匹配objcPropertyDecl节点)
            // 回调callback,其实是在CJLMatchCallback里面重写run方法(真正回调的是回调run方法)
matcher.addMatcher(objcPropertyDecl().bind("objcPropertyDecl"), &callback);
        }

        // 重载两个方法 HandleTopLevelDecl 和 HandleTranslationUnit

        // 解析完毕一个顶级的声明就回调一次(顶级节点,即全局变量,属性,函数等)
        bool HandleTopLevelDecl(DeclGroupRef D) {
//            cout<<"正在解析..."<<endl;
            return true;
        }

        // 当整个文件都解析完毕后回调
        void HandleTranslationUnit(ASTContext &Ctx) {
//            cout<<"文件解析完毕!!!"<<endl;
            // 将文件解析完毕后的上下文context(即AST语法树) 给 matcher
            matcher.matchAST(Ctx);
        }
    };

    //2、继承PluginASTAction,实现我们自定义的XJASTAction,即自定义AST语法树行为
    class XJASTAction : public PluginASTAction {
    public:

        // 重载ParseArgs 和 CreateASTConsumer方法

        /*
         解析给定的插件命令行参数
         - param CI 编译器实例,用于报告诊断。
         - return 如果解析成功,则为true;否则,插件将被销毁,并且不执行任何操作。该插件负责使用CompilerInstance的Diagnostic对象报告错误。
         */
        bool ParseArgs(const CompilerInstance &CI, const std::vector<std::string> &arg) {
            return true;
        }

        // 返回自定义的XJASTConsumer对象,抽象类ASTConsumer的子类
        unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &CI, StringRef InFile) {
            /**
             传递CI
             CI用于:
             - 判断文件是否是用户的
             - 抛出警告
             */
            return unique_ptr<XJASTConsumer>(new XJASTConsumer(CI));
        }
    };
}

// 第一步:注册插件,并自定义XJASTAction类
// 1、注册插件
static FrontendPluginRegistry::Add<XJPlugin::XJASTAction> X("XJPlugin", "this is XJPlugin");

原理主要分为三步:

通过终端测试插件:

// 命令格式
自己编译的clang文件路径 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.5.sdk(SDK路径)/ -Xclang -load -Xclang 插件(.dyld)路径 -Xclang -add-plugin -Xclang 插件名 -c 源码路径

// 例子
/Users/用户名/llvm-project/build_xcode/Debug/bin/clang -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.5.sdk/ -Xclang -load -Xclang /Users/用户名/llvm-project/build_xcode/Debug/lib/XJPlugin.dylib -Xclang -add-plugin -Xclang XJPlugin -c /Users/用户名/Desktop/DemoCode/PluginTestDemo/PluginTestDemo/ViewController.m

3.3: Xcode集成插件

此插件只作为研究clang之用,实际开发的项目中最好不要继承,因为会影响Xcode编译速度。此插件集成是针对项目,不是针对整个Xcode,测试项目可以放心集成。

3.3.1: 加载插件

 -Xclang -load -Xclang (.dylib)插件动态库路径 -Xclang -add-plugin -Xclang 插件名

如果你正在跳槽或者正准备跳槽不妨动动小手,添加一下咱们的交流群1012951431来获取一份详细的大厂面试资料为你的跳槽多添一份保障。

3.3.2: 设置编译器

总结

关于LLVMClang的研究到此就告一段落了。下篇文章将进入启动优化的探索,敬请期待。

上一篇 下一篇

猜你喜欢

热点阅读