iOS之武功秘籍⑰: Clang插件开发

时间:2021-7-21 作者:qvyue

iOS之武功秘籍 文章汇总

写在前面

上篇我们介绍了LLVM的编译流程,接下来我们就来玩玩怎么做插件吧…..

本节可能用到的秘籍Demo

一、配置LLVM环境

特别提醒:

  • 1.LLVM源码大2.29G编译后文件将近30G,所以请确保电脑硬盘空间足够
  • 2.编译时,电脑温度会飙升90多度CPU资源占满,请用空调伺候着,有可能会黑屏;
  • 3.编译时间长达1个多小时,请合理安排时间,可以先洗澡什么的…

如果以上3点,你确定能接受,那我们就开始吧.

① LLVM下载

在github下载LLVM相关资源库:

  • clang、clang-tools-extra、compiler-rt、libcxx、libcxxabi、llvm五个库:

    iOS之武功秘籍⑰: Clang插件开发
  • 解压并移除名称中的版本号

    iOS之武功秘籍⑰: Clang插件开发
  • 按以下顺序将文件夹移到指定位置:
  • clang-tools-extra移到clang文件夹中的clang/tools文件中
  • clang文件夹移到llvm/tools
  • compiler-rt、libcxx、libcxxabi都移到llvm/projects
    iOS之武功秘籍⑰: Clang插件开发
    iOS之武功秘籍⑰: Clang插件开发

② LLVM编译

由于最新的LLVM只支持cmake来编译,所以需要安装cmake

②.1 安装cmake

  • 查看brew是否安装cmake,如果已经安装,则跳过下面步骤 — brew list
  • 通过brew安装cmakebrew install cmake

②.2 编译LLVM

有两种编译方式:

  • 通过Xcode编译LLVM
  • 通过ninja编译LLVM
②.2.1 通过xcode编译LLVM
  • llvm同级目录创建build文件夹,cdbuild文件夹,运行cmake命令,将llvm编译成Xcode项目

    cd build
    cmake -G Xcode ../llvm   
    // 或者: cmake -G Xcode CMAKE_BUILD_TYPE="Release" ../llvm
    // 或者: cmake -G Xcode CMAKE_BUILD_TYPE="debug" ../llvm 
    

注意:

  • build文件夹是存放cmake生成的Xcode文件的.放哪里都可以.
  • cmake编译的对象是llvm文件.所以使用cmake -G Xcode ../llvm编译并生成Xcode文件时,请核对llvm的文件路径.
  • 成功之后,可以看到生成的Xcode文件:
    iOS之武功秘籍⑰: Clang插件开发
  • 使用Xcode打开LLVM.xcodeproj
    • 选择手动创建Schemes

      iOS之武功秘籍⑰: Clang插件开发
* 添加`clang`和`clangTooling`两个`Target`,并完成两个`target`的编译[图片上传失败...(image-b63796-1614944397110)]
* 编译成功后,我们的准备工作就完成了.可以正式开始插件开发了
②.2.2 通过ninja编译LLVM
  • llvm同级目录创建build文件夹
  • 使用ninja进行编译则还需要安装ninja,使用brew install ninja命令安装ninja
  • llvm源码根目录下新建一个build_ninja目录,最终会在build_ninja目录下生成build.ninja
  • llvm源码根目录下新建llvm_release目录,最终编译文件会在llvm_release文件夹路径下
cd build

//注意DCMAKE_INSTALL_PREFIX后面不能有空格
cmake -G Ninja ../llvm -DCMAKE_INSTALL_PREFIX= 安装路径(本机为/ Users/xxx/xxx/LLVM/llvm_release)
  • 依次执行编译,安装指令
ninja

ninja install

小编这里选择的是用Xcode编译的.

二、自定义插件

  • llvm/tools/clang/tools文件夹中,创建CJPlugin文件夹,即插件名称
    iOS之武功秘籍⑰: Clang插件开发
  • /llvm/tools/clang/tools目录下的CMakeLists.txt文件,新增add_clang_subdirectory(CJPlugin),此处的CJPlugin即为上一步创建的插件名称
    iOS之武功秘籍⑰: Clang插件开发
  • CJPlugin目录下新建两个文件,分别是CJPlugi.cppCMakeLists.txt,并在CMakeLists.txt中加上以下代码
    //1、通过终端在CJPlugin目录下创建
    touch CJPlugin.cpp
    
    touch CMakeLists.txt
    
    //2、CMakeLists.txt中添加以下代码
    add_llvm_library( CJPlugin MODULE BUILDTREE_ONLY 
        CJPlugin.cpp
    )
    
iOS之武功秘籍⑰: Clang插件开发
  • 接下来利用cmake重新生成Xcode项目,在build目录下执行cmake -G Xcode ../llvm命令
  • 最后可以在LLVMXcode项目中可以看到Loadable modules目录下由自定义的CJPlugin目录了,然后可以在里面编写插件代码了
    iOS之武功秘籍⑰: Clang插件开发
  • Manage Schemes添加我们的CJPlugin

    iOS之武功秘籍⑰: Clang插件开发
  • CJPlugin目录下的CJPlugin.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 CJPlugin {
    
    //第三步:扫描完毕的回调函数
    //4、自定义回调类,继承自MatchCallback
    class CJMatchCallback: public MatchFinder::MatchCallback {
        
    private:
        //CI传递路径:CJASTAction类中的CreateASTConsumer方法参数 - CJConsumer的构造函数 - CJMatchCallback的私有属性,通过构造函数从CJASTConsumer构造函数中获取
        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) {
            //判断类型是否是NSString | NSArray | NSDictionary
            if (typeStr.find("NSString") != string::npos ||
                typeStr.find("NSArray") != string::npos ||
                typeStr.find("NSDictionary") != string::npos/*...*/)
            {
                return true;
            }
            
            return false;
        }
        
    public:
        CJMatchCallback(CompilerInstance &CI):CI(CI){}
        
        //重写run方法
        void run(const MatchFinder::MatchResult &Result) {
            //通过result获取到相关节点 -- 根据节点标记获取(标记需要与CJASTConsumer构造方法中一致)
            const ObjCPropertyDecl *propertyDecl = Result.Nodes.getNodeAs("objcPropertyDecl");
            //判断节点有值,并且是用户文件
            if (propertyDecl && isUserSourceCode(CI.getSourceManager().getFilename(propertyDecl->getSourceRange().getBegin()).str()) ) {
                //15、获取节点的描述信息
                ObjCPropertyAttribute::Kind attrKind = propertyDecl->getPropertyAttributes();
                //获取节点的类型,并转成字符串
                string typeStr = propertyDecl->getType().getAsString();
    //            coutgetTypeSourceInfo() && isShouldUseCopy(typeStr) && !(attrKind & ObjCPropertyAttribute::kind_copy)) {
                    //使用CI发警告信息
                    //通过CI获取诊断引擎
                    DiagnosticsEngine &diag = CI.getDiagnostics();
                    //通过诊断引擎 report报告 错误,即抛出异常
                    /*
                    错误位置:getBeginLoc 节点开始位置
                    错误:getCustomDiagID(等级,提示)
                     */
                    diag.Report(propertyDecl->getBeginLoc(), diag.getCustomDiagID(DiagnosticsEngine::Warning, "%0 - 这个地方推荐使用copy!!")) &args) {
            return true;
        }
        
        //返回ASTConsumer类型对象,其中ASTConsumer是一个抽象类,即基类
        /*
         解析给定的插件命令行参数。
         - param CI 编译器实例,用于报告诊断。
         - return 如果解析成功,则为true;否则,插件将被销毁,并且不执行任何操作。该插件负责使用CompilerInstance的Diagnostic对象报告错误。
         */
        unique_ptr CreateASTConsumer(CompilerInstance &CI, StringRef iFile) {
            //返回自定义的CJASTConsumer,即ASTConsumer的子类对象
            /*
             CI用于:
             - 判断文件是否使用户的
             - 抛出警告
             */
            return unique_ptr (new CJASTConsumer(CI));
        }
        
    };
    
    }
    
    //第一步:注册插件,并自定义AST语法树Action类
    //1、注册插件
    static FrontendPluginRegistry::Add CJ("CJPlugin", "This is CJPlugin");
    
    

其原理主要分为三步

  • 【第一步】注册插件,并自定义AST语法树Action
    • 继承自PluginASTAction,自定义ASTAction,需要重载两个方法ParseArgsCreateASTConsumer,其中的重点方法是CreateASTConsumer,方法中有个参数CI即编译实例对象,主要用于以下两个方面
      • 用于判断文件是否是用户的
      • 用于抛出警告
    • 通过FrontendPluginRegistry注册插件,需要关联插件名与自定义的ASTAction
  • 【第二步】扫描配置完毕
    • 继承自ASTConsumer类,实现自定义的子类CJASTConsumer,有两个参数MatchFinder对象matcher以及CJMatchCallback自定义的回调对象callback
    • 实现构造函数,主要是创建MatchFinder对象,以及将CI床底给回调对象
    • 实现两个回调方法
      • HandleTopLevelDecl:解析完一个顶级的声明,就回调一次
      • HandleTranslationUnit:整个文件都解析完成的回调,将文件解析完毕后的上下文context(即AST语法树) 给 matcher
  • 【第三步】扫描完毕的回调函数
    • 继承自MatchFinder::MatchCallback,自定义回调类CJMatchCallback
    • 定义CompilerInstance私有属性,用于接收ASTConsumer类传递过来的CI信息
    • 重写run方法
      • 1、通过result,根据节点标记,获取相应节点,此时的标记需要与CJASTConsumer构造方法中一致
      • 2、判断节点有值,并且是用户文件即isUserSourceCode私有方法
      • 3、获取节点的描述信息
      • 4、获取节点的类型,并转成字符串
      • 5、判断应该使用copy,但是没有使用copy
      • 6、通过CI获取诊断引擎
      • 7、通过诊断引擎报告错误

嘿嘿,然后在终端中测试插件
llvm的同级目录创建我们的ClangDemo.cd到ClangDemo`文件夹执行下面指令

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

//例子
/Users/changjiang/Desktop/iOS之武功秘籍 ⑰:Clang插件开发/build/Debug/bin/clang -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.4.sdk/ -Xclang -load -Xclang /Users/changjiang/Desktop/iOS之武功秘籍 ⑰:Clang插件开发/build/Debug/lib/CJPlugin.dylib -Xclang -add-plugin -Xclang CJPlugin -c /Users/changjiang/Desktop/iOS之武功秘籍 ⑰:Clang插件开发/ClangDemo/ClangDemo/ViewController.m
iOS之武功秘籍⑰: Clang插件开发

三、Xcode集成插件

① 加载插件

打开测试项目,在target->Build Settings -> Other C Flags 添加以下内容

 -Xclang -load -Xclang /Users/changjiang/Desktop/iOS之武功秘籍 ⑰:Clang插件开发/build/Debug/lib/CJPlugin.dylib -Xclang -add-plugin -Xclang CJPlugin

/Users/changjiang/Desktop/iOS之武功秘籍 ⑰:Clang插件开发/build/Debug/lib/CJPlugin.dylib是自己的CJPlugin.dylib绝对路径

iOS之武功秘籍⑰: Clang插件开发

② 设置编译器

接着Command + B编译,报错

由于clang插件需要使用对应的版本去加载,如果版本不一致会导致编译失败,如下所示

iOS之武功秘籍⑰: Clang插件开发
  • Build Settings栏目新增两项用户定义的设置分别是CCCXX

    iOS之武功秘籍⑰: Clang插件开发
  • CC 对应的是自己编译的clang的绝对路径

  • CXX 对应的是自己编译的clang++的绝对路径

    iOS之武功秘籍⑰: Clang插件开发
  • 接下来在Build Settings中搜索index,将Enable Index-Wihle-Building FunctionalityDefault改为NO
    iOS之武功秘籍⑰: Clang插件开发
  • 最后,重新编译测试项目,会出现下面的效果

    iOS之武功秘籍⑰: Clang插件开发
  • 修改name的修饰符为copyCommand+B编译后看,name已经不报错了
    iOS之武功秘籍⑰: Clang插件开发
  • 恭喜你… 成功了!

写在后面

通过这个本篇的小插件,应该对语法树、编译流程,有了更深刻的认识吧…

和谐学习,不急不躁.我还是我,颜色不一样的烟火.

声明:本文内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:qvyue@qq.com 进行举报,并提供相关证据,工作人员会在5个工作日内联系你,一经查实,本站将立刻删除涉嫌侵权内容。