开发 clang 插件:0 基础感受底层组

1,683 阅读4分钟

开发 clang 插件, 最常见的就是,属性检查器

本文最后的效果:

截屏2021-08-31 上午12.20.26.png

llvm-project, 直接尚

本文采用 CPP ,开发插件

1, 项目准备

1.1 安装 cmake

brew  install cmake

cmake 可以帮我们,根据脚本,自动创建工程

查看,之前安装过没有

可以用

brew list | grep ^cmake

1.2 获取 llvm 的工程

1.2.1 下载 llvm-project

git clone https://github.com/llvm/llvm-project

使用 Xcode 生成 llvm 项目

进入

cd /Users/jzd/Movies/A_B/llvm-project

等价于

> cd /yourPath/llvm-project

生产

cmake -S llvm -B build -G Xcode -DLLVM_ENABLE_PROJECTS="clang;libcxx;libcxxabi"

需要添加这三个库, clang 、libcxx 、libcxxabi

( 这一步,可能时间有点长 )

得到我们要的工程文件

截屏2021-08-30 下午4.14.15.png

1.2.2, 编译

run 一下

( 这一步,可能时间比较长 )

2, 插件开发

2.1 开发前的准备

2.1.1 做模版配置

进入到这个文件夹

/yourPath/llvm-project/clang/tools

修改文件 CMakeLists.txt

CMakeLists.txt 是 cmake 的配置文件

添加最后一句

add_clang_subdirectory(libclang)
add_clang_subdirectory(amdgpu-arch)

// 添加这一句, 添加插件目录
add_clang_subdirectory(propPlugIn)

创建 /yourPath/llvm-project/clang/tools 下的

文件夹 propPlugIn 文件夹

与 cmake 的配置文件中,添加的语句对应

效果如图

截屏2021-08-30 下午7.49.21.png

可看到,最右端,有两个文件

配置文件 CMakeLists.txt 中写一句:

add_llvm_library( propPlugIn MODULE BUILDTREE_ONLY propertyPlugIn.cpp)

添加 llvm 插件

上面提到的 cpp 文件,为空

2.1.2 配置完成,出结果

重新跑一下

cd /yourPath/llvm-project

cmake -S llvm -B build -G Xcode -DLLVM_ENABLE_PROJECTS="clang;libcxx;libcxxabi"

此时查看

/yourPath/llvm-project 中的工程,长这样

截屏2021-08-30 下午7.50.37.png

阶段性小结: 此时 cmake 暂且放一放, 使用 Xcode 操作

2.2 插件开发中

2.2.1 第一阶段,找出抽象语法树 AST

总是要准备的

导入头文件

#include <iostream>

#include "clang/AST/ASTContext.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;

添加 cpp 命名空间

namespace PropertyPlugIn {

}
编写 clang 插件,主要工作就是,重载 clang 编译过程的函数

就是针对 PluginASTAction 这个类,

  • 第一个方法 ParseArgs ,是套路

  • 第 2 个方法 CreateASTConsumer ,是做事情的

先暂时使用抽象类 ASTConsumer

重载了这两个方法,就可以注册插件了


namespace PropertyPlugIn {

    //继承 PluginASTAction ,实现我们自定义的 Action
    class PropASTAction:public PluginASTAction{

        public:
        bool ParseArgs(const CompilerInstance &CI, const std::vector<std::string> &arg) override{
            return true;
        }
        
        
        // ASTConsumer, 是一个抽象语法树的接口
        
        unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &CI, StringRef InFile) override{
            return unique_ptr<ASTConsumer>(new ASTConsumer);
        }
        
    };
 

}

// 注册插件
static FrontendPluginRegistry::Add<PropertyPlugIn::PropASTAction> PropertyPlugInName("name of PropertyPlugIn","description of PropertyPlugIn");

编译下,插件就出来了

路径是

/yourPath/llvm-project/build/Debug/lib/propPlugIn.dylib

截屏2021-08-30 下午8.38.24.png

PropASTAction 通过 ASTConsumer 的子类,起作用

ASTConsumer 的子类,提供了一个时机

整个文件都解析完成


namespace PropertyPlugIn {
  
    //自定义的 PropConsumer
    class PropConsumer: public ASTConsumer{

    public:

        // 整个文件都解析完成的回调
        void HandleTranslationUnit(ASTContext &Ctx) override {
            // 处理时机
            cout<<"文件解析完毕!"<<endl;
        }
    };


    // 对 class PropASTAction 的修改,见文尾的 github repo
 

}
ASTConsumer , 添加过滤

拿到 AST 后,会得到很多信息,

这里我们只关注属性

添加过滤器,在 PropConsumer 的初始化方法中,建立绑定

//自定义的 PropConsumer
    class PropConsumer: public ASTConsumer{
        
    private:
        //AST节点的查找过滤器!
        MatchFinder filter;
        FilterCallback callback;
        
    public:
        PropConsumer(){
            // 添加过滤器,并指定关注的节点类型
            // 回调在 FilterCallback 里面的 run 方法
            // bind, 是对节点,做一个标记
            // objcPropertyDecl, OC 属性声明,对应我们关心的属性
            // 解析的时候,绑定
            filter.addMatcher(objcPropertyDecl().bind("propObjC"), &callback);
        }

        //整个文件都解析完成的回调!
        void HandleTranslationUnit(ASTContext &Ctx) override {
            // 简单理解就是,解析完成,就把语法树,给过滤器
            filter.matchAST(Ctx);
        }
        
    };
MatchFinder 过滤类的回调类,做事情

回调类,通过重载 run 方法,起作用

class FilterCallback: public MatchFinder::MatchCallback{
        
        void run(const MatchFinder::MatchResult &Result) override {
            // Result 通过 tag 获取到节点
            const ObjCPropertyDecl * propertyDecl = Result.Nodes.getNodeAs<ObjCPropertyDecl>("propObjC");
             // 判断节点有值  
            if (propertyDecl) {
                // 拿到节点的类型
                string propType = propertyDecl->getType().getAsString();
                cout << "属性的类型是 " << propType << endl;
            }
        }
    };
二次过滤,拿到的属性节点中,过滤掉系统的属性
  • 先获取文件的路径,

通过编译器实例 CompilerInstance 的资源管理器 getSourceManager,

判断每一个节点的位置

// 修改后,

// PropConsumer 和 PropASTAction ,两个类的变化,见 github repo
class FilterCallback: public MatchFinder::MatchCallback{
    private:
        CompilerInstance &CI;
        
    public:
        FilterCallback(CompilerInstance &CI):CI(CI){  }
        
        void run(const MatchFinder::MatchResult &Result) override {
            // Result 通过 tag 获取到节点
            const ObjCPropertyDecl * propertyDecl = Result.Nodes.getNodeAs<ObjCPropertyDecl>("propObjC");
            // 文件路径
            string fileName = CI.getSourceManager().getFilename(propertyDecl->getSourceRange().getBegin()).str();
             // 判断节点有值
            if (propertyDecl) {
                // 拿到节点的类型
                string propType = propertyDecl->getType().getAsString();
                cout << fileName << "   的属性的类型是 " << propType << endl;
            }
        }
    
    };


  • 过滤掉系统的
class FilterCallback: public MatchFinder::MatchCallback{
    private:
        CompilerInstance &CI;
        
        bool isCustom(const string fileName){
            if (fileName.empty()) return false;
            // 非 Xcode 中的源码都认为是用户的
            if (fileName.find("/Applications/Xcode.app/") == 0) return false;
            return true;
        }
        
        
    public:
        FilterCallback(CompilerInstance &CI):CI(CI){  }
        
        void run(const MatchFinder::MatchResult &Result) override {
            // Result 通过 tag 获取到节点
            const ObjCPropertyDecl * propertyDecl = Result.Nodes.getNodeAs<ObjCPropertyDecl>("propObjC");
            string fileName = CI.getSourceManager().getFilename(propertyDecl->getSourceRange().getBegin()).str();
            //  判断节点有值, 且为用户文件
            if (propertyDecl && isCustom(fileName)) {
                // 拿到节点的类型
                string propType = propertyDecl->getType().getAsString();
                cout << fileName << "   的属性的类型是 " << propType << endl;
            }
        }
    };

2.2.2 第一阶段, 插件的验证

命令行输入

注意路径的前缀,替换为自个的

/Users/jzd/Movies/A_B/llvm-project/build/Debug/bin/clang        -isysroot         /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.5.sdk     -Xclang  -load -Xclang         /Users/jzd/Movies/A_B/llvm-project/build/Debug/lib/propPlugIn.dylib      -Xclang    -add-plugin     -Xclang   "name of PropertyPlugIn"     -c    /Users/jzd/Movies/A_B/ViewController.m

使用这个路径的 clang

/yourPath/llvm-project/build/Debug/bin/clang

提示: 找到你的 Mac 上 Xcode 的 iPhoneSimulator sdk

open /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/

就可以,方便的查看了

还可以跟系统的 clang 对比

clang       -isysroot         /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.5.sdk    -fmodules   -fsyntax-only -Xclang -ast-dump    /Users/jzd/Movies/A_B/ViewController.m

小结: 从 AST 中,我们过滤出了属性,再过滤掉系统的属性,留下开发者写的属性

接下来,过滤掉,写法正确的属性,留下写法错误的属性

2.2.3 插件的应用,例子为 copy

[NSString, NSArray, NSDictionary] 作为属性,要求使用 copy 修饰

先定位可能的问题
        //   判断是否应该用  copy  修饰
        bool copyApplys(const string typeStr){
            if (typeStr.find("NSString") != string::npos ||
                typeStr.find("NSArray") != string::npos ||
                typeStr.find("NSDictionary") != string::npos) {
                return true;
            }
            return false;
        }
再找出可能的问题
class FilterCallback: public MatchFinder::MatchCallback{
         
        // ...   
        // 其余部分,见 github repo
        
        void run(const MatchFinder::MatchResult &Result) override {
            // Result 通过 tag 获取到节点
            const ObjCPropertyDecl * propertyDecl = Result.Nodes.getNodeAs<ObjCPropertyDecl>("propObjC");
            string fileName = CI.getSourceManager().getFilename(propertyDecl->getSourceRange().getBegin()).str();
            //  判断节点有值, 且为用户文件
            if (propertyDecl && isCustom(fileName)) {
                // 拿到节点的类型
                string propType = propertyDecl->getType().getAsString();
                
                //  拿到节点的描述信息
                ObjCPropertyAttribute::Kind attrKind = propertyDecl->getPropertyAttributes();
                
                if (copyApplys(propType) && !(attrKind & ObjCPropertyAttribute::kind_copy)){
                    // 找出可能的错误
                    cout << propType << " 不 copy, 后果 ...  " << endl;
                }
            }
        }
    };

2.2.4 插件报错

需使用诊断器 DiagnosticsEngine

FilterCallback 类的 run 方法中,


     if (copyApplys(propType) && !(attrKind & ObjCPropertyAttribute::kind_copy)){
            //  诊断器
            DiagnosticsEngine &diag = CI.getDiagnostics();
           //  报告,  Report
           //  第一个参数,警告放在哪个位置
           //  第 2 个参数,警告的标识
           diag.Report(propertyDecl->getBeginLoc(),diag.getCustomDiagID(DiagnosticsEngine::Error, "%0  该属性 ,不 copy,  后果 ..."))<<propType;
     }

效果长这样:

A_B /Users/jzd/Movies/A_B/llvm-project/build/Debug/bin/clang        -isysroot         /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.5.sdk     -Xclang  -load -Xclang         /Users/jzd/Movies/A_B/llvm-project/build/Debug/lib/propPlugIn.dylib      -Xclang    -add-plugin     -Xclang   "name of PropertyPlugIn"     -c    /Users/jzd/Movies/A_B/ViewController.m

/Users/jzd/Movies/A_B/ViewController.m:14:1: error: NSArray *  该属性 ,不 copy,  后果 ...

@property(nonatomic, strong) NSArray* arrs;

^

1 error generated.

2.2.5 把该插件,集成到 Xcode 中

加载自定义的插件代码

需要让当前的 Xcode 工程,走自定义编译出来的插件,与对应的 Clang

  • Other C Flags

把命令行输入的,填入

内容比较多的那一行,是编译出来的 Clang 插件动态库

截屏2021-08-31 上午12.10.25.png

  • 更改参与编译的 Clang 路径

新建两个选项

截屏2021-08-31 上午12.03.12.png

CC 对应 clang

CXX 对应 clang++

截屏2021-08-31 上午12.13.27.png

  • 编译选项,索引选 No

截屏2021-08-31 上午12.14.30.png

3, 原理补充

llvm 分为编译器前端和后端

  • 编译器前端,我们主要使用到 clang 和 swift,生成抽象语法树 AST, 生成 IR

clang 针对 C 语言、CPP、Objective - C

  • 编译器后端,做代码生成的工作,包括 IR -> 汇编 -> 二进制

github repo