1. 写在前面
在上篇博客中已经介绍了LLVM
下载流程和LLVM
的编译流程,也对编译完成的LLVM
工程进行了 Clang
和clangTooling
的编译。
iOS底层探索之LLVM(二)——自定义Clang插件(上)
最终实现的效果本篇博客将手把手教大家,进行代码编写,自定义一个
Clang
插件,最终实现的功能是对不正确使用属性修饰会进行报错,并提示正确的用词,实现效果如下。
2. 前期准备
2.1 新建插件
在/llvm/tools/clang/tools
目录下新建插件JPPlugins
(这个是你自己建的,名字随便都可以,你自己知道就可以)
2.2 修改CMakeLists.txt
修改/llvm/tools/clang/tools
目录下的文件CMakeLists.txt
,新增加一句add_clang_subdirectory(JPPlugins)
- 在
JPPlugins
目录下新建一个名为JPPlugins.cpp
的文件和CMakeLists.txt
的文件, 在CMakeLists.txt
中写上如下代码
add_llvm_library( JPPlugins MODULE BUILDTREE_ONLY
JPPlugins.cpp
)
2.3 编译插件
新建JPPlugins.cpp和CMakeLists.txt文件- 接下来再次使用
cmake
命令重新生成一下xcode
项目,还是在llvm_build
目录中 使用cmake -G Xcode ../llvm
命令。 - 最后可以在
LLVM
的Xcode
项目中可以看到Loadable modules
目录下有自己的Plugin
目录了,我兴致勃勃的打开工程一看。。。。。
插件生成成功什么?居然失败了,什么都没有啊!这是在和我开玩笑吗?我仔细一看,是否是
JPPlugins.cpp
文件和JPPlugins
插件同名导致失败的呢?我于是改了下cpp
的名称,同时把插件名的s
去掉了,还真就成功了!这就让我百思不得解了,就很奇怪!
- 我这倔脾气,我就不信这个邪了,我又改回原来最初的名称,
JPPlugins.cpp
文件和JPPlugins
插件依然是同名,我又编译了一次,发现还是失败了。 - 我又改成
JP.cpp
文件和插件名为JPPlugins
,这回不一样了,该成功了吧!结果还是失败了。 - 我又进行了第三次尝试,
JPPlugin.cpp
文件和插件名为JPPlugin
,这回我把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
文件路径为:llvm-project/llvm_build/Debug/bin/clang
-
模拟器文件路径为:
/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk
,根据自己的电脑去判断。 -
插件名字:就是你建立的插件的名字,我这里是
JPPlugin
-
源码文件路径:就是你需要插件去识别的文件的路径
-
例如下面👇这样
/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
插件需要使用对应的版本去加载,如果版本不一致则会导致编译错误,会出现如下图所示:
- 在
Build Settings
栏目中新增两项用户定义的设置
分别是CC 和CXX
-
CC
对应的是自己编译的clang
的绝对路径 -
CXX
对应的是自己编译的clang++
的绝对路径
- 设置分别是CC 和CXX
- 接下来在
Build Settings
栏目中搜索index
,将Enable Index-Wihle-Building Functionality
将Default
改为NO
,我的Xcode
有毒,搜索不到,我是硬找的,直接往下翻,好一番查找,终于找到了。
3.3 编译测试插件
- 最后
command + B
编译一下
从图中的结果,可以看出,
clang
的插件完美的运行了。
低调低调.png
4. 总结
- 过程是曲折的,结果是美好的!😁
- 我的
Xcode
版本是Version 12.5
macOS Big Sur 11.4
- 以上内容仅供参考,每个人的电脑环境不一样,可能有差别。
- 配置的时候一定要注意路径不要写错了。
- 这只是记录我自己的探索过程,最主要的是一些思路和方法,踩过的坑,希望可以帮到大家避雷!
5. 写在后面
关注我,更多内容持续输出!
敬请期待!
🌹 喜欢就点个赞吧👍🌹
🌹 觉得有收获的,可以来一波 收藏+关注,以免你下次找不到我😁🌹
🌹欢迎大家留言交流,批评指正,
转发
请注明出处,谢谢支持!🌹
网友评论