iOS

iOS底层探索之LLVM(三)——自定义Clang插件(下)

2021-09-25  本文已影响0人  俊而不逊

1. 写在前面

上篇博客中已经介绍了LLVM下载流程和LLVM的编译流程,也对编译完成的LLVM工程进行了 ClangclangTooling的编译。

LLVM.png

iOS底层探索之LLVM(二)——自定义Clang插件(上)

iOS底层探索之LLVM(一)——LLVM初体验

本篇博客将手把手教大家,进行代码编写,自定义一个 Clang插件,最终实现的功能是对不正确使用属性修饰会进行报错,并提示正确的用词,实现效果如下。

最终实现的效果

2. 前期准备

2.1 新建插件

/llvm/tools/clang/tools目录下新建插件JPPlugins(这个是你自己建的,名字随便都可以,你自己知道就可以)

新建插件

2.2 修改CMakeLists.txt

修改/llvm/tools/clang/tools目录下的文件CMakeLists.txt,新增加一句add_clang_subdirectory(JPPlugins)

修改CMakeLists.txt

add_llvm_library( JPPlugins MODULE BUILDTREE_ONLY
JPPlugins.cpp
)

2.3 编译插件

新建JPPlugins.cpp和CMakeLists.txt文件 查看是否生成了插件

什么?居然失败了,什么都没有啊!这是在和我开玩笑吗?我仔细一看,是否是 JPPlugins.cpp文件和JPPlugins插件同名导致失败的呢?我于是改了下 cpp的名称,同时把插件名的s去掉了,还真就成功了!这就让我百思不得解了,就很奇怪!

插件生成成功 添加插件然后进行编译
我也不去下什么结论, 反正同名的是可以编译成功的,具体是不是有s的后缀导致的,我也不知道,反正我这两次的成功是和这个有关,时间比较多的老铁可以去验证一下,这里就不再折腾去验证了。

那么我们现在就可以去 cpp里面编写Clang插件的代码了。

3. 编写插件代码

废话不多写,直接上代码,步骤就不一一列出来了,都写在代码里面了。


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

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

       private:
           // CI传递路径:JPASTAction类中的CreateASTConsumer方法参数 -> JPASTConsumer的构造函数 -> JPMatchCallback的私有属性,通过构造函数从JPASTConsumer构造函数中获取
            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:
        // 构造方法
            JPMatchCallback(CompilerInstance &CI):CI(CI) {}

            // 重载run方法
            void run(const MatchFinder::MatchResult &Result) {
                    // 通过Result获取节点对象,根据节点id("objcPropertyDecl")获取(此id需要与JPASTConsumer构造方法中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、自定义JPASTConsumer,继承自抽象类 ASTConsumer,用于监听AST节点的信息 -- 过滤器
    class JPASTConsumer : public ASTConsumer {
        private:
            // AST 节点查找器(过滤器)
            MatchFinder matcher;
            // 回调对象
            JPMatchCallback callback;

        public:
            // 构造方法中创建MatchFinder对象
            JPASTConsumer(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,实现我们自定义的JPASTAction,即自定义AST语法树行为
    class JPASTAction : public PluginASTAction {
        public:

            // 重载ParseArgs 和 CreateASTConsumer方法

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

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

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

3.1 终端测试插件

新建立一个工程,在ViewController.m里面写入如下代码

@interface ViewController ()

@property (nonatomic , strong) NSString *name;
@property (nonatomic , strong) NSArray *array;

@end

“ 自己编译的 clang文件路径 -isysroot 模拟器文件路径 -Xclang -load -Xclang插件路径(.dylib) -Xclang -add-plugin -Xclang 插件名字 -c 源码文件路径 ”

自己编译的 clang文件路径

/Users/RENO/Desktop/TEST/JPDemo/llvm-project/llvm_build/Debug/bin/clang -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk -Xclang -load -Xclang /Users/RENO/Desktop/TEST/JPDemo/llvm-project/llvm_build/Debug/lib/JPPlugin.dylib -Xclang -add-plugin -Xclang JPPlugin -c /Users/RENO/Desktop/TEST/JPDemo/PluginTestDemo/PluginTestDemo/ViewController.m

终端测试插件

3.2 Xcode 集成插件

打开你建立的测试工程,在代码工程的 Build Settings -> Other C Flags 添加上如下的内容:

-Xclang -load -Xclang 插件路径(.dylib) -Xclang -add-plugin -Xclang 插件名字

加载插件

由于Clang插件需要使用对应的版本去加载,如果版本不一致则会导致编译错误,会出现如下图所示:

编译错误 在这里插入图片描述
分别是CC 和CXX CC 和CXX 在这里插入图片描述 在这里插入图片描述

3.3 编译测试插件

编译测试工作
从图中的结果,可以看出,clang的插件完美的运行了。 低调低调.png

4. 总结

5. 写在后面

关注我,更多内容持续输出!

敬请期待!

🌹 喜欢就点个赞吧👍🌹

🌹 觉得有收获的,可以来一波 收藏+关注,以免你下次找不到我😁🌹

🌹欢迎大家留言交流,批评指正,转发请注明出处,谢谢支持!🌹

上一篇下一篇

猜你喜欢

热点阅读