iOS底层原理之自定义Clang插件
前言
前文主要介绍了下LLVM
和Clang
相关的概念、设计思想和编译流程,本篇文章将使用LLVM
和Clang
实现一个简单的插件。废话不多说,让我们开始今天的内容吧。
一: LLVM下载
编写Clang
插件之前,需要先下载和编译LLVM
。
由于国内的网络限制,我们需要借助镜像下载
LLVM
的源码。 mirror.tuna.tsinghua.edu.cn/help/llvm-p…
这里提供两种下载方式,一种是下载整个LLVM
(包括各个子仓库,比如clang
等等),一种是只下载LLVM
,然后根据自己需要再去下载子仓库。
1.1: 下载完整LLVM
,包含子仓库(2.78G
)
git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm-project.git
1.2: 只下载LLVM
,不包含子仓库(1.52G
)
git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm-project/llvm.git
1.2.1: 根据需要下载相应的子仓库
自定义插件需要的子仓库
- 在
LLVM
的tools
目录下下载Clang
。
cd llvm/tools
git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm-project/clang.git
如果想研究
lldb
的话,需要在LLVM
的tools
目录下下载lldb
。自定义插件不需要。cd llvm/tools git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm-project/lldb.git 复制代码
- 在
LLVM
的projects
目录下下载compiler-rt,libcxx,libcxxabi
。
cd ../projects
git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm-project/compiler-rt.git
git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm-project/libcxx.git
git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm-project/libcxxabi.git
- 在
Clang
的tools
下安装clang-tools-extra
工具。
cd ../tools/clang/tools
git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm-project/clang-tools-extra.git
二: LLVM
编译
新版macOS
默认的shell
是zsh
,所以,首先进入终端执行:
echo 'export OSX_COMMANDLINE_SDKROOT="/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk"' >> ~/.zshrc
然后再执行:
source ~/.zshrc
由于最新的LLVM
只支持cmake
来编译了,所以我们还需要安装brew
和cmake
。相关安装方法请移步brew和cmake安装。
一些常见的构建系统生成器(generator
)有:
-
Ninja
:大多数LLVM
开发人员都使用Ninja
。 -
Unix Makefiles
:用于生成与make
兼容的并行makefile
。 -
Visual Studio
:用于生成Visual Studio
项目和解决方案。 -
Xcode
:用于生成Xcode
项目。
作为iOS
开发人员,当然首选Xcode
来进行编译了。
2.1: 使用Xcode
构建LLVM
项目
首先使用Xcode
为generator
,通过cmake
将LLVM
编译成Xcode
项目。
2.1.1: 完整LLVM
编译方法
cd llvm-project // 进入完整llvm文件夹
mkdir build_xcode // 新建文件夹build_xcode
cd build_xcode // 进入build_xcode
cmake -G <generator> [options] ../llvm // 编译成Xcode项目,具体命令看下面
这里generator
我们选择Xcode
,-DLLVM_ENABLE_PROJECTS
就是需要编译的子项目,这里我们需要加上clang,compiler-rt,libcxx,libcxxabi,clang-tools-extra
。
cmake -G Xcode -DLLVM_ENABLE_PROJECTS='libcxx;libcxxabi;clang;clang-tools-extra;compiler-rt' -DLLDB_USE_SYSTEM_DEBUGSERVER=ON -DLLDB_TEST_COMPILER=clang++ -DCMAKE_OSX_SYSROOT=$OSX_COMMANDLINE_SDKROOT ../llvm
2.1.2: 不完整LLVM
编译方法
mkdir build_xcode // 在llvm所在目录新建文件夹build_xcode
cd build_xcode // 进入build_xcode
cmake -G Xcode ../llvm // 编译成Xcode项目
不完整
LLVM
由于我们已经根据自己的情况下载了子仓库,所以不用添加[options]
直接编译就可以了。
开始编译(完整包为例):
大概几分钟后后,检测映射完成。
此时build_xcode
目录下大概有67M
内容(指定不同[options]
,大小会有所不同):
2.2: 使用ninja
构建LLVM
项目(不推荐)
-
使用
ninja
进行编译,需要先安装ninja
,相关安装方法请移步brew和cmake安装。 -
在
llvm
源码根目录下新建build_ninja
目录,最终会在build_ninja
目录下生成build.ninja
。 -
在
llvm
源码根目录下新建llvm-release
目录,最终编译文件会在llvm-release
文件夹路径下。
cd llvm_build
// 注意DCMAKE_INSTALL_PREFIX后面不能有空格
cmake -G Ninja ../llvm -DCMAKE_INSTALL_PREFIX= 安装路径(本机为/ Users/xxx/xxx/LLVM/llvm_release)
- 依次执行编译、安装指令。
ninja
ninja install
2.3: 使用Xcode
编译Clang
- 进入
build_xcode
目录打开LLVM.xcodeproj
:
- 进入
Xcode
界面:
⚠️注意:不要选择
Automatically Create Schemes
,选择Manually Manage Schemes
。否则会引入一些不必要的
scheme
,拖累Xcode
速度。原则:使用哪个
scheme
,就引入哪个。
- 点击左下角加号,在
Target
中选择我们需要的添加:
- 自定义插件需要添加
clang
和clangTooling
:
- 开始运行
clang
和clangTooling
,第一次运行时需要进行编译,往后再运行,即可直接运行:
⚠️注意:每次运行时要通过
Run Without Building
运行。这意味着当你编译一次之后,代码没有改变的情况下,不需要重新编译,直接运行现有可执行文件即可。
- 选择
Build & Run
:
- 真正进入编译模式:
- 起飞🚀,感受机器的轰鸣吧!!!趁这个时间可以洗个澡或吃个饭😂。
三: Clang
插件
开始创建插件之前先对要实现的功能做一个简单的介绍:
- 自定义插件想要实现的功能是当检测到
NSString
、NSArray
、NSDictionary
类型的属性使用的修饰属性不为copy
时,发出警告。
3.1: 创建插件
在llvm-project/llvm/tools/clang/tools
目录下新建插件目录XJPlugin
:
修改llvm-project/llvm/tools/clang/tools
目录(即同目录)下的CMakeLists.txt
文件,在最下面新增add_clang_subdirectory(XJPlugin)
。
在XJPlugin
目录下新建一个名为XJPlugin.cpp
的文件和CMakeLists.txt
的文件。在CMakeLists.txt
中添加如下代码:
// 通过终端在XJPlugin目录下创建这两个文件
touch CJLPlugin.cpp
touch CMakeLists.txt
// CMakeLists.txt文件中添加如下代码
add_llvm_library( XJPlugin MODULE BUILDTREE_ONLY
XJPlugin.cpp
)
接下来使用cmake
重新构建一下Xcode
项目,终端进入build_xcode
目录,运行如下命令:
cmake -G Xcode -DLLVM_ENABLE_PROJECTS='libcxx;libcxxabi;clang;clang-tools-extra;compiler-rt' -DLLDB_USE_SYSTEM_DEBUGSERVER=ON -DLLDB_TEST_COMPILER=clang++ -DCMAKE_OSX_SYSROOT=$OSX_COMMANDLINE_SDKROOT ../llvm
重新进入build_xcode
目录打开LLVM.xcodeproj
,然后添加XJPlugin
的scheme
,并进行编译:
现在在LLVM
的Xcode
项目的Loadable modules
目录下就可以看到我们的XJPlugin
目录了。接下来就在里面编写插件代码。
工程目录非常多,可以全选之后按住
command
键,鼠标左键点击目录左边的箭头全部折叠,这样就方便找到Loadable modules
目录了。
3.2: 编写插件代码
在XJPlugin.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 XJPlugin {
// 第三步:扫描完毕回调
// 4、自定义回调类,继承自MatchCallback
class XJMatchCallback : public MatchFinder::MatchCallback {
private:
// CI传递路径:XJASTAction类中的CreateASTConsumer方法参数 -> XJASTConsumer的构造函数 -> XJMatchCallback的私有属性,通过构造函数从XJASTConsumer构造函数中获取
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:
// 构造方法
XJMatchCallback(CompilerInstance &CI):CI(CI) {}
// 重载run方法
void run(const MatchFinder::MatchResult &Result) {
// 通过Result获取节点对象,根据节点id("objcPropertyDecl")获取(此id需要与XJASTConsumer构造方法中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、自定义XJASTConsumer,继承自抽象类 ASTConsumer,用于监听AST节点的信息 -- 过滤器
class XJASTConsumer : public ASTConsumer {
private:
// AST 节点查找器(过滤器)
MatchFinder matcher;
// 回调对象
XJMatchCallback callback;
public:
// 构造方法中创建MatchFinder对象
XJASTConsumer(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,实现我们自定义的XJASTAction,即自定义AST语法树行为
class XJASTAction : public PluginASTAction {
public:
// 重载ParseArgs 和 CreateASTConsumer方法
/*
解析给定的插件命令行参数
- param CI 编译器实例,用于报告诊断。
- return 如果解析成功,则为true;否则,插件将被销毁,并且不执行任何操作。该插件负责使用CompilerInstance的Diagnostic对象报告错误。
*/
bool ParseArgs(const CompilerInstance &CI, const std::vector<std::string> &arg) {
return true;
}
// 返回自定义的XJASTConsumer对象,抽象类ASTConsumer的子类
unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &CI, StringRef InFile) {
/**
传递CI
CI用于:
- 判断文件是否是用户的
- 抛出警告
*/
return unique_ptr<XJASTConsumer>(new XJASTConsumer(CI));
}
};
}
// 第一步:注册插件,并自定义XJASTAction类
// 1、注册插件
static FrontendPluginRegistry::Add<XJPlugin::XJASTAction> X("XJPlugin", "this is XJPlugin");
原理主要分为三步:
-
【第一步】注册插件,并自定义XJASTAction类
- 自定义
XJASTAction
类(继承自抽象类PluginASTAction
),重载两个函数ParseArgs
和CreateASTConsumer
,在CreateASTConsumer
中创建XJASTConsumer
类对象,并将编译器实例CI
传递过去。CI
主要用于以下两个方面- 判断文件是否是用户的
- 抛出警告
- 通过
FrontendPluginRegistry
注册插件,需要关联插件名与自定义的XJASTAction
类。
- 自定义
-
【第二步】扫描配置完毕
- 自定义
XJASTConsumer
类(继承自ASTConsumer
),声明节点查找器MatchFinder matcher
和回调对象XJMatchCallback callback
。 - 实现构造函数,创建
MatchFinder
对象,并将CI
传递给回调对象callback
。 - 重载两个方法
-
HandleTopLevelDecl
:解析完毕一个顶级的声明就回调一次 -
HandleTranslationUnit
:当整个文件都解析完毕后回调,将文件解析完毕后的上下文context
(即AST
语法树)给matcher
。
-
- 自定义
-
【第三步】扫描完毕的回调
- 自定义回调类
XJMatchCallback
(继承自MatchCallback
),声明私有变量CI
,用于接收ASTConsumer
类传递过来的CI
。 - 重写
run
方法- 1、通过
Result
根据节点id
获取节点对象(此id
需要与XJASTConsumer
构造方法中bind
的id
一致)。 - 2、判断节点有值并且是用户文件
- 3、获取属性节点的描述信息
- 4、获取属性节点的类型,并转成字符串
- 5、判断属性是否需要用
copy
但是没有用copy
- 6、通过
CI
获取诊断引擎 - 7、通过诊断引擎报告错误
- 1、通过
- 自定义回调类
通过终端测试插件:
// 命令格式
自己编译的clang文件路径 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.5.sdk(SDK路径)/ -Xclang -load -Xclang 插件(.dyld)路径 -Xclang -add-plugin -Xclang 插件名 -c 源码路径
// 例子
/Users/用户名/llvm-project/build_xcode/Debug/bin/clang -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.5.sdk/ -Xclang -load -Xclang /Users/用户名/llvm-project/build_xcode/Debug/lib/XJPlugin.dylib -Xclang -add-plugin -Xclang XJPlugin -c /Users/用户名/Desktop/DemoCode/PluginTestDemo/PluginTestDemo/ViewController.m
3.3: Xcode
集成插件
此插件只作为研究clang
之用,实际开发的项目中最好不要继承,因为会影响Xcode
编译速度。此插件集成是针对项目,不是针对整个Xcode
,测试项目可以放心集成。
3.3.1: 加载插件
- 打开测试项目,在
target -> Build Settings -> Other C Flags
添加如下内容:
-Xclang -load -Xclang (.dylib)插件动态库路径 -Xclang -add-plugin -Xclang 插件名
如果你正在跳槽或者正准备跳槽不妨动动小手,添加一下咱们的交流群1012951431来获取一份详细的大厂面试资料为你的跳槽多添一份保障。
3.3.2: 设置编译器
- 由于
clang
插件需要使用对应的版本去加载,如果版本不一致会导致编译失败,如下所示:
-
在
Build Settings
栏目中新增两项用户定义的设置,分别是CC
和CXX
-
CC
对应的是自己编译的clang
的绝对路径 -
CXX
对应的是自己编译的clang++
的绝对路径
-
- 接下来在
Build Settings
中搜索index
,将Enable Index-Wihle-Building Functionality
的Default
改为NO
- 最后,重新编译测试项目,会出现我们想要的效果:
总结
关于LLVM
和Clang
的研究到此就告一段落了。下篇文章将进入启动优化的探索,敬请期待。