iOS之武功秘籍⑰: Clang插件开发
2021-03-05 本文已影响0人
長茳
写在前面
上篇我们介绍了LLVM
的编译流程,接下来我们就来玩玩怎么做插件吧.....
一、配置LLVM环境
特别提醒:
- 1.
LLVM
源码大2.29G
,编译后
文件将近30G
,所以请确保
电脑硬盘空间足够
; - 2.
编译时
,电脑温度会飙升90多度
,CPU
资源占满,请用空调伺候
着,有可能会黑屏; - 3.
编译时间
长达1个多小时
,请合理安排时间,可以先洗澡什么的...
如果以上3点,你确定能接受,那我们就开始吧.
① LLVM下载
在github下载LLVM相关资源库:
-
clang、clang-tools-extra、compiler-rt、libcxx、libcxxabi、llvm
五个库:
- 解压并移除名称中的版本号
- 按以下顺序将文件夹移到指定位置:
- 将
clang-tools-extra
移到clang文件夹中的clang/tools
文件中 - 将
clang
文件夹移到llvm/tools
中 - 将
compiler-rt、libcxx、libcxxabi
都移到llvm/projects
中
② LLVM编译
由于最新的
LLVM
只支持cmake
来编译,所以需要安装cmake
②.1 安装cmake
- 查看
brew
是否安装cmake
,如果已经安装,则跳过下面步骤 --brew list
- 通过
brew
安装cmake
--brew install cmake
②.2 编译LLVM
有两种编译方式:
- 通过
Xcode
编译LLVM
- 通过
ninja
编译LLVM
②.2.1 通过xcode编译LLVM
-
在
llvm
同级目录创建build
文件夹,cd
到build
文件夹,运行cmake
命令,将llvm
编译成Xcode
项目cd build cmake -G Xcode ../llvm // 或者: cmake -G Xcode CMAKE_BUILD_TYPE="Release" ../llvm // 或者: cmake -G Xcode CMAKE_BUILD_TYPE="debug" ../llvm
注意:
build
文件夹是存放cmake
生成的Xcode
文件的.放哪里都可以.cmake
编译的对象是llvm
文件.所以使用cmake -G Xcode ../llvm
编译并生成Xcode
文件时,请核对llvm
的文件路径.
- 成功之后,可以看到生成的
Xcode
文件:
- 使用
Xcode
打开LLVM.xcodeproj
- 选择手动创建Schemes
* 添加`clang`和`clangTooling`两个`Target`,并完成两个`target`的编译[图片上传失败...(image-b63796-1614944397110)]
* 编译成功后,我们的准备工作就完成了.可以正式开始插件开发了
②.2.2 通过ninja编译LLVM
- 在
llvm
同级目录创建build
文件夹 - 使用
ninja
进行编译则还需要安装ninja
,使用brew install ninja
命令安装ninja
- 在
llvm
源码根目录下新建一个build_ninja
目录,最终会在build_ninja
目录下生成build.ninja
- 在
llvm
源码根目录下新建llvm_release
目录,最终编译文件会在llvm_release
文件夹路径下
cd build
//注意DCMAKE_INSTALL_PREFIX后面不能有空格
cmake -G Ninja ../llvm -DCMAKE_INSTALL_PREFIX= 安装路径(本机为/ Users/xxx/xxx/LLVM/llvm_release)
- 依次执行编译,安装指令
ninja
ninja install
小编这里选择的是用Xcode编译的.
二、自定义插件
- 在
llvm/tools/clang/tools
文件夹中,创建CJPlugin
文件夹,即插件名称
- 在
/llvm/tools/clang/tools
目录下的CMakeLists.txt
文件,新增add_clang_subdirectory(CJPlugin)
,此处的CJPlugin
即为上一步创建的插件名称
- 在
CJPlugin
目录下新建两个文件,分别是CJPlugi.cpp
和CMakeLists.txt
,并在CMakeLists.txt
中加上以下代码//1、通过终端在CJPlugin目录下创建 touch CJPlugin.cpp touch CMakeLists.txt //2、CMakeLists.txt中添加以下代码 add_llvm_library( CJPlugin MODULE BUILDTREE_ONLY CJPlugin.cpp )
- 接下来利用
cmake
重新生成Xcode
项目,在build
目录下执行cmake -G Xcode ../llvm
命令 - 最后可以在
LLVM
的Xcode
项目中可以看到Loadable modules
目录下由自定义的CJPlugin
目录了,然后可以在里面编写插件代码了
-
Manage Schemes
添加我们的CJPlugin
-
在
CJPlugin
目录下的CJPlugin.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 CJPlugin { //第三步:扫描完毕的回调函数 //4、自定义回调类,继承自MatchCallback class CJMatchCallback: public MatchFinder::MatchCallback { private: //CI传递路径:CJASTAction类中的CreateASTConsumer方法参数 - CJConsumer的构造函数 - CJMatchCallback的私有属性,通过构造函数从CJASTConsumer构造函数中获取 CompilerInstance &CI; //判断是否是自己的文件 bool isUserSourceCode(const string filename) { //文件名不为空 if (filename.empty()) return false; //非xcode中的源码都认为是用户的 if (filename.find("/Applications/Xcode.app/") == 0) 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: CJMatchCallback(CompilerInstance &CI):CI(CI){} //重写run方法 void run(const MatchFinder::MatchResult &Result) { //通过result获取到相关节点 -- 根据节点标记获取(标记需要与CJASTConsumer构造方法中一致) const ObjCPropertyDecl *propertyDecl = Result.Nodes.getNodeAs<ObjCPropertyDecl>("objcPropertyDecl"); //判断节点有值,并且是用户文件 if (propertyDecl && isUserSourceCode(CI.getSourceManager().getFilename(propertyDecl->getSourceRange().getBegin()).str()) ) { //15、获取节点的描述信息 ObjCPropertyAttribute::Kind attrKind = propertyDecl->getPropertyAttributes(); //获取节点的类型,并转成字符串 string typeStr = propertyDecl->getType().getAsString(); // cout<<"---------拿到了:"<<typeStr<<"---------"<<endl; //判断应该使用copy,但是没有使用copy if (propertyDecl->getTypeSourceInfo() && isShouldUseCopy(typeStr) && !(attrKind & ObjCPropertyAttribute::kind_copy)) { //使用CI发警告信息 //通过CI获取诊断引擎 DiagnosticsEngine &diag = CI.getDiagnostics(); //通过诊断引擎 report报告 错误,即抛出异常 /* 错误位置:getBeginLoc 节点开始位置 错误:getCustomDiagID(等级,提示) */ diag.Report(propertyDecl->getBeginLoc(), diag.getCustomDiagID(DiagnosticsEngine::Warning, "%0 - 这个地方推荐使用copy!!"))<< typeStr; } } } }; //第二步:扫描配置完毕 //3、自定义CJASTConsumer,继承自ASTConsumer,用于监听AST节点的信息 -- 过滤器 class CJASTConsumer: public ASTConsumer { private: //AST节点的查找过滤器 MatchFinder matcher; //定义回调类对象 CJMatchCallback callback; public: //构造方法中创建matcherFinder对象 CJASTConsumer(CompilerInstance &CI) : callback(CI) { //添加一个MatchFinder,每个objcPropertyDecl节点绑定一个objcPropertyDecl标识(去匹配objcPropertyDecl节点) //回调callback,其实是在CJMatchCallback里面重写run方法(真正回调的是回调run方法) matcher.addMatcher(objcPropertyDecl().bind("objcPropertyDecl"), &callback); } //实现两个回调方法 HandleTopLevelDecl 和 HandleTranslationUnit //解析完一个顶级的声明,就回调一次(顶级节点,相当于一个全局变量、函数声明) bool HandleTopLevelDecl(DeclGroupRef D){ // cout<<"正在解析..."<<endl; return true; } //整个文件都解析完成的回调 void HandleTranslationUnit(ASTContext &context) { // cout<<"文件解析完毕!"<<endl; //将文件解析完毕后的上下文context(即AST语法树) 给 matcher matcher.matchAST(context); } }; //2、继承PluginASTAction,实现我们自定义的Action,即自定义AST语法树行为 class CJASTAction: public PluginASTAction { public: //重载ParseArgs 和 CreateASTConsumer方法 bool ParseArgs(const CompilerInstance &ci, const std::vector<std::string> &args) { return true; } //返回ASTConsumer类型对象,其中ASTConsumer是一个抽象类,即基类 /* 解析给定的插件命令行参数。 - param CI 编译器实例,用于报告诊断。 - return 如果解析成功,则为true;否则,插件将被销毁,并且不执行任何操作。该插件负责使用CompilerInstance的Diagnostic对象报告错误。 */ unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &CI, StringRef iFile) { //返回自定义的CJASTConsumer,即ASTConsumer的子类对象 /* CI用于: - 判断文件是否使用户的 - 抛出警告 */ return unique_ptr<CJASTConsumer> (new CJASTConsumer(CI)); } }; } //第一步:注册插件,并自定义AST语法树Action类 //1、注册插件 static FrontendPluginRegistry::Add<CJPlugin::CJASTAction> CJ("CJPlugin", "This is CJPlugin");
其原理主要分为三步
- 【第一步】注册插件,并自定义
AST
语法树Action
类- 继承自
PluginASTAction
,自定义ASTAction
,需要重载两个方法ParseArgs
和CreateASTConsumer
,其中的重点方法是CreateASTConsumer
,方法中有个参数CI
即编译实例对象,主要用于以下两个方面- 用于判断文件是否是用户的
- 用于抛出警告
- 通过
FrontendPluginRegistry
注册插件,需要关联插件名与自定义的ASTAction
类
- 继承自
- 【第二步】扫描配置完毕
- 继承自
ASTConsumer
类,实现自定义的子类CJASTConsumer
,有两个参数MatchFinder
对象matcher
以及CJMatchCallback
自定义的回调对象callback
- 实现构造函数,主要是创建
MatchFinder
对象,以及将CI
床底给回调对象 - 实现两个回调方法
-
HandleTopLevelDecl
:解析完一个顶级的声明,就回调一次 -
HandleTranslationUnit
:整个文件都解析完成的回调,将文件解析完毕后的上下文context
(即AST语法树) 给matcher
-
- 继承自
- 【第三步】扫描完毕的回调函数
- 继承自
MatchFinder::MatchCallback
,自定义回调类CJMatchCallback
- 定义
CompilerInstance
私有属性,用于接收ASTConsumer
类传递过来的CI
信息 - 重写
run
方法- 1、通过
result
,根据节点标记,获取相应节点,此时的标记需要与CJASTConsumer
构造方法中一致 - 2、判断节点有值,并且是用户文件即
isUserSourceCode
私有方法 - 3、获取节点的描述信息
- 4、获取节点的类型,并转成字符串
- 5、判断应该使用
copy
,但是没有使用copy
- 6、通过
CI
获取诊断引擎 - 7、通过诊断引擎报告错误
- 1、通过
- 继承自
嘿嘿,然后在终端中测试插件
在llvm
的同级目录创建我们的ClangDemo
.cd
到ClangDemo`文件夹执行下面指令
//命令格式
自己编译的clang文件路径 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.4.sdk/ -Xclang -load -Xclang 插件(.dyld)路径 -Xclang -add-plugin -Xclang 插件名 -c 源码路径
//例子
/Users/changjiang/Desktop/iOS之武功秘籍\ ⑰:Clang插件开发/build/Debug/bin/clang -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.4.sdk/ -Xclang -load -Xclang /Users/changjiang/Desktop/iOS之武功秘籍\ ⑰:Clang插件开发/build/Debug/lib/CJPlugin.dylib -Xclang -add-plugin -Xclang CJPlugin -c /Users/changjiang/Desktop/iOS之武功秘籍\ ⑰:Clang插件开发/ClangDemo/ClangDemo/ViewController.m
三、Xcode集成插件
① 加载插件
打开测试项目,在target->Build Settings -> Other C Flags
添加以下内容
-Xclang -load -Xclang /Users/changjiang/Desktop/iOS之武功秘籍\ ⑰:Clang插件开发/build/Debug/lib/CJPlugin.dylib -Xclang -add-plugin -Xclang CJPlugin
/Users/changjiang/Desktop/iOS之武功秘籍\ ⑰:Clang插件开发/build/Debug/lib/CJPlugin.dylib
是自己的CJPlugin.dylib
绝对路径
② 设置编译器
接着Command + B
编译,报错
由于clang
插件需要使用对应的版本去加载,如果版本不一致会导致编译失败,如下所示
-
在
Build Settings
栏目新增两项用户定义的设置分别是CC
和CXX
-
CC
对应的是自己编译的clang
的绝对路径 -
CXX
对应的是自己编译的clang++
的绝对路径
- 接下来在
Build Settings
中搜索index
,将Enable Index-Wihle-Building Functionality
的Default
改为NO
- 最后,重新编译测试项目,会出现下面的效果
- 修改
name
的修饰符为copy
,Command+B
编译后看,name
已经不报错了
- 恭喜你... 成功了!
写在后面
通过这个本篇的小插件,应该对语法树、编译流程,有了更深刻的认识吧...
和谐学习,不急不躁.我还是我,颜色不一样的烟火.