首页 > 移动开发 > IOS > iOS底层原理之自定义Clang插件
嗨,这里是逻辑iOS技术号:一个让知识变得感性,让学习变得轻松!活跃的技术小站,希望给你的生活与技术带来意思不一样!关注公众号,回复“ 面试题 ”,即可领取更多大厂面试题型哦~ 小逻辑相信我们的生活不止眼前的苟且,还有我们向往的诗和大厂高薪工作~
前文主要介绍了下LLVM和Clang相关的概念、设计思想和编译流程,本篇文章将使用LLVM和Clang实现一个简单的插件。废话不多说,让我们开始今天的内容吧。
编写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/toolsgit clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm-project/clang.git
(滑动显示更多)
如果想研究lldb的话,需要在LLVM的tools目录下下载lldb。自定义插件不需要。
cd llvm/toolsgit clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm-project/lldb.git
(滑动显示更多)
在LLVM的projects目录下下载compiler-rt,libcxx,libcxxabi。
cd ../projectsgit clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm-project/compiler-rt.gitgit clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm-project/libcxx.gitgit clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm-project/libcxxabi.git
(滑动显示更多)
在Clang的tools下安装clang-tools-extra工具。
cd ../tools/clang/toolsgit clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm-project/clang-tools-extra.git
(滑动显示更多)
新版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_xcodecd build_xcode // 进入build_xcodecmake -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_xcodecd build_xcode // 进入build_xcodecmake -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)
(滑动显示更多)
依次执行编译、安装指令。
ninjaninja install
进入build_xcode目录打开LLVM.xcodeproj:
进入Xcode界面:
注意:不要选择Automatically Create Schemes,选择Manually Manage Schemes。
否则会引入一些不必要的scheme,拖累Xcode速度。
原则:使用哪个scheme,就引入哪个。
点击左下角加号,在Target中选择我们需要的添加:
开始运行clang和clangTooling,第一次运行时需要进行编译,往后再运行,即可直接运行:
选择Build & Run:
真正进入编译模式:
开始创建插件之前先对要实现的功能做一个简单的介绍:
自定义插件想要实现的功能是当检测到NSString、NSArray、NSDictionary类型的属性使用的修饰属性不为copy时,发出警告。
在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.cpptouch CMakeLists.txt// CMakeLists.txt文件中添加如下代码add_llvm_library( XJPlugin MODULE BUILDTREE_ONLYXJPlugin.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目录了。接下来就在里面编写插件代码。
在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、自定义回调类,继承自MatchCallbackclass 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 / NSDictionaryif (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,但是没有使用copyif (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语法树) 给 matchermatcher.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) {/**传递CICI用于:- 判断文件是否是用户的- 抛出警告*/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、通过诊断引擎报告错误
通过终端测试插件:
// 命令格式自己编译的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
(滑动显示更多)
此插件只作为研究clang之用,实际开发的项目中最好不要继承,因为会影响Xcode编译速度。此插件集成是针对项目,不是针对整个Xcode,测试项目可以放心集成。
打开测试项目,在target -> Build Settings -> Other C Flags添加如下内容:
由于clang插件需要使用对应的版本去加载,如果版本不一致会导致编译失败,如下所示:
在Build Settings栏目中新增两项用户定义的设置,分别是CC和CXX
CC 对应的是自己编译的clang的绝对路径
CXX 对应的是自己编译的clang++的绝对路径
接下来在Build Settings中搜索index,将Enable Index-Wihle-Building Functionality的Default改为NO
最后,重新编译测试项目,会出现我们想要的效果:
关于LLVM和Clang的研究到此就告一段落了。下篇文章将进入启动优化的探索,敬请期待。
来源:掘金
作者:温暖
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/70005484/viewspace-2795561/ ,如需转载,请注明出处,否则将追究法律责任。