OC底层原理探索之LLVM下

1,327 阅读5分钟

目标

访问代码的时候,如果发现了不标准的修饰,需要提示、警告

编写插件代码上

上一篇我们已经创建了一个插件的模块,需要写.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 ast_matchers;

我们写插件的话,可以查找下clang官方提供的一些插件开发的API。clang本身就是编译器的前端,我们需要在它读代码的时候做一些操作,需要在它提供给我们的一些回调方法或者我们自己重载实现的方法里面去做。所以也就是一个二次开发的过程。 ​

PluginASTAction抽象语法树

  1. 定义一个类 继承PluginASTAction,因为读取的是抽象语法树,所以自定义处理
namespace HKPlugin {
    class HKASTAction:public PluginASTAction {}
}
  1. 注册插件:
static FrontendPluginRegistry::Add<HKPlugin::HKASTAction> 
X("HKPlugin","this is the description"); // 左边是插件的名称,右边是插件的描述
  1. 读取AST需要重载两个函数,所以需要在HKASTAction重载下面两个函数
  std::unique_ptr<ASTConsumer> CreateASTConsumer(
      CompilerInstance &CI, StringRef InFile
bool ParseArgs(const CompilerInstance &CI, const vector<string> &arg)
  1. 3中的ASTConsumer是一个基类,我们需要自己实现,这个就是读取AST的关键。创建一个类HKConsumer继承于ASTConsumer
class HKConsumer:public ASTConsumer{}
  1. 重点实现的类就是HKConsumer,而这个又有两个函数需要实现
bool HandleTopLevelDecl(DeclGroupRef D) 
// 解析完毕一个顶级的声明就回调一次 比如全局变量、函数定义以及属性
void HandleTranslationUnit(ASTContext &Ctx) //整个文件都解析完成之后回调

BULID之后,我们show in finder找到这个文件 Debug -> bin -> clang :编译完的二进制 Debug -> lib -> HKPlugin: 自定义插件的路径 image.png 现在我们使用这个来编译我们自己的代码 ​

测试插件

自己编译的clang文件路径也就是上面的橙色标注的路径 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator12.sdk/ -Xclang -load -Xclang 插件路径也就是上面粉色标注的路径 Xclang -add-plugin -Xclang 插件名 -c 源码路径

思路分析

目的:当用Strong修饰NSString属性的时候,希望自制的插件提示使用copy修饰 疑问:那么需要知道在词法分析的哪一个节点. 所以需要定位到词法语法分析那个编译阶段, 我们在VC中写入两个属性然后用系统自带的clang查看编译流程

@interface ViewController ()
@property(nonatomic, strong) NSString* name;
@property(nonatomic, strong) NSArray* arrs;
@end

clang -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.5.sdk -fmodules -fsyntax-only -Xclang -ast-dump ViewController.m image.png 我们发现属性跟ObjCPropertyDecl这个关键词有关。 ​

编写插件代码下

tips: MatchFinder AST语法树节点查找;MatchCallback找到回调

  1. AST节点查找 过滤器
    class HKConsumer:public ASTConsumer{
    private:
        //MatchFinder  AST 节点的过滤器
        MatchFinder matcher;
    }
  1. 重写HKConsumer的构造方法
HKConsumer(){
//添加一个MatchFinder去匹配ObjCPropertyDecl节点
//回调!
	matcher.addMatcher(objcPropertyDecl().bind("objcPropertyDecl"), &callback);
}

第一个参数是需要去匹配的节点,第二个参数是找到这个节点的回调。这个回调是一个基类

class HKMatchCallback: public  MatchFinder::MatchCallback
  1. 重写run方法
void run(const MatchFinder::MatchResult &Result)
  1. 在run的结果获取到节点对象,通过getType().getAsString()拿到属性节点的字符串
  2. 但是由于节点字符串太多,需要过滤掉系统的。所以我们需要知道当前所有的节点的路径
CI.getSourceManager().getFilename(propertyDecl->getSourceRange().getBegin()).str()

image.png

//判断是否是自己的文件
 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){
      if(typeStr.find("NSString") != string::npos ||
         typeStr.find("NSArray") != string::npos ||
         typeStr.find("NSDictionary") != string::npos){
      	 	return true;
       }
       return false;
}
  1. 获取当前节点的描述信息跟Copy做对比

image.png 应该使用copy但是没有使用copy的判断是,

isShouldUseCopy(typeStr) && !(attrKind & ObjCPropertyDecl::OBJC_PR_copy)

这种情况需要发出警告

  1. 拿到编译器的诊断引擎,发出警告
DiagnosticsEngine &diag = CI.getDiagnostics();
diag.Report(propertyDecl->getLocation(),diag.getCustomDiagID(DiagnosticsEngine::Error, "这个地方应该用Copy"));

image.png

集成到Xcode

Build Settings搜索Other C Flags添加 image.png

-Xclang -load -Xclang xxx.dylib -Xclang -add-plugin -Xclang xxx

xxx.dylib:动态库路径 ​

由于Clang插件需要使用对应的版本去加载,如果版本不一致会导致编译错误,会出现以下提示:Expected in: flat namespace,此时需要在设置里新增用户定义

image.png

CC:对应自己编译的clang的绝对路径
CXX:对应自己编译的clang++的绝对路径 image.png

同时搜索index,将Enable index-while-buliding-Functionality修改为NO image.png

运行,最终效果如下。 image.png

完整代码

#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 ast_matchers;


namespace HKPlugin {
    class HKMatchCallback: public  MatchFinder::MatchCallback{
    private:
        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){
            if(typeStr.find("NSString") != string::npos ||
               typeStr.find("NSArray") != string::npos ||
               typeStr.find("NSDictionary") != string::npos){
                return true;
            }
            return false;
        }
        
    public:
        HKMatchCallback(CompilerInstance &CI):CI(CI){}
        void run(const MatchFinder::MatchResult &Result) {
            //通过结果获取到节点对象
           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();
                //拿到节点的描述信息
                ObjCPropertyDecl::PropertyAttributeKind attrKind = propertyDecl->getPropertyAttributes();
                if (isShouldUseCopy(typeStr) && !(attrKind & ObjCPropertyDecl::OBJC_PR_copy)) {//应该使用copy但是没有使用Copy
                    //诊断引擎
                    DiagnosticsEngine &diag = CI.getDiagnostics();
                    //Report 报告
                    diag.Report(propertyDecl->getLocation(),diag.getCustomDiagID(DiagnosticsEngine::Error, "这个地方应该用Copy"));
//                    cout<<typeStr<<"应该使用copy修饰但是没有用!发出警告!!"<<endl;
                }
                
               
            }
        }
    };
    

    //自定义的HKConsumer
    class HKConsumer:public ASTConsumer{
    private:
        //MatchFinder  AST 节点的过滤器
        MatchFinder matcher;
        HKMatchCallback callback;
    public:
        HKConsumer(CompilerInstance &CI):callback(CI){
            //添加一个MatchFinder去匹配ObjCPropertyDecl节点
            //回调!
            matcher.addMatcher(objcPropertyDecl().bind("objcPropertyDecl"), &callback);
        }
        
        //解析完毕一个顶级的声明就回调一次
        bool HandleTopLevelDecl(DeclGroupRef D){
            return true;
        }
        
        //当整个文件都解析完成后回调!!
        void HandleTranslationUnit(ASTContext &Ctx) {
            cout<<"文件解析完毕!!"<<endl;
            matcher.matchAST(Ctx);
        }
        
    };

    //定义一个类  PluginASTAction
    class HKASTAction:public PluginASTAction{
    public:
        bool ParseArgs(const CompilerInstance &CI, const vector<string> &arg) {
            return  true;
        }
        
        
        std::unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &CI, StringRef InFile) {
            return unique_ptr<HKConsumer> (new HKConsumer(CI));
        }
        
        
    };
    
}

//注册插件!
static FrontendPluginRegistry::Add<HKPlugin::HKASTAction> X("HKPlugin","this is the description");