LLVM
概述
LLVM
是架构编译器(compiler
)的框架系统,以C++
编写而成。用于优化以任意程序语言编写的程序的编译时间(compile-time
)、链接时间(link-time
)、运行时间(run-time
)以及空闲时间(idle-time
),对开发者保持开放,并兼任已有脚本。
LLVM
计划启动于2000
年,最初由美国UIUC
大学的Chris Lattner
博士主持开展。2006
年Chris Lattner
加盟Apple Inc
,并致力于LLVM
在Apple
开放体系中的应用。
Apple
也是LLVM
计划的主要自助者。
目前LLVM
已经被苹果iOS
开发工具、Xilinx Vivado
、Facebook
、Google
等各大公司采用。
LLVM
下载
由于国内的网络限制,我们需要借助镜像下载LLVM
的源码
mirror.tuna.tsinghua.edu.cn/help/llvm/
传统编译器设计
- 源码(
Source Code
),经过编译器前端(Frontend
)→优化器(Optimizer
)→编译器后端(Backend
),生成机器代码(Machine Code
)-
机器代码(
Machine Code
):就是CPU
可执行的二进制代码 -
从源码到机器码的生成,这个过程都是编译器负责完成的
-
iOS
的编译器架构
Objective-C
、C
、C++
使用的编译器前端是Clang
,Swift
使用的编译器前端是swiftc
,而它们使用的编译器后端都是LLVM
。
各个模块的职责:
- 编译器前端(
Frontend
)-
编译器前端的任务是解析源代码。它会进行:词法分析、语法分析、语义分析,检查源代码是否存在错误,然后构建抽象语法树(
Abstract Syntax Tree, AST
); -
LLVM
的前端还会生成中间代码(intermediate representation, IR
)。
-
- 优化器(
Optimizer
)-
优化器负责进行各种优化。改善代码的运行时间,例如:消除冗余计算等。
-
- 后端(
Backend
)/代码生成器(Code Generator
)- 将代码映射到目标指令集。生成机器代码,并进行机器代码的相关优化。
LLVM
的设计
当编译器决定支持多种源语言或多种硬件框架时,LLVM
最重要的地方就来了。其他的编译器如GCC
,它方法非常成功,但由于它是作为整体应用程序设计的,因此它们的用途受到了很大的限制
LLVM
设计的最重要方面是,使用通用的代码表示形式(IR
),它是用来在编译器中表示代码的形式。所以LLVM
可以为任何编程语言独立编写前端,并且可以为任意硬件架构独立编写后端
- 简单来说,
LLVM
最大的优势,就是将编译器的前后端分离,从而提高可扩展性。
Clang
Clang
是LLVM
项目中的一个子项目;
它是基于LLVM
架构的轻量级编译器,诞生之初为了替代GCC
,提供更快的编译速度;
它是负责编译C
、C++
、Objective-C
语音的编译器,它属于整个LLVM
架构中的编译器前端;
对于开发者来说,研究Clang
可以给我们带来很多好处。
编译流程
创建main.m
文件,写入以下代码:
#import <stdio.h>
int main(int argc, const char * argv[]) {
return 0;
}
复制代码
通过命令,打印源码的编译阶段
clang -ccc-print-phases main.m
-------------------------
//输出以下内容:
+- 0: input, "main.m", objective-c
+- 1: preprocessor, {0}, objective-c-cpp-output
+- 2: compiler, {1}, ir
+- 3: backend, {2}, assembler
+- 4: assembler, {3}, object
+- 5: linker, {4}, image
6: bind-arch, "x86_64", {5}, image
复制代码
-
0:输入文件,找到源文件;
-
1:预处理阶段,这个过程包括宏的替换,头文件的导入;
-
2:编译阶段,进行词法分析、语法分析、检测语法是否正确,最终生成IR;
-
3:后端,
LLVM
会通过一个一个的Pass
去优化,每个Pass
做一些事情,最终生成汇编代码; -
4:生成
.o
目标文件; -
5:链接,链接需要的动态库和静态库,生成可执行文件;
-
6:通过不同的架构,生成对应的可执行文件。
预处理阶段
预编译阶段:将宏和导入的头文件进行替换
打开main.m
文件,写入以下代码:
#import <stdio.h>
#define C 30
int main(int argc, const char * argv[]) {
int a = 10;
int b = 20;
printf("%d",a + b + C);
return 0;
}
复制代码
通过命令,打印预处理阶段
clang -E main.m
//可生成预处理后的文件
//clang -E main.m >> main2.m
-------------------------
//输出以下内容:
# 1 "main.m"
# 1 "<built-in>" 1
# 1 "<built-in>" 3
# 379 "<built-in>" 3
# 1 "<command line>" 1
# 1 "<built-in>" 2
# 1 "main.m" 2
...
typedef signed char __int8_t;
typedef unsigned char __uint8_t;
typedef short __int16_t;
typedef unsigned short __uint16_t;
typedef int __int32_t;
typedef unsigned int __uint32_t;
typedef long long __int64_t;
typedef unsigned long long __uint64_t;
typedef long __darwin_intptr_t;
typedef unsigned int __darwin_natural_t;
...
int main(int argc, const char * argv[]) {
int a = 10;
int b = 20;
printf("%d",a + b + 30);
return 0;
}
复制代码
- 展开宏和
stdio
头文件,main
函数中原本+ C
变为+ 30
。
使用define
和typedef
的区别:
-
define
:宏定义,在预处理阶段会被替换:- 可用来做代码混淆,将
App
中核心代码,用系统相似的名称进行取别名,然后在预处理阶段就被替换,以此达到代码混淆的目的。
- 可用来做代码混淆,将
-
typedef
:对数据类型取别名,在预处理阶段不会被替换掉。
编译阶段
编译阶段可划分为三个部分:
-
词法分析;
-
语法分析;
-
生成
IR
中间代码。
词法分析
预处理完成后,就会进行词法分析,这里会把代码切成一个个Token
,例如:大小括号,等于号,还有字符串等。
命令:
clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m
//指定sdk路径
//clang -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.5.sdk -fmodules -fsyntax-only -Xclang -dump-tokens main.m
复制代码
查看词法分析之后的结果:
语法分析
词法分析完成之后就是语法分析,它的任务是验证语法是否正确。在词法分析的基础上,将单词序列组合成各类语法短语,例如:“程序”,“语句”,“表达式”等,然后将所有节点组成抽象语法树(Abstract Syntax Tree, AST
)。语法分析程序判断源程序在结构上是否正确。
命令:
clang -fmodules -fsyntax-only -Xclang -ast-dump main.m
复制代码
查看语法分析之后的结果:
重点关键字的介绍:
-
FunctionDecl
:函数 -
ParmVarDecl
:参数 -
CallExpr
:函数调用 -
BinaryOperator
:运算符
生成IR
中间代码
完成以上步骤后,就会开始生成IR中间代码,代码生成器(Code Generator
)会将语法树自顶向下遍历,逐步翻译成LLVM IR
。
通过以下命令,可以生成.ll
文件,查看IR
代码:
clang -S -fobjc-arc -emit-llvm main.m
复制代码
Objective-C
代码,在这一步会进行Runtime
的桥接:property
合成,ARC
处理等。
查看IR
中间代码:
IR
基本语法介绍:
-
@
:全局标示 -
%
:局部标示 -
alloca
:开辟空间 -
align
:内存对齐 -
i32
:32
个bit
,4字节
-
store
:写入内存 -
load
:读取数据 -
call
:调用函数 -
ret
:返回
IR
的优化
在Xcode
中,找到Target
→Build Setting
→Optimization Level
,可以对当前项目设置优化等级。
在LLVM
中,优化级别分别是-O0
、-O1
、-O2
、-O3
、-Os
(第一个是大写英文字母O
)。
通过以下命令,可设置优化等级,并生成IR
代码:
clang -Os -S -fobjc-arc -emit-llvm main.m -o main.ll
复制代码
查看优化后的IR
代码:
main
函数中的代码优化的非常简短,直接计算出结果并返回。
Bitcode
Xcode7
以后,开启Bitcode
设置,苹果会做进一步的优化,生成.bc
中间代码。
命令:
clang -emit-llvm -c main.ll -o main.bc
复制代码
什么是Bitcode
?
Bitcode
是被编译程序的一种中间形式的代码。包含Bitcode
并上传到App Store Connect
的App
,会在App Store
上编译和链接。包含Bitcode
可以在不提交新版本App
的情况下,允许Apple
在将来的时候再次优化你的App
二进制文件。
在Xcode
中,默认开启Bitcode
设置。如果你的App
支持Bitcode
,App
使用到的其他二进制形式也要支持Bitcode
,否则就会报错。
解决Bitcode
报错只有两种方案:
-
【方案一】将不支持
Bitcode
的SDK
移除掉,或等待第三方更新。 -
【方案二】:将使用
Bitcode
的选项设置为NO
。
生成汇编代码
通过最终的.ll
或.bc
代码,生成汇编代码。
命令:
clang -S -fobjc-arc main.ll -o main.s
clang -S -fobjc-arc main.bc -o main.s
复制代码
查看汇编代码:
汇编代码也可以设置OPT
的优化等级进行优化。
clang -Os -S -fobjc-arc main.ll -o main.s
复制代码
查看优化后的汇编代码:
生成目标文件(汇编器)
目标文件的生成,是汇编器以汇编代码作为输入,将汇编代码转换为机器代码,最后输出目标文件(object file
)。
命令:
clang -fmodules -c main.s -o main.o
复制代码
通过nm
命令,查看main.o
中的符号:
xcrun nm -nm main.o
-------------------------
//输出以下内容:
(undefined) external _printf
0000000000000000 (__TEXT,__text) external _main
复制代码
-
_printf
函数,被标记为undefined external
:-
undefined
:表示在当前文件中,暂时找不到符号。因为printf
为外部函数,链接后才能找到符号所属动态库。 -
external
:表示这个符号在外部是可以被访问的。
-
生成可执行文件(链接)
链接:将多个目标文件合并,符号表(包括重定位符号表)合并成一张表,经过链接最后,会分配虚拟内存地址,最终生成可执行文件或动态库。
这个过程还会链接需要的动态库和静态库
-
静态库,和可执行文件合并。
-
动态库,独立存在,运行时,由
dyld
动态加载。
使用以下命令,生成可执行文件:
clang main.o -o main
复制代码
查看链接后可执行文件的符号:
xcrun nm -nm main
-------------------------
//输出以下内容:
(undefined) external _printf (from libSystem)
(undefined) external dyld_stub_binder (from libSystem)
0000000100000000 (__TEXT,__text) [referenced dynamically] external __mh_execute_header
0000000100003f77 (__TEXT,__text) external _main
0000000100008008 (__DATA,__data) non-external __dyld_private
复制代码
-
链接后,
_printf
符号可以找到所属的动态库,但依然被标记为undefined
。因为libSystem
属于系统动态库,在运行时进行动态绑定。 -
链接后,还多了
dyld_stub_binder
符号,它在运行时用于符号的重绑定:-
以
printf
函数为例,printf
函数存在于libSystem
系统库中,它存在于懒加载符号表中。它的函数地址在运行时,首次对printf
函数进行调用,才会通过dyld_stub_binder
进行重绑定。 -
而
dyld_stub_binder
函数地址的绑定时机:当dyld
加载主程序时,符号被dyld
直接绑定。
-
Clang
插件
编写一个Clang
插件,实现效果:定义NSString
、NSArray
、NSDictionary
类型的属性,未使用copy
修饰,对该属性提示警告。
下载LLVM
下载LLVM
项目:
git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm/llvm.git
复制代码
在LLVM
的tools
目录下,下载Clang
:
cd llvm/tools
git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm/clang.git
复制代码
在LLVM
的projects
目录下,下载compiler-rt
、libcxx
、libcxxabi
:
cd ../projects
git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm/compiler-rt.g
it
git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm/libcxx.git
git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm/libcxxabi.git
复制代码
在Clang
的tools
下,安装extra
工具:
cd ../tools/clang/tools
git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm/clang-tools-e
xtra.git
复制代码
安装cmake
使用brew
命令,查看是否安装cmake
,如果已安装,跳过此步骤:
brew list
复制代码
通过brew
安装cmake
:
brew install cmake
复制代码
编译LLVM
通过Xcode
编译LLVM
cmake
编译成Xcode
项目:
mkdir build_xcode
cd build_xcode
cmake -G Xcode ../llvm
复制代码
使用Xcode
编译Clang
选择手动管理Schemes
点击左下⻆加号,在Target
中添加clang
和clangTooling
通过Run Without Building
运⾏,代码没有改变的时候,不需要重新编译,直接运⾏现有可执⾏⽂件即可:
通过ninja
编译LLVM
安装ninja
安装`ninja`
复制代码
在LLVM
源码根目录下,新建一个build_ninja
目录,最终会在build_ninja
目录下生成build.ninja
。
在LLVM
源码根目录下,新建一个llvm_release
目录,最终编译文件会在llvm_release
文件夹路径下。
cd llvm_build
cmake -G Ninja ../llvm -DCMAKE_INSTALL_PREFIX=安装路径
复制代码
- 本机为
/Users/xxx/xxx/LLVM/llvm_release
,注意DCMAKE_INSTALL_PREFIX
后面不能有空格。
依次执行编译、安装指令
ninja
ninja install
复制代码
创建插件
在/llvm/tools/clang/tools
目录下,新建插件HKPlugin
修改/llvm/tools/clang/tools
目录下的CMakeLists.txt
文件
新增add_clang_subdirectory(HKPlugin)
:
在HKPlugin
目录下,新建HKPlugi.cpp
和CMakeLists.txt
文件:
打开CMakeLists.txt
文件,写入以下内容:
add_llvm_library( HKPlugin MODULE BUILDTREE_ONLY HKPlugin.cpp )
复制代码
利用cmake
重新生成Xcode
项目,在build_xcode
目录中执行cmake
命令:
cmake -G Xcode ../llvm
复制代码
最后,可以在LLVM
的Xcode
项目中,在Loadable modules
目录下找到自定义Plugin
目录:
- 打开
HKPlugi.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;
复制代码
定义命名空间、定义HKASTAction
类,继承自系统的PluginASTAction
类:
namespace HKPlugin {
class HKASTAction:public PluginASTAction{
};
}
复制代码
注册插件:
static FrontendPluginRegistry::Add<HKPlugin::HKASTAction> X("HKPlugin","this is the description");
复制代码
参数1
:插件名称参数2
:插件描述
现有的需求分为三个步骤:
-
【第一步】读取代码
-
【第二步】找到目标类型定义的属性和修饰符
-
【第三步】不符合标准,提示警告
实现需求的第一步读取代码,需要用到AST
语法树,然后对AST
节点进行解析
我们可以使用以下两个函数:
-
CreateASTConsumer
-
ParseArgs
在HKASTAction
类中,重写CreateASTConsumer
和ParseArgs
函数
namespace HKPlugin {
class HKASTAction:public PluginASTAction {
public:
std::unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &CI, StringRef InFile) { return unique_ptr<ASTConsumer> (new ASTConsumer);
}
bool ParseArgs(const CompilerInstance &CI, const std::vector<std::string> &arg) {
return true;
}
};
}
复制代码
ASTConsumer
是系统提供的基类,作为基类,它的作用大多有两种:
-
抽取代码;
-
由开发者继承,实现它的子类,对其进行扩展。
所以,我们不能直接使用ASTConsumer
,需要对其进行继承,实现自定义子类。
namespace HKPlugin {
class HKConsumer:public ASTConsumer {
public:
bool HandleTopLevelDecl(DeclGroupRef D) {
cout<<"正在解析..."<<endl;
return true;
}
void HandleTranslationUnit(ASTContext &Ctx) {
cout<<"文件解析完成..."<<endl;
}
};
class HKASTAction:public PluginASTAction {
public:
std::unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &CI, StringRef InFile) {
return unique_ptr<HKConsumer> (new HKConsumer);
}
bool ParseArgs(const CompilerInstance &CI, const std::vector<std::string> &arg) {
return true;
}
};
}
复制代码
重写HandleTopLevelDecl
和HandleTranslationUnit
函数
-
HandleTopLevelDecl
:顶级节点解析回调函数,顶级节点,例如:全局变量、函数定义、属性; -
HandleTranslationUnit
:整个文件解析完成后的回调。
编译HKPlugin
项目,在项目的Products
目录下,找到编译出的clang
可执行文件:
同样在Products
目录下,找到HKPlugin.dylib
使用插件,测试文件和顶级节点的解析。
创建hello.m
文件,写入以下代码:
int sum(int a);
int a;
int sum(int a){
int b = 10;
return 10 + b;
}
int sum2(int a,int b){
int c = 10;
return a + b + c;
}
复制代码
使用以下命令,测试插件
//自己编译的clang路径 -isysroot/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.5.sdk/ -Xclang -load -Xclang 插件(.dylib)路径 -Xclang -add-plugin -Xclang 插件名称 -c 源码路径
/Volumes/study/Source/llvm-hk/build_xcode/Debug/bin/clang -isysroot/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.5.sdk/ -Xclang -load -Xclang /Volumes/study/Source/llvm-hk/build_xcode/Debug/lib/HKPlugin.dylib -Xclang -add-plugin -XclangHKPlugin -c hello.m
-------------------------
//输出以下内容:
正在解析...
正在解析...
正在解析...
正在解析...
文件解析完成...
复制代码
- 共解析出四个顶级节点。
分析OC
代码
搭建App
项目,打开ViewController.m
文件,写入以下代码:
#import "ViewController.h"
@interface ViewController ()
@property(nonatomic, strong) NSString* name;
@property(nonatomic, strong) NSArray* arrs;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
}
@end
复制代码
生成AST
代码,找到属性的声明
- 在
ObjCPropertyDecl
节点中,可以找到属性的声明,包含属性的类型和修饰符。
AST
节点的过滤
系统API
提供MatchFinder
,用于AST
语法树节点的查找。
其中addMatcher函数,可以查找指定节点:
void addMatcher(const DeclarationMatcher &NodeMatch, MatchCallback *Action);
复制代码
参数1
:设置指定节点;参数2
:执行回调,此处并非使用回调函数,而是一个回调类。需要继承MatchCallback
系统类,实现自己的子类。
添加MatchFinder
所在命名空间:
using namespace clang::ast_matchers;
复制代码
实现HKMatchHandler
回调类,继承自MatchCallback
:
class HKMatchHandler:public MatchFinder::MatchCallback {
public:
void run(const MatchFinder::MatchResult &Result) {
const ObjCPropertyDecl *propertyDecl = Result.Nodes.getNodeAs<ObjCPropertyDecl>("objcPropertyDecl"); ,
if(propertyDecl) {
string typeStr = propertyDecl->getType().getAsString();
cout<<"------拿到了:"<<typeStr<<endl;
}
}
};
复制代码
-
必须实现
run
函数,它就是真正的回调函数; -
通过
Result
结果,获取节点对象; -
通过节点对象的
getType().getAsString()
,以字符串的形式返回属性类型。
在HKConsumer
类中,定义私有MatchFinder
和HKMatchHandler
,重写构造方法,添加AST
节点过滤器:
class HKConsumer:public ASTConsumer {
private:
MatchFinder matcher;
HKMatchHandler handler;
public:
HKConsumer() {
matcher.addMatcher(objcPropertyDecl().bind("objcPropertyDecl"), &handler);
}
};
复制代码
- 解析语法树,查找
objcPropertyDecl
节点。
在文件解析完成的回调函数中,调用matcher
的matchAST
函数,将文件的语法树传入过滤器:
void HandleTranslationUnit(ASTContext &Ctx) {
cout<<"文件解析完成..."<<endl;
matcher.matchAST(Ctx);
}
复制代码
测试插件
-
通过语法树分析,可以找到属性的声明,包含属性的类型和修饰符;
-
但也存在一些问题,在预处理阶段,头文件会被展开,我们可能会获取到系统头文件中的属性,所以我们要想办法过滤掉系统文件中的代码。
过滤系统文件
可以通过文件路径判断系统文件,因为系统文件都存在于/Applications/Xcode.app/
开头的目录中。
在PluginASTAction
类中,存在CompilerInstance
类型的CI
参数:
std::unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &CI, StringRef InFile) override = 0;
复制代码
CI
为编译器实例对象,可以通过它获取到文件路径,以及警告的提示。
重写HKConsumer
的构造函数,增加CI
参数:
HKConsumer(CompilerInstance &CI) {
matcher.addMatcher(objcPropertyDecl().bind("objcPropertyDecl"), &handler);
}
复制代码
在HKASTAction
类中,创建ASTConsumer
时,将CI
传入:
std::unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &CI, StringRef InFile) {
return unique_ptr<HKConsumer> (new HKConsumer(CI));
}
复制代码
重写HKMatchHandler
的构造函数,增加CI
参数。定义私有CompilerInstance
,通过构造函数对其赋值:
class HKMatchHandler:public MatchFinder::MatchCallback {
private:
CompilerInstance &CI;
public:
HKMatchHandler(CompilerInstance &CI):CI(CI){
}
};
复制代码
在HKConsumer
的构造函数中,对HKMatchHandler
中的CI
进行传递:
HKConsumer(CompilerInstance &CI):handler(CI) {
matcher.addMatcher(objcPropertyDecl().bind("objcPropertyDecl"), &handler);
}
复制代码
在HKMatchHandler
使用CI
,获取文件路径并进行过滤:
class HKMatchHandler:public MatchFinder::MatchCallback {
private:
CompilerInstance &CI;
bool isUserSourceCode(const string fileName) {
if(fileName.empty()) {
return false;
}
if(fileName.find("/Applications/Xcode.app/")==0) {
return false;
}
return true;
}
public:
HKMatchHandler(CompilerInstance &CI):CI(CI) {
}
void run(const MatchFinder::MatchResult &Result) {
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();
cout<<"------拿到了:"<<typeStr<<endl;
}
}
};
复制代码
-
通过
CI.getSourceManager().getFilename
获取文件名称,包含文件路径; -
需要传入
SourceLocation
,可以通过节点的propertyDecl->getSourceRange().getBegin()
获得; -
实现
isUserSourceCode
函数,判断路径非空,并且非/Applications/Xcode.app/
目录开头,视为自定义文件。
测试插件
文件解析完成...
------拿到了:NSString *
------拿到了:NSArray *
复制代码
- 成功过滤系统文件,获取到自定义文件中的两个属性。
判断属性的类型
实现isShouldUseCopy
函数,传入属性类型,判断当前类型是否为必须使用copy
修饰的类型:
class HKMatchHandler:public MatchFinder::MatchCallback {
private:
CompilerInstance &CI;
bool isUserSourceCode(const string fileName) {
if(fileName.empty()) {
return false;
}
if(fileName.find("/Applications/Xcode.app/")==0) {
return false;
}
return true;
}
bool isShouldUseCopy(const string typeStr) {
if(typeStr.find("NSString") != string::npos || typeStr.find("NSArray") != string::npos || typeStr.find("NSDictionary") != string::npos) {
return true;
}
return false;
}
public:
HKMatchHandler(CompilerInstance &CI):CI(CI){
}
void run(const MatchFinder::MatchResult &Result) {
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();
if(isShouldUseCopy(typeStr)){ cout<<"------拿到了:"<<typeStr<<endl;
}
}
}
};
复制代码
在ViewController.m
中,增加其他类型的属性声明
#import "ViewController.h"
@interface ViewController ()
@property(nonatomic, strong) NSString* name;
@property(nonatomic, strong) NSArray* arrs;
@property(nonatomic, strong) id objc;
@property(nonatomic, strong) NSSet *sets;
@property(nonatomic, strong) NSDictionary * dict;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
}
@end
复制代码
测试插件
文件解析完成...
------拿到了:NSString *
------拿到了:NSArray *
------拿到了:NSDictionary *
复制代码
- 成功过滤其他类型的属性
判断属性的修饰符
通过propertyDecl->getPropertyAttributes()
获取属性修饰符,和OBJC_PR_copy
进行位与运算:
void run(const MatchFinder::MatchResult &Result) {
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();
ObjCPropertyDecl::PropertyAttributeKind attr = propertyDecl->getPropertyAttributes();
if(isShouldUseCopy(typeStr) && !(attr & ObjCPropertyDecl::OBJC_PR_copy)){
cout<<"------请使用copy修饰:"<<typeStr<<endl;
}
}
}
复制代码
测试插件:
文件解析完成...
------请使用copy修饰:NSString *
------请使用copy修饰:NSArray *
------请使用copy修饰:NSDictionary *
复制代码
提示警告信息
当判断目标类型使用非copy
修饰,目前只是内容打印,正确的做法在Xcode
中提示警告信息
使用编译器实例对象CI
提示警告信息:
void run(const MatchFinder::MatchResult &Result) {
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();
ObjCPropertyDecl::PropertyAttributeKind attr = propertyDecl->getPropertyAttributes();
if(isShouldUseCopy(typeStr) && !(attr & ObjCPropertyDecl::OBJC_PR_copy)) {
DiagnosticsEngine &diag = CI.getDiagnostics();
diag.Report(propertyDecl->getLocation(), diag.getCustomDiagID(DiagnosticsEngine::Warning, "请使用copy修饰"));
}
}
}
复制代码
-
通过
CI
的getDiagnostics
函数,获取诊断引擎,需要传入位置和DiagID
; -
通过节点获取位置,使用
propertyDecl->getLocation()
获得当前节点的位置; -
通过
diag.getCustomDiagID
获取DiagID
,设置提示级别和文案。
测试插件:
文件解析完成...
ViewController.m:12:40: warning: 请使用copy修饰
@property(nonatomic, strong) NSString* name;
^
ViewController.m:13:39: warning: 请使用copy修饰
@property(nonatomic, strong) NSArray* arrs;
^
ViewController.m:16:45: warning: 请使用copy修饰
@property(nonatomic, strong) NSDictionary * dict;
^
3 warnings generated.
复制代码
Xcode
集成插件
打开测试项目,在Xcode
中注册插件,来到Build Settings
→Other C Flags
:
//-Xclang -load -Xclang (.dylib)插件路径 -Xclang -add-plugin -Xclang 插件名称
-Xclang -load -Xclang /Volumes/study/Source/llvm-hk/build_xcode/Debug/lib/HKPlugin.dylib -Xclang -add-plugin -Xclang HKPlugin
复制代码
在Xcode
中替换Clang
,来到Build Settings
中新增两项用户自定义设置:
、
分别添加CC
和CXX
:
-
CC
对应自己编译的Clang
绝对路径; -
CXX
对应自己编译的Clang++
绝对路径。
/Volumes/study/Source/llvm-hk/build_xcode/Debug/bin/clang
/Volumes/study/Source/llvm-hk/build_xcode/Debug/bin/clang++
复制代码
在Build Settings
中,将Enable Index-Wihle-Building Functionality
设置为NO
:
测试插件