编写插件代码
- 在
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");
- 现有的需求有三:
读取代码
、找到目标类型定义的属性和修饰符
、不符合标准,提示警告
- 实现需求的第一点
读取代码
,需要用到AST语法树
,然后对AST节点进行解析
。用到两个函数:CreateASTConsumer
、ParseArgs
在HKASTAction
类中,重写CreateASTConsumer
和ParseArgs
函数
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;
}
};
}
- 重写
HandleTopLevelDecl
和HandleTranslationUnit
函数
HandleTopLevelDecl
:顶级节点解析回调函数,顶级节点,例如:全局变量、函数定义、属性;
HandleTranslationUnit
:整个文件解析完成后的回调。
编译HKPlugin
项目,在项目的Products
目录下,找到编译出的clang可执行文件
![](https://img.haomeiwen.com/i1212147/8582a99aac273d83.png)
同样在Products
目录下,找到HKPlugin.dylib
![](https://img.haomeiwen.com/i1212147/def0ef90e9a7e464.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
![](https://img.haomeiwen.com/i1212147/eacf8babfba6154b.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;
}
}
};
- 必须实现
run
函数,它就是真正的回调函数
- 通过
Result
结果,获取节点对象
- 通过节点对象的
getType().getAsString()
,以字符串的形式返回属性类型
- 在
HKConsumer
类中,定义私有MatchFinder
和HKMatchHandler
,重写构造方法
添加AST节点过滤器
class HKConsumer:public ASTConsumer {
private:
MatchFinder matcher;
HKMatchHandler handler;
public:
HKConsumer() {
matcher.addMatcher(objcPropertyDecl().bind("objcPropertyDecl"), &handler);
}
};
- 解析语法树,查找
objcPropertyDecl
节点。在文件解析完成的回调函数中,调用matcher
的matchAST
函数,将文件的语法树传入过滤器
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
![](https://img.haomeiwen.com/i1212147/b15512c9e1452e3e.png)
- 通过
语法树分析
,可以找到属性的声明
,包含属性的类型
和修饰符
- 但也存在一些问题,在
预处理阶段
,头文件会被展开,我们可能会获取到系统头文件中的属性
,所以我们要想办法过滤掉系统文件中的代码
过滤系统文件
可以通过文件路径判断系统文件,因为系统文件都存在于/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;
}
}
};
- 通过
CI.getSourceManager().getFilename
获取文件名称,包含文件路径
- 需要传入
SourceLocation
,可以通过节点的propertyDecl->getSourceRange().getBegin()
获得 - 实现
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修饰"));
}
}
}
- 通过
CI
的getDiagnostics
函数,获取诊断引擎
,需要传入位置
和DiagID
- 通过节点获取
位置
,使用propertyDecl->getLocation()
获得当前节点的位置 - 通过
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
![](https://img.haomeiwen.com/i1212147/d5c36196fb3d196b.png)
-
在
Xcode
中替换Clang
,来到Build Settings
中新增两项用户自定义设置
image.png
-
分别添加
CC
和CXX
image.png
-
CC
对应的是自己编译的clang的绝对路径
-
CXX
对应的是自己编译的clang++的绝对路径
-
在
Build Settings
中将Enable Index-Wihle-Building Functionality
设置为NO
image.png
-
运行
demo
测试插件
![](https://img.haomeiwen.com/i1212147/a69cba5df82bc902.png)
最后总结
Clang插件开发好处
- 更好的研究编译器
- 对LLVM有了更深入的理解
- 有助于更好的学习
启动优化
相关内容
- 以往的内存相关理解都停留在
iOS API
上,而启动优化是与系统内存相关(物理内存
&虚拟内存
) -
二进制文件重排
做项目优化 - Clang插桩、Swift插桩
网友评论