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:
测试插件