开发 clang 插件, 最常见的就是,属性检查器
本文最后的效果:
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
( 这一步,可能时间有点长 )
得到我们要的工程文件
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 的配置文件中,添加的语句对应
效果如图
可看到,最右端,有两个文件
配置文件 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
中的工程,长这样
阶段性小结: 此时 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
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 插件动态库
- 更改参与编译的 Clang 路径
新建两个选项
CC 对应 clang
CXX 对应 clang++
- 编译选项,索引选 No
3, 原理补充
llvm 分为编译器前端和后端
- 编译器前端,我们主要使用到 clang 和 swift,生成抽象语法树 AST, 生成 IR
clang 针对 C 语言、CPP、Objective - C
- 编译器后端,做代码生成的工作,包括 IR -> 汇编 -> 二进制