美文网首页
iOS之武功秘籍⑰: Clang插件开发

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

作者: 長茳 | 来源:发表于2021-03-05 19:41 被阅读0次

    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五个库:
    • 解压并移除名称中的版本号
    • 按以下顺序将文件夹移到指定位置:
    • clang-tools-extra移到clang文件夹中的clang/tools文件中
    • clang文件夹移到llvm/tools
    • compiler-rt、libcxx、libcxxabi都移到llvm/projects

    ② LLVM编译

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

    ②.1 安装cmake

    • 查看brew是否安装cmake,如果已经安装,则跳过下面步骤 -- brew list
    • 通过brew安装cmake -- brew 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文件:
    • 使用Xcode打开LLVM.xcodeproj
      • 选择手动创建Schemes
    * 添加`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文件夹,即插件名称
    • /llvm/tools/clang/tools目录下的CMakeLists.txt文件,新增add_clang_subdirectory(CJPlugin),此处的CJPlugin即为上一步创建的插件名称
    • 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
      )
      
    • 接下来利用cmake重新生成Xcode项目,在build目录下执行cmake -G Xcode ../llvm命令
    • 最后可以在LLVMXcode项目中可以看到Loadable modules目录下由自定义的CJPlugin目录了,然后可以在里面编写插件代码了
    • Manage Schemes添加我们的CJPlugin
    • CJPlugin目录下的CJPlugin.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 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>("objcPropertyDecl");
              //判断节点有值,并且是用户文件
              if (propertyDecl && isUserSourceCode(CI.getSourceManager().getFilename(propertyDecl->getSourceRange().getBegin()).str()) ) {
                  //15、获取节点的描述信息
                  ObjCPropertyAttribute::Kind attrKind = propertyDecl->getPropertyAttributes();
                  //获取节点的类型,并转成字符串
                  string typeStr = propertyDecl->getType().getAsString();
      //            cout<<"---------拿到了:"<<typeStr<<"---------"<<endl;
                  
                  //判断应该使用copy,但是没有使用copy
                  if (propertyDecl->getTypeSourceInfo() && 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!!"))<< typeStr;
                  }
              }
          }
      };
      
      
      //第二步:扫描配置完毕
      //3、自定义CJASTConsumer,继承自ASTConsumer,用于监听AST节点的信息 -- 过滤器
      class CJASTConsumer: public ASTConsumer {
      private:
          //AST节点的查找过滤器
          MatchFinder matcher;
          //定义回调类对象
          CJMatchCallback callback;
          
      public:
          //构造方法中创建matcherFinder对象
          CJASTConsumer(CompilerInstance &CI) : callback(CI) {
              //添加一个MatchFinder,每个objcPropertyDecl节点绑定一个objcPropertyDecl标识(去匹配objcPropertyDecl节点)
              //回调callback,其实是在CJMatchCallback里面重写run方法(真正回调的是回调run方法)
              matcher.addMatcher(objcPropertyDecl().bind("objcPropertyDecl"), &callback);
          }
          
          //实现两个回调方法 HandleTopLevelDecl 和 HandleTranslationUnit
          //解析完一个顶级的声明,就回调一次(顶级节点,相当于一个全局变量、函数声明)
          bool HandleTopLevelDecl(DeclGroupRef D){
      //        cout<<"正在解析..."<<endl;
              return  true;
          }
          
          //整个文件都解析完成的回调
          void HandleTranslationUnit(ASTContext &context) {
      //        cout<<"文件解析完毕!"<<endl;
              //将文件解析完毕后的上下文context(即AST语法树) 给 matcher
              matcher.matchAST(context);
          }
      };
      
      //2、继承PluginASTAction,实现我们自定义的Action,即自定义AST语法树行为
      class CJASTAction: public PluginASTAction {
          
      public:
          //重载ParseArgs 和 CreateASTConsumer方法
          bool ParseArgs(const CompilerInstance &ci, const std::vector<std::string> &args) {
              return true;
          }
          
          //返回ASTConsumer类型对象,其中ASTConsumer是一个抽象类,即基类
          /*
           解析给定的插件命令行参数。
           - param CI 编译器实例,用于报告诊断。
           - return 如果解析成功,则为true;否则,插件将被销毁,并且不执行任何操作。该插件负责使用CompilerInstance的Diagnostic对象报告错误。
           */
          unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &CI, StringRef iFile) {
              //返回自定义的CJASTConsumer,即ASTConsumer的子类对象
              /*
               CI用于:
               - 判断文件是否使用户的
               - 抛出警告
               */
              return unique_ptr<CJASTConsumer> (new CJASTConsumer(CI));
          }
          
      };
      
      }
      
      //第一步:注册插件,并自定义AST语法树Action类
      //1、注册插件
      static FrontendPluginRegistry::Add<CJPlugin::CJASTAction> 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
    

    三、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绝对路径

    ② 设置编译器

    接着Command + B编译,报错

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

    • Build Settings栏目新增两项用户定义的设置分别是CCCXX

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

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

    • 接下来在Build Settings中搜索index,将Enable Index-Wihle-Building FunctionalityDefault改为NO
    • 最后,重新编译测试项目,会出现下面的效果
    • 修改name的修饰符为copyCommand+B编译后看,name已经不报错了
    • 恭喜你... 成功了!

    写在后面

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

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

    相关文章

      网友评论

          本文标题:iOS之武功秘籍⑰: Clang插件开发

          本文链接:https://www.haomeiwen.com/subject/ndrdqltx.html