本文主要介绍Clang插件开发流程以及常用功能设置
主要内容:
- LLVM下载
- LLVM编译
- 自定义Clang插件
- 编写插件代码
- 插件集成
- 常用功能设置
1、 LLVM下载
我这里直接从git上下载的,并且下载的是完整版,所以一次下载好就够了。
- 下载LLVM项目
git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm-project.git
- 添加文件到llvm文件夹下
- 把 clang 拷贝到llvm/tools目录下
- 把compiler-rt,libcxx,libcxxabi,拷贝到llvm/projects目录下
- 把clang-tools-extra工具拷贝到llvm/tools/Clang/tools目录下
2、 LLVM编译
我这里使用XCode直接编译
- 安装cmake
先查看是否已安装cmake
brew list
如果没有安装需要通过brew安装
brew install cmake
- 使用cmake编译成XCode项目
- 创建文件夹llvm_xcode
- 命令行进入到llvm_xcode目录下
- 输入命令开始编译
cmake -G Xcode llvm路径
- 使用xcode编译Clang
我选择手动创建Schemes
分别选择Clang、ClangTooling进行编译
3、 创建Clang插件
- 创建文件夹
1)在llvm/tools/clang/tools文件夹下创建一个文件夹"WYClang"

文件夹.png
2)在llvm/tools/clang/tools/CmakeLists.txt中增加WYClang插件文件夹名称
# libclang may require clang-tidy in clang-tools-extra.
add_clang_subdirectory(libclang)
//增加自定义的插件
add_clang_subdirectory(WYClang)
- 创建文件
在WYClang文件夹中分别创建CMakeLists.txt文件和WYClang.cpp文件
在CMakeLists.txt文件中增加如下代码,表示增加一个插件文件,接下来就可以在WYClang.cpp编码了(这里的WYClang就是我们的插件名称)
add_llvm_library( WYClang MODULE BUILDTREE_ONLY
WYClang.cpp
)

新建文件.png
- 再次编译XCode
在llvm_xcode目录下输入命令开始编译cmake -G Xcode llvm路径
此时可以看到我们创建的WYClang文件已经存在了,此时就可以在这个文件里写插件代码了

Clang目录.png
-
XCode中选择YClang的Scheme进行编译
再次编译.png
4、 编写插件代码
4.1 编码
在WYClang目录下的WYClang.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 clang::ast_matchers;
// 插件命名空间
namespace WYClang {
// 第三步:扫描完毕回调
// 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<WYClang::JPASTAction> X("WYClang", "this is WYClang");
4.2 代码分析
在我22 - 编译流程认识中我们可以知道语法分析是在编译时进行的,分析完成后会生成一个抽象语法树,因此我们就对抽象语法树进行处理
4.2.1 思路:
- 在抽象语法树中查找语法节点
- 通过判断语法节点查找到我们想要的语句
- 通过回调对象执行run函数来自定义语法检测
4.2.2 过程:
- 注册插件
- 获取自定义语法树
- 获取解析后的语法树
- 通过AST节点查找器绑定属性标识
- 在AST节点查找器中添加回调函数
- 添加回调函数
- 获取节点对象(属性节点)
- 获取文件名称
- 判断是否是用户文件
- 判断是否应该用copy但是没有用
- 通过诊断信息进行错误报告
4.2.3 重要类
- PluginASTAction:AST操作类
- 是AST即抽象语法树,在语法树中就决定了如何进行语法解析
- 我们在编译时进行语法分析就是基于抽象语法树的
- ASTConsumer:AST解析器
- 它是用来对象AST进行解析的
- 有两个重要函数,一个是顶级声明函数,一个是解析结束回调函数
- 还可以在构造函数中设置节点查找器的匹配方式,并且设置匹配成功后的回调函数
- 当整个文件都解析后就可以将语法树添加到节点查找器中
- MatchFinder:节点查找器
- 它拿到语法树后,再设置查找的条件和查找成功后的回调函数
- 就可以在语法树的语法分析时,检查到我们设置的条件时进行回调函数的执行
- JPMatchCallback:回调函数
- 这是一个类,其中真正要执行的回调函数是run()
- 需要注册到节点查找器中
- 当语法分析到我们所设置的条件时就会执行回调函数
- ASTContext
- 保存长寿命的AST节点(例如类型和DECL),这些节点可以在文件的整个语义分析中引用。
- FrontendPluginRegistry
- 注册前端编译器的插件
- 传入需要注册插件的AST操作类
- CompilerInstance(编译器实例)
- 用于管理Clang的单个实例的Helper类
- CompilerInstance有两个用途:1)它管理运行编译器所需的各种对象,例如预处理器、目标信息和AST上下文。2) 它提供了构造和操作常见Clang对象的实用程序。
- 编译器实例通常拥有它管理的所有对象的实例。但是,客户端仍然可以通过手动设置对象并在销毁编译器状态之前重新获取所有权来共享对象。
- 编译器实例旨在简化客户端,但不是锁定它们
- 在编译器实例中执行所有操作。如果可能,函数有两种形式;一个简短的表单重用CompilerInstance对象,另一个较长的表单接受任何必需对象的显式实例。
- DiagnosticsEngine
- 诊断引擎
- 它可以用来报告诊断信息
4.2.4 重要函数
1. 注册插件
代码:
FrontendPluginRegistry::Add<DarkClang::JPASTAction> X("DarkClang", "this is DarkClang")
说明
- FrontendPluginRegistry类就是用来注册前端编译器的插件
- 通过add函数注册插件
- 传入的是我们自定义的插件Action(存疑)
2. 获取AST解析器(JPASTConsumer)
代码:
unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &CI, StringRef InFile) {
/**
传递CI
CI用于:
- 判断文件是否是用户的
- 抛出警告
*/
return unique_ptr<JPASTConsumer>(new JPASTConsumer(CI));
}
说明:
- CreateASTConsumer函数就是用来生成AST解析器
- 参数有两个,CI即编译器实例,可以用来管理编译器的各个对象,因此我们需要获取到这个CI
- InFile是用来,打印看一下
3. 解析插件命令行参数
代码:
/*
解析给定的插件命令行参数
- param CI 编译器实例,用于报告诊断。
- return 如果解析成功,则为true;否则,插件将被销毁,并且不执行任何操作。该插件负责使用CompilerInstance的Diagnostic对象报告错误。
*/
bool ParseArgs(const CompilerInstance &CI, const std::vector<std::string> &arg) {
return true;
}
说明:
- 该函数用来解析给定的插件命令行参数
- CI即编译器实例,可以用来报告诊断信息,该自定义插件就会使用这里的CI来报告错误
- 如果解析失败会销毁该插件,并且不进行任何操作
4. 顶级声明回调
代码:
// 解析完毕一个顶级的声明就回调一次(顶级节点,即全局变量,属性,函数等)
bool HandleTopLevelDecl(DeclGroupRef D) {
//cout<<"正在解析..."<<endl;
return true;
}
说明:
- 每解析一个顶级声明,都会进行一次回调
- 顶级声明就是顶级节点的声明,顶级节点可以看做是作用域是全局的变量函数等,如果是局部的就不算顶级声明,比如全局的函数声明或定义、全局的变量都属于顶级节点
5. 解析结束回调
代码:
// 当整个文件都解析完毕后回调
void HandleTranslationUnit(ASTContext &Ctx) {
// cout<<"文件解析完毕!!!"<<endl;
// 将文件解析完毕后的上下文context(即AST语法树) 给 matcher
matcher.matchAST(Ctx);
}
说明:
- 当文件中所有的节点都解析完成后调用该函数
- ASTContext就是解析后的结果,也就是AST语法树
- 我们这里是将AST语法树传递给AST节点查找器,让他来对节点进行处理
6. 节点过滤回调
代码:
void run(const MatchFinder::MatchResult &Result) {
}
说明:
- run()函数是属于MatchCallback类中的。
- 当AST节点查找器监听到节点表示后,就会执行MatchCallback中的run()函数进行回调
7. 报告诊断日志
代码:
// Report 报告
/**
错误位置:getLocation 节点位置
错误:getCustomDiagID(等级,提示)
*/
diag.Report(objcMessageExpr->getEndLoc(),diag.getCustomDiagID(DiagnosticsEngine::Error, "%0 - 该方法不推荐使用,请使用自定义Color属性"))<< nodeStr;
说明:
- 当查找到我们的目标节点后就通过diag.Report函数进行报告诊断日志
- 有两个参数
- 1)错误位置,也就是日志信息显示在什么位置
- 2)错误信息,在此可以设置报错等级,比如忽略、警告、报错等,还可以设置提示的内容
4.3 插件测试
命令:
自己编译的clang文件路径 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.1.sdk/ -Xclang -load -Xclang 插件(.dyld)路径 -Xclang -add-plugin -Xclang 插件名 -c 源码路径
示例:

结果.png
4.4 总结
5、 插件集成
5.1 添加插件到工程中

添加插件.png
5.2 设置编译器
- 新增用户定义设置

新增用户定义设置.png
- 设置Index-Wihle-Building

设置Index-Wihle-Building.png
5.3 最终效果

效果.png
6、 常用功能设置
6.2 检测方法调用
代码:
#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 methodClang {
// 第三步:扫描完毕回调
// 4、自定义回调类,继承自MatchCallback
class JPMatchCallback : public MatchFinder::MatchCallback {
private:
CompilerInstance &CI;
// 判断是否是自己的文件
bool isUserSourceCode(const string fileName) {
// 文件名不为空
if (fileName.empty()) return false;
// 非Xcode中的代码都认为是用户的
if (0 == fileName.find("/Applications/Xcode.app/")) return false;
return true;
}
bool isColorNode(const string nodeStr) {
//方法判断
if (nodeStr.find("colorWithWhite") != string::npos) return true;
return false;
}
public:
// 构造方法
JPMatchCallback(CompilerInstance &CI):CI(CI) {}
// 重载run方法
void run(const MatchFinder::MatchResult &Result) {
const ObjCMessageExpr *objcMessageExpr = Result.Nodes.getNodeAs<ObjCMessageExpr>("objcMessageExpr");
// 获取文件名称(包含路径)
string fileName = CI.getSourceManager().getFilename(objcMessageExpr->getSourceRange().getBegin()).str();
// 如果节点有值 && 是用户文件
if (objcMessageExpr && isUserSourceCode(fileName)) {
string nodeStr = objcMessageExpr->getSelector().getAsString();
if (isColorNode(nodeStr)) {
// 通过CI获取诊断引擎
DiagnosticsEngine &diag = CI.getDiagnostics();
// Report 报告
/**
错误位置:getLocation 节点位置
错误:getCustomDiagID(等级,提示)
*/
diag.Report(objcMessageExpr->getEndLoc(), diag.getCustomDiagID(DiagnosticsEngine::Error, "%0 - 该方法不推荐使用,请使用自定义Color属性"))<< nodeStr;
}
}
}
};
// 第二步:扫描配置完毕
// 3、自定义JPASTConsumer,继承自抽象类 ASTConsumer,用于监听AST节点的信息 -- 过滤器
class JPASTConsumer : public ASTConsumer {
private:
// AST 节点查找器(过滤器)
MatchFinder matcher;
// 回调对象
JPMatchCallback callback;
public:
// 构造方法中创建MatchFinder对象
JPASTConsumer(CompilerInstance &CI):callback(CI) { // 构造即将CI传递给callback
matcher.addMatcher(objcMessageExpr().bind("objcMessageExpr"), &callback);
}
// 解析完毕一个顶级的声明就回调一次(顶级节点,即全局变量,属性,函数等)
bool HandleTopLevelDecl(DeclGroupRef D) {
return true;
}
// 当整个文件都解析完毕后回调
void HandleTranslationUnit(ASTContext &Ctx) {
matcher.matchAST(Ctx);
}
};
//2、继承PluginASTAction,实现我们自定义的JPASTAction,即自定义AST语法树行为
class JPASTAction : public PluginASTAction {
public:
bool ParseArgs(const CompilerInstance &CI, const std::vector<std::string> &arg) {
return true;
}
// 返回自定义的JPASTConsumer对象,抽象类ASTConsumer的子类
unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &CI, StringRef InFile) {
return unique_ptr<JPASTConsumer>(new JPASTConsumer(CI));
}
};
}
// 第一步:注册插件,并自定义JPASTAction类
// 1、注册插件
static FrontendPluginRegistry::Add<methodClang::JPASTAction> X("methodClang", "this is methodClang");
结果:

插件运行结果.png
