美文网首页
二十六、Clang插件开发

二十六、Clang插件开发

作者: KB_MORE | 来源:发表于2020-11-16 15:34 被阅读0次

1、准备工作

由于国内网络限制,需要借助镜像下载llvm的源码,此处为镜像链接

  • 下载LLVM项目
git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm/llvm.git
  • 在LLVM的projects目录下下载compiler-rt、libcxx、libcxxabi
cd ../projects

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

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

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

Clangtools下安装extra工具

cd ../tools/clang/tools

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

2、LLVM编译

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

安装cmake

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

编译LLVM

有两种编译方式:

- 通过xcode编译LLVM
  • 通过ninja编译LLVM
- 通过xcode编译LLVM
  • cmake编译成Xcode项目
mkdir build_xcode

cd build_xcode

cmake -G Xcode ../llvm

cmake 的时候报错, 很多原因就是因为网络下载不顺畅,多次中断导致, 可一步一步下载上面呢的内容, 保证每步正常进行, 我第一次下载中断过几次,cmake报错,找了多种方案都没解决, 最后删除所有以上文件进行再次下载,最后camke成功

  • 使用xcode编译Clang

    • 选择自动创建Schemes
      图片.png
  • 编译(CMD + B),选择ALL_BUILD Secheme进行编译,预计1+小时

image

如果编译报错,可以尝试方案:

选择手动创建Schemes,然后编译编译Clang + ClangTooling即可

- 通过ninja编译LLVM
  • 使用ninja进行编译则还需要安装ninja,使用以下命令安装ninja
brew install ninja
  • 在LLVM源码根目录下新建一个build_ninja目录,最终会在build_ninja目录下生成build.ninja`

在LLVM源码根目录下新建llvm_release目录,最终编译文件会在llvm_release文件夹路径下

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

  • 一次执行编译,安装指令
ninja

ninja install

3、创建插件

  • /llvm/tools/clang/tools下新建插件CJLPlugin

    image
  • /llvm/tools/clang/tools目录下的CMakeLists.txt文件,新增add_clang_subdirectory(CJLPlugin),此处的CJLPlugin即为上一步创建的插件名称

    image
  • CJLPlugin目录下新建两个文件,分别是CJLPlugi.cppCMakeLists.txt,并在CMakeLists.txt中加上以下代码

//1、通过终端在CJLPlugin目录下创建
touch CJLPlugin.cpp

touch CMakeLists.txt

//2、CMakeLists.txt中添加以下代码
add_llvm_library( CJLPlugin MODULE BUILDTREE_ONLY 
    CJLPlugin.cpp
)

图片.png
  • 接下来利用cmake重新生成Xcode项目,在build_xcode目录下执行以下命令
cmake -G Xcode ../llvm
  • 最后可以在LLVM的xcode项目中可以看到Loadable modules目录下由自定义的CJLPlugin目录了,然后可以在里面编写插件代码了

    image

编写插件代码

CJLPlugin目录下的CJLPlugin.cpp文件中,加入以下代码

// create by CJL
// 2020/11/15

#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 CJLPlugin {

//第三步:扫描完毕的回调函数
//4、自定义回调类,继承自MatchCallback
class CJLMatchCallback: public MatchFinder::MatchCallback {
    
private:
    //CI传递路径:CJLASTAction类中的CreateASTConsumer方法参数 - CJLConsumer的构造函数 - CJLMatchCallback的私有属性,通过构造函数从CJLASTConsumer构造函数中获取
    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:
    CJLMatchCallback(CompilerInstance &CI) :CI(CI) {}
    
    //重写run方法
    void run(const MatchFinder::MatchResult &Result) {
        //通过result获取到相关节点 -- 根据节点标记获取(标记需要与CJLASTConsumer构造方法中一致)
        const ObjCPropertyDecl *propertyDecl = Result.Nodes.getNodeAs<ObjCPropertyDecl>("objcPropertyDecl");
        //判断节点有值,并且是用户文件
        if (propertyDecl && isUserSourceCode(CI.getSourceManager().getFilename(propertyDecl->getSourceRange().getBegin()).str()) ) {
            //15、获取节点的描述信息
            ObjCPropertyDecl::PropertyAttributeKind attrKind = propertyDecl->getPropertyAttributes();
            //获取节点的类型,并转成字符串
            string typeStr = propertyDecl->getType().getAsString();
//            cout<<"---------拿到了:"<<typeStr<<"---------"<<endl;
            
            //判断应该使用copy,但是没有使用copy
            if (propertyDecl->getTypeSourceInfo() && isShouldUseCopy(typeStr) && !(attrKind & ObjCPropertyDecl::OBJC_PR_copy)) {
                //使用CI发警告信息
                //通过CI获取诊断引擎
                DiagnosticsEngine &diag = CI.getDiagnostics();
                //通过诊断引擎 report报告 错误,即抛出异常
                /*
                错误位置:getBeginLoc 节点开始位置
                错误:getCustomDiagID(等级,提示)
                 */
                diag.Report(propertyDecl->getBeginLoc(), diag.getCustomDiagID(DiagnosticsEngine::Warning, "%0 - 这个地方推荐使用copy!!"))<< typeStr;
            }
        }
    }
};


//第二步:扫描配置完毕
//3、自定义CJLASTConsumer,继承自ASTConsumer,用于监听AST节点的信息 -- 过滤器
class CJLASTConsumer: public ASTConsumer {
private:
    //AST节点的查找过滤器
    MatchFinder matcher;
    //定义回调类对象
    CJLMatchCallback callback;
    
public:
    //构造方法中创建matcherFinder对象
    CJLASTConsumer(CompilerInstance &CI) : callback(CI) {
        //添加一个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 &context) {
//        cout<<"文件解析完毕!"<<endl;
        //将文件解析完毕后的上下文context(即AST语法树) 给 matcher
        matcher.matchAST(context);
    }
};

//2、继承PluginASTAction,实现我们自定义的Action,即自定义AST语法树行为
class CJLASTAction: 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) {
        //返回自定义的CJLASTConsumer,即ASTConsumer的子类对象
        /*
         CI用于:
         - 判断文件是否使用户的
         - 抛出警告
         */
        return unique_ptr<CJLASTConsumer> (new CJLASTConsumer(CI));
    }
    
};

}

//第一步:注册插件,并自定义AST语法树Action类
//1、注册插件
static FrontendPluginRegistry::Add<CJLPlugin::CJLASTAction> CJL("CJLPlugin", "This is CJLPlugin");

其原理主要分为三步

  • 【第一步】注册插件,并自定义AST语法树Action类
    • 继承自PluginASTAction,自定义ASTAction,需要重载两个方法ParseArgsCreateASTConsumer,其中的重点方法是CreateASTConsumer,方法中有个参数CI即编译实例对象,主要用于以下两个方面

      • 用于判断文件是否是用户的

      • 用于抛出警告

    • 通过FrontendPluginRegistry注册插件,需要关联插件名与自定义的ASTAction类

  • 【第二步】扫描配置完毕
    • 继承自ASTConsumer类,实现自定义的子类CJLASTConsumer,有两个参数MatchFinder对象matcher以及CJLMatchCallback自定义的回调对象callback

    • 实现构造函数,主要是创建MatchFinder对象,以及将CI床底给回调对象

    • 实现两个回调方法

      • HandleTopLevelDecl:解析完一个顶级的声明,就回调一次
      • HandleTranslationUnit:整个文件都解析完成的回调,将文件解析完毕后的上下文context(即AST语法树) 给 matcher
  • 【第三步】扫描完毕的回调函数
    • 继承自MatchFinder::MatchCallback,自定义回调类CJLMatchCallback

    • 定义CompilerInstance私有属性,用于接收ASTConsumer类传递过来的CI信息

    • 重写run方法

      • 1、通过result,根据节点标记,获取相应节点,此时的标记需要与CJLASTConsumer构造方法中一致

      • 2、判断节点有值,并且是用户文件即isUserSourceCode私有方法

      • 3、获取节点的描述信息

      • 4、获取节点的类型,并转成字符串

      • 5、判断应该使用copy,但是没有使用copy

      • 6、通过CI获取诊断引擎

      • 7、通过诊断引擎报告错误

所以,综上所述,clang插件开发的流程图如下

image

然后在终端中测试插件

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

//例子
/Users/XXX/Desktop/build_xcode/Debug/bin/clang -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.1.sdk/ -Xclang -load -Xclang /Users/XXXX/Desktop/build_xcode/Debug/lib/CJLPlugin.dylib -Xclang -add-plugin -Xclang CJLPlugin -c /Users/XXXX/Desktop/XXX/XXXX/测试demo/testClang/testClang/ViewController.m
图片.png

4、Xcode集成插件
加载插件

打开测试项目,在target->Build Settings -> Other C Flags 添加以下内容
 -Xclang -load -Xclang (.dylib)动态库路径 -Xclang -add-plugin -Xclang CJLPlugin
图片.png

设置编译器

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

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

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

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

image
  • 接下来在Build Settings中搜索index,将Enable Index-Wihle-Building FunctionalityDefault改为NO

    image
  • 最后,重新编译测试项目,会出现下面的效果

    image

作者:Style_月月
链接:https://www.jianshu.com/p/4f18226705ec
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

相关文章

网友评论

      本文标题:二十六、Clang插件开发

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