Clang插件开发

作者: 浅墨入画 | 来源:发表于2021-10-17 15:06 被阅读0次

编写插件代码

  • HKPlugin目录下的HKPlugin.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;
  • 定义命名空间、定义HKASTAction类,继承自系统的PluginASTAction
namespace HKPlugin { 
    class HKASTAction:public PluginASTAction{
    
    }; 
}
  • 注册插件
static FrontendPluginRegistry::Add<HKPlugin::HKASTAction> 
    X("HKPlugin","this is the description");
  • 现有的需求有三:读取代码找到目标类型定义的属性和修饰符不符合标准,提示警告
  1. 实现需求的第一点读取代码,需要用到AST语法树,然后对AST节点进行解析。用到两个函数:CreateASTConsumerParseArgs
    HKASTAction类中,重写CreateASTConsumerParseArgs函数
namespace HKPlugin { 
    class HKASTAction:public PluginASTAction { 
        public:
            std::unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &CI, StringRef InFile) {
                return unique_ptr<ASTConsumer> (new ASTConsumer); 
            } 
            
            bool ParseArgs(const CompilerInstance &CI, const std::vector<std::string> &arg) { 
                return true; 
            } 
        }; 
    }

ASTConsumer是系统提供的基类,作为基类它的作用大多有两种:抽取代码由开发者继承,实现它的子类,对其进行扩展。所以我们不能直接使用ASTConsumer,需要对其进行继承,实现自定义子类

namespace HKPlugin {
    // 自定义类HKConsumer
    class HKConsumer:public ASTConsumer {
        public:
            // 解析完毕一个顶级的声明就回调一次
            bool HandleTopLevelDecl(DeclGroupRef D) {
                cout<<"正在解析..."<<endl;
                return true;
            } 
            
            void HandleTranslationUnit(ASTContext &Ctx) {
                cout<<"文件解析完成..."<<endl;
            }
    };

    class HKASTAction:public PluginASTAction { 
        public:
            std::unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &CI, StringRef InFile) { 
                return unique_ptr<HKConsumer> (new HKConsumer); 
            }
            
            bool ParseArgs(const CompilerInstance &CI, const std::vector<std::string> &arg) { 
                return true; 
            } 
    };
}
  1. 重写HandleTopLevelDeclHandleTranslationUnit函数
    HandleTopLevelDecl:顶级节点解析回调函数,顶级节点,例如:全局变量、函数定义、属性;
    HandleTranslationUnit:整个文件解析完成后的回调。

编译HKPlugin项目,在项目的Products目录下,找到编译出的clang可执行文件

image.png

同样在Products目录下,找到HKPlugin.dylib

image.png
测试插件

使用插件,测试文件和顶级节点的解析

  • 创建hello.m文件,代码如下
int sum(int a); 
int a;

int sum(int a){ 
    int b = 10; 
    return 10 + b; 
} 

int sum2(int a,int b){
    int c = 10; 
    return a + b + c; 
}
  • 使用以下命令,测试插件
自己编译的clang路径 -isysroot/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.5.sdk/ -Xclang -load -Xclang 插件(.dylib)路径 -Xclang -add-plugin -Xclang 插件名称 -c 源码路径

/Users/Documents/Source/llvm-hk/build_xcode/Debug/bin/clang -isysroot/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.5.sdk/ -Xclang -load -Xclang /Volumes/study/Source/llvm-hk/build_xcode/Debug/lib/HKPlugin.dylib -Xclang -add-plugin -XclangHKPlugin -c hello.m

//输出以下内容: 说明共解析出四个顶级节点
正在解析... 
正在解析... 
正在解析... 
正在解析... 
文件解析完成...

分析OC代码

需求是测试某个文件中有多少个顶级节点

  • 创建空工程,ViewController.m文件写入以下代码
#import "ViewController.h"

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

@implementation ViewController 
- (void)viewDidLoad { 
    [super viewDidLoad]; 
}

@end
  • 生成AST代码,查找属性声明
/Users/Documents/Source/llvm-hk/build_xcode/Debug/bin/clang -isysroot/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.5.sdk/ -Xclang -load -Xclang /Volumes/study/Source/llvm-hk/build_xcode/Debug/lib/HKPlugin.dylib -Xclang -add-plugin -XclangHKPlugin -c ./ViewController.m
image.png

ObjCPropertyDecl节点中可以找到属性的声明,包含属性的类型修饰符

MatchFinder

系统API提供MatchFinder,用于AST语法树节点的查找

  • 其中addMatcher函数,可以查找指定节点
参数1:设置指定节点;
参数2:执行回调,此处并非使用回调函数,而是一个回调类。需要继承MatchCallback系统类,实现自己的子类
void addMatcher(const DeclarationMatcher &NodeMatch, MatchCallback *Action);
  • 添加MatchFinder所在命名空间
using namespace ast_matchers;
  • 实现HKMatchHandler回调类,继承自MatchCallback
class HKMatchHandler:public MatchFinder::MatchCallback { 
    public: 
        void run(const MatchFinder::MatchResult &Result) {
            // 通过结果获取到节点对象
            const ObjCPropertyDecl *propertyDecl = Result.Nodes.getNodeAs<ObjCPropertyDecl>("objcPropertyDecl"); ,
            
            if(propertyDecl) { 
                string typeStr = propertyDecl->getType().getAsString(); 
                cout<<"------拿到了:"<<typeStr<<endl; 
            } 
    } 
};
  1. 必须实现run函数,它就是真正的回调函数
  2. 通过Result结果,获取节点对象
  3. 通过节点对象的getType().getAsString(),以字符串的形式返回属性类型
  • HKConsumer类中,定义私有MatchFinderHKMatchHandler,重写构造方法添加AST节点过滤器
class HKConsumer:public ASTConsumer { 
    private: 
        MatchFinder matcher; 
        HKMatchHandler handler; 
        
    public: 
        HKConsumer() { 
            matcher.addMatcher(objcPropertyDecl().bind("objcPropertyDecl"), &handler); 
        } 
};
  • 解析语法树,查找objcPropertyDecl节点。在文件解析完成的回调函数中,调用matchermatchAST函数,将文件的语法树传入过滤器
void HandleTranslationUnit(ASTContext &Ctx) { 
    cout<<"文件解析完成..."<<endl; 
    matcher.matchAST(Ctx); 
}
  • 测试插件
/Users/Documents/Source/llvm-hk/build_xcode/Debug/bin/clang -isysroot/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.5.sdk/ -Xclang -load -Xclang /Volumes/study/Source/llvm-hk/build_xcode/Debug/lib/HKPlugin.dylib -Xclang -add-plugin -XclangHKPlugin -c ./ViewController.m
image.png
  1. 通过语法树分析,可以找到属性的声明,包含属性的类型修饰符
  2. 但也存在一些问题,在预处理阶段,头文件会被展开,我们可能会获取到系统头文件中的属性,所以我们要想办法过滤掉系统文件中的代码

过滤系统文件

可以通过文件路径判断系统文件,因为系统文件都存在于/Applications/Xcode.app/开头的目录中

  • PluginASTAction类中,存在CompilerInstance类型的CI参数
// CI为编译器实例对象,可以通过它获取到文件路径,以及警告的提示
std::unique_ptr<ASTConsumer>
CreateASTConsumer(CompilerInstance &CI, StringRef InFile) override = 0;
  • 重写HKConsumer的构造函数,增加CI参数
HKConsumer(CompilerInstance &CI) { 
    matcher.addMatcher(objcPropertyDecl().bind("objcPropertyDecl"), &handler); 
}
  • HKASTAction类中,创建ASTConsumer时,将CI传入
std::unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &CI, StringRef InFile) { 
    return unique_ptr<HKConsumer> (new HKConsumer(CI)); 
}
  • 重写HKMatchHandler的构造函数,增加CI参数。定义私有CompilerInstance,通过构造函数对其赋值
class HKMatchHandler:public MatchFinder::MatchCallback { 
    private: 
        CompilerInstance &CI; 
    public: 
        HKMatchHandler(CompilerInstance &CI):CI(CI){ 
        
        } 
};
  • HKConsumer的构造函数中,对HKMatchHandler中的CI进行传递
HKConsumer(CompilerInstance &CI):handler(CI) { 
    // 添加一个MatchFinder去匹配objcPropertyDecl节点
    // 回调
    matcher.addMatcher(objcPropertyDecl().bind("objcPropertyDecl"), &handler); 
}
  • HKMatchHandler使用CI,获取文件路径并进行过滤
class HKMatchHandler:public MatchFinder::MatchCallback {
    private: 
        CompilerInstance &CI;
        // 判断是否是自己的文件
        bool isUserSourceCode(const string fileName) { 
            if(fileName.empty()) { 
                return false; 
            } 
            if(fileName.find("/Applications/Xcode.app/")==0) { 
                return false; 
            } 
            return true;
         }
         
     public:
         HKMatchHandler(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(); 
                 cout<<"------拿到了:"<<typeStr<<"它属于文件:"<<fileName<<endl; 
             } 
         }
};
  1. 通过CI.getSourceManager().getFilename获取文件名称,包含文件路径
  2. 需要传入SourceLocation,可以通过节点的propertyDecl->getSourceRange().getBegin()获得
  3. 实现isUserSourceCode函数,判断路径非空,并且非/Applications/Xcode.app/目录开头,视为自定义文件。
  • 测试插件
/Users/Documents/Source/llvm-hk/build_xcode/Debug/bin/clang -isysroot/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.5.sdk/ -Xclang -load -Xclang /Volumes/study/Source/llvm-hk/build_xcode/Debug/lib/HKPlugin.dylib -Xclang -add-plugin -XclangHKPlugin -c ./ViewController.m

文件解析完成... 
------拿到了:NSString *它属于文件:./ViewController.m
------拿到了:NSArray *它属于文件:./ViewController.m

成功过滤系统文件,获取到自定义文件中的两个属性

定位类型是否该使用Copy

  • 判断自定义属性是否需要使用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;
}
  • run方法中自定义属性类型进行判断
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(); 
           if(isShouldUseCopy(typeStr)) { 
            cout<<typeStr<<"应该使用copy修饰"<<endl; 
        }     
     } 
}
  • 测试插件
/Users/Documents/Source/llvm-hk/build_xcode/Debug/bin/clang -isysroot/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.5.sdk/ -Xclang -load -Xclang /Volumes/study/Source/llvm-hk/build_xcode/Debug/lib/HKPlugin.dylib -Xclang -add-plugin -XclangHKPlugin -c ./ViewController.m

文件解析完成... 
NSString *应该使用copy修饰
NSArray *应该使用copy修饰

获取属性的修饰符

  • 通过propertyDecl->getPropertyAttributes()获取属性修饰符,和OBJC_PR_copy进行位与运算
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(); 
           // 应该使用copy但是没有使用
           if(isShouldUseCopy(typeStr) && !(attrKind & ObjCPropertyDecl::OBJC_PR_copy)) { 
            cout<<typeStr<<"应该使用copy修饰但是没有用!发出警告!!"<<endl; 
        }     
     } 
}
  • 测试插件
/Users/Documents/Source/llvm-hk/build_xcode/Debug/bin/clang -isysroot/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.5.sdk/ -Xclang -load -Xclang /Volumes/study/Source/llvm-hk/build_xcode/Debug/lib/HKPlugin.dylib -Xclang -add-plugin -XclangHKPlugin -c ./ViewController.m

文件解析完成... 
NSString *应该使用copy修饰但是没有用!发出警告!!
NSArray *应该使用copy修饰但是没有用!发出警告!!

发出警告信息

当判断目标类型使用非copy修饰,目前只是内容打印提示,正确的做法是在Xcode中提示警告信息

  • 使用编译器实例对象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(); 
           // 应该使用copy但是没有使用
           if(isShouldUseCopy(typeStr) && !(attrKind & ObjCPropertyDecl::OBJC_PR_copy)) { 
               // 诊断引擎
               DiagnosticsEngine &diag = CI.getDiagnostics(); 
               // Report报告
               diag.Report(propertyDecl->getLocation(), diag.getCustomDiagID(DiagnosticsEngine::Warning, "请使用copy修饰")); 
           }     
     } 
}
  1. 通过CIgetDiagnostics函数,获取诊断引擎,需要传入位置DiagID
  2. 通过节点获取位置,使用propertyDecl->getLocation()获得当前节点的位置
  3. 通过diag.getCustomDiagID获取DiagID,设置提示级别文案
  • 测试插件
/Users/Documents/Source/llvm-hk/build_xcode/Debug/bin/clang -isysroot/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.5.sdk/ -Xclang -load -Xclang /Volumes/study/Source/llvm-hk/build_xcode/Debug/lib/HKPlugin.dylib -Xclang -add-plugin -XclangHKPlugin -c ./ViewController.m

文件解析完成... 
ViewController.m:14:33: warning: 请使用copy修饰
@property(nonatomic, strong) NSString* name;
                                       ^ 
ViewController.m:14:33: warning: 请使用copy修饰
@property(nonatomic, strong) NSArray* arrs;
                                      ^
2 warning generated.

Xcode集成编译器插件

  • 打开测试项目demo,在Xcode中注册插件,来到Build Settings→Other C Flags添加以下内容
-Xclang -load -Xclang (.dylib)动态库路径 -Xclang -add-plugin -Xclang CJLPlugin
image.png
  • Xcode中替换Clang,来到Build Settings中新增两项用户自定义设置

    image.png
  • 分别添加CCCXX

    image.png
  1. CC 对应的是自己编译的clang的绝对路径
  2. CXX 对应的是自己编译的clang++的绝对路径
  • Build Settings中将Enable Index-Wihle-Building Functionality设置为NO

    image.png
  • 运行demo测试插件

image.png

最后总结

Clang插件开发好处

  • 更好的研究编译器
  • 对LLVM有了更深入的理解
  • 有助于更好的学习启动优化相关内容
  1. 以往的内存相关理解都停留在iOS API上,而启动优化是与系统内存相关(物理内存 & 虚拟内存)
  2. 二进制文件重排做项目优化
  3. Clang插桩、Swift插桩

相关文章

网友评论

    本文标题:Clang插件开发

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