1. LLVM概述

LLVM是架构编译器(compiler)的框架系统,以C++编写而成。用于优化以任意程序语言编写的程序的编译时间(compile-time)、链接时间(link-time)、运行时间(run-time)以及空闲时间(idle-time),对开发者保持开放,并兼任已有脚本

LLVM计划启动于2000年,最初由美国UIUC大学的Chris Lattner博士主持开展。2006Chris Lattner加盟Apple Inc,并致力于LLVMApple开放体系中的应用

Apple也是LLVM计划的主要自助者

目前LLVM已经被苹果iOS开发工具、Xilinx VivadoFacebookGoogle等各大公司采用

1.1 传统编译器设计

image.png

  • 源码(Source Code),经过编译器前端(Frontend)→优化器(Optimizer)→编译器后端(Backend),生成机器代码(Machine Code

    • 机器代码(Machine Code):就是CPU可执行的二进制代码

    • 从源码到机器码的生成,这个过程都是编译器负责完成的

1.2 iOS的编译器架构

Objective-CCC++使用的编译器前端是ClangSwift使用的编译器前端是swiftc,而它们使用的编译器后端都是LLVM
image.png

各个模块的职责:

  • 编译器前端(Frontend

    • 编译器前端的任务是解析源代码。它会进行:词法分析、语法分析、语义分析,检查源代码是否存在错误,然后构建抽象语法树(Abstract Syntax Tree, AST

    • LLVM的前端还会生成中间代码(intermediate representation, IR

  • 优化器(Optimizer

    • 优化器负责进行各种优化。改善代码的运行时间,例如:消除冗余计算等
  • 后端(Backend)/代码生成器(Code Generator

    • 将代码映射到目标指令集。生成机器代码,并进行机器代码的相关优化

1.3 LLVM的设计

当编译器决定支持多种源语言或多种硬件框架时,LLVM最重要的地方就来了。其他的编译器如GCC,它方法非常成功,但由于它是作为整体应用程序设计的,因此它们的用途受到了很大的限制

LLVM设计的最重要方面是,使用通用的代码表示形式(IR),它是用来在编译器中表示代码的形式。所以LLVM可以为任何编程语言独立编写前端,并且可以为任意硬件架构独立编写后端
image.png

  • 简单来说,LLVM最大的优势,就是将编译器的前后端分离,从而提高可扩展性

1.4 Clang

ClangLLVM项目中的一个子项目

它是基于LLVM架构的轻量级编译器,诞生之初为了替代GCC,提供更快的编译速度

它是负责编译CC++Objective-C语音的编译器,它属于整个LLVM架构中的编译器前端

对于开发者来说,研究Clang可以给我们带来很多好处

2. 编译流程

创建main.m文件,写入以下代码:

  1. #import <stdio.h>
  2. int main(int argc, const char * argv[]) {
  3. return 0;
  4. }

通过命令,打印源码的编译阶段

  1. clang -ccc-print-phases main.m
  2. -------------------------
  3. //输出以下内容:
  4. +- 0: input, "main.m", objective-c
  5. +- 1: preprocessor, {0}, objective-c-cpp-output
  6. +- 2: compiler, {1}, ir
  7. +- 3: backend, {2}, assembler
  8. +- 4: assembler, {3}, object
  9. +- 5: linker, {4}, image
  10. 6: bind-arch, "x86_64", {5}, image
  • 0:输入文件,找到源文件
  • 1:预处理阶段,这个过程包括宏的替换,头文件的导入
  • 2:编译阶段,进行词法分析、语法分析、检测语法是否正确,最终生成IR
  • 3:后端,LLVM会通过一个一个的Pass去优化,每个Pass做一些事情,最终生成汇编代码
  • 4:生成.o目标文件
  • 5:链接,链接需要的动态库和静态库,生成可执行文件
  • 6:通过不同的架构,生成对应的可执行文件

2.1 预处理阶段

预编译阶段:将宏和导入的头文件进行替换

打开main.m文件,写入以下代码:

  1. #import <stdio.h>
  2. #define C 30
  3. int main(int argc, const char * argv[]) {
  4. int a = 10;
  5. int b = 20;
  6. printf("%d",a + b + C);
  7. return 0;
  8. }

通过命令,打印预处理阶段

  1. clang -E main.m
  2. //可生成预处理后的文件
  3. //clang -E main.m >> main2.m
  4. -------------------------
  5. //输出以下内容:
  6. # 1 "main.m"
  7. # 1 "<built-in>" 1
  8. # 1 "<built-in>" 3
  9. # 379 "<built-in>" 3
  10. # 1 "<command line>" 1
  11. # 1 "<built-in>" 2
  12. # 1 "main.m" 2
  13. ...
  14. typedef signed char __int8_t;
  15. typedef unsigned char __uint8_t;
  16. typedef short __int16_t;
  17. typedef unsigned short __uint16_t;
  18. typedef int __int32_t;
  19. typedef unsigned int __uint32_t;
  20. typedef long long __int64_t;
  21. typedef unsigned long long __uint64_t;
  22. typedef long __darwin_intptr_t;
  23. typedef unsigned int __darwin_natural_t;
  24. ...
  25. int main(int argc, const char * argv[]) {
  26. int a = 10;
  27. int b = 20;
  28. printf("%d",a + b + 30);
  29. return 0;
  30. }
  • 展开宏和stdio头文件,main函数中原本+ C变为+ 30

使用definetypedef的区别:

  • define:宏定义,在预处理阶段会被替换

    • 可用来做代码混淆,将App中核心代码,用系统相似的名称进行取别名,然后在预处理阶段就被替换,以此达到代码混淆的目的
  • typedef:对数据类型取别名,在预处理阶段不会被替换掉

2.2 编译阶段

编译阶段可划分为三个部分:

  • 词法分析

  • 语法分析

  • 生成IR中间代码

2.2.1 词法分析

预处理完成后,就会进行词法分析,这里会把代码切成一个个Token,例如:大小括号,等于号,还有字符串等

命令:

  1. clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m
  2. //指定sdk路径
  3. //clang -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.5.sdk -fmodules -fsyntax-only -Xclang -dump-tokens main.m

查看词法分析之后的结果:
image.png

2.2.2 语法分析

词法分析完成之后就是语法分析,它的任务是验证语法是否正确。在词法分析的基础上,将单词序列组合成各类语法短语,例如:“程序”,“语句”,“表达式”等,然后将所有节点组成抽象语法树(Abstract Syntax Tree, AST)。语法分析程序判断源程序在结构上是否正确

命令:

  1. clang -fmodules -fsyntax-only -Xclang -ast-dump main.m

查看语法分析之后的结果:
image.png

重点关键字的介绍:

  • FunctionDecl:函数

  • ParmVarDecl:参数

  • CallExpr:函数调用

  • BinaryOperator:运算符

2.2.3 生成IR中间代码

完成以上步骤后,就会开始生成IR中间代码,代码生成器(Code Generator)会将语法树自顶向下遍历,逐步翻译成LLVM IR

通过以下命令,可以生成.ll文件,查看IR代码:

  1. clang -S -fobjc-arc -emit-llvm main.m
  • Objective-C代码,在这一步会进行Runtime的桥接:property合成,ARC处理等

查看IR中间代码:
image.png

IR基本语法介绍:

  • @:全局标示

  • %:局部标示

  • alloca:开辟空间

  • align:内存对齐

  • i3232bit4字节

  • store:写入内存

  • load:读取数据

  • call:调用函数

  • ret:返回

2.2.4 IR的优化

Xcode中,找到TargetBuild SettingOptimization Level,可以对当前项目设置优化等级

LLVM中,优化级别分别是-O0-O1-O2-O3-Os(第一个是大写英文字母O

通过以下命令,可设置优化等级,并生成IR代码:

  1. clang -Os -S -fobjc-arc -emit-llvm main.m -o main.ll

查看优化后的IR代码:
image.png

  • main函数中的代码优化的非常简短,直接计算出结果并返回

2.2.5 Bitcode

Xcode7以后,开启Bitcode设置,苹果会做进一步的优化,生成.bc中间代码

命令:

  1. clang -emit-llvm -c main.ll -o main.bc

什么是Bitcode

Bitcode是被编译程序的一种中间形式的代码。包含Bitcode并上传到App Store ConnectApp,会在App Store上编译和链接。包含Bitcode可以在不提交新版本App的情况下,允许Apple在将来的时候再次优化你的App二进制文件

Xcode中,默认开启Bitcode设置。如果你的App支持BitcodeApp使用到的其他二进制形式也要支持Bitcode,否则就会报错

解决Bitcode报错只有两种方案:

  • 【方案一】将不支持BitcodeSDK移除掉,或等待第三方更新

  • 【方案二】:将使用Bitcode的选项设置为NO

2.3 生成汇编代码

通过最终的.ll.bc代码,生成汇编代码

命令:

  1. clang -S -fobjc-arc main.ll -o main.s
  2. clang -S -fobjc-arc main.bc -o main.s

查看汇编代码:
image.png

汇编代码也可以设置OPT的优化等级进行优化

  1. clang -Os -S -fobjc-arc main.ll -o main.s

查看优化后的汇编代码:
image.png

2.4 生成目标文件(汇编器)

目标文件的生成,是汇编器以汇编代码作为输入,将汇编代码转换为机器代码,最后输出目标文件(object file

命令:

  1. clang -fmodules -c main.s -o main.o

通过nm命令,查看main.o中的符号:

  1. xcrun nm -nm main.o
  2. -------------------------
  3. //输出以下内容:
  4. (undefined) external _printf
  5. 0000000000000000 (__TEXT,__text) external _main
  • _printf函数,被标记为undefined external

    • undefined:表示在当前文件中,暂时找不到符号。因为printf为外部函数,链接后才能找到符号所属动态库

    • external:表示这个符号在外部是可以被访问的

2.5 生成可执行文件(链接)

链接:将多个目标文件合并,符号表(包括重定位符号表)合并成一张表,经过链接最后,会分配虚拟内存地址,最终生成可执行文件或动态库

这个过程还会链接需要的动态库和静态库

  • 静态库,和可执行文件合并

  • 动态库,独立存在,运行时,由dyld动态加载

使用以下命令,生成可执行文件:

  1. clang main.o -o main

查看链接后可执行文件的符号:

  1. xcrun nm -nm main
  2. -------------------------
  3. //输出以下内容:
  4. (undefined) external _printf (from libSystem)
  5. (undefined) external dyld_stub_binder (from libSystem)
  6. 0000000100000000 (__TEXT,__text) [referenced dynamically] external __mh_execute_header
  7. 0000000100003f77 (__TEXT,__text) external _main
  8. 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直接绑定

3. Clang插件

编写一个Clang插件,实现效果:定义NSStringNSArrayNSDictionary类型的属性,未使用copy修饰,对该属性提示警告

3.1 下载LLVM

由于国内的网络限制,需要借助镜像下载LLVM的源码:https://mirror.tuna.tsinghua.edu.cn/help/llvm/

下载LLVM项目

  1. git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm/llvm.git

LLVMtools目录下,下载Clang

  1. cd llvm/tools
  2. git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm/clang.git

LLVMprojects目录下,下载compiler-rtlibcxxlibcxxabi

  1. cd ../projects
  2. git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm/compiler-rt.g
  3. it
  4. git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm/libcxx.git
  5. git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm/libcxxabi.git

Clangtools下,安装extra工具

  1. cd ../tools/clang/tools
  2. git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm/clang-tools-e
  3. xtra.git

3.2 安装cmake

使用brew命令,查看是否安装cmake,如果已安装,跳过此步骤

  1. brew list

通过brew安装cmake

  1. brew install cmake

3.3 编译LLVM

3.3.1 通过Xcode编译LLVM

cmake编译成Xcode项目

  1. mkdir build_xcode
  2. cd build_xcode
  3. cmake -G Xcode ../llvm

使用Xcode编译Clang

选择手动管理Schemes
image.png

点击左下⻆加号,在Target中添加clangclangTooling
image.png

通过Run Without Building运⾏,代码没有改变的时候,不需要重新编译,直接运⾏现有可执⾏⽂件即可
image.png

3.3.2 通过ninja编译LLVM

安装ninja

  1. brew install ninja

LLVM源码根目录下,新建一个build_ninja目录,最终会在build_ninja目录下生成build.ninja

LLVM源码根目录下,新建一个llvm_release目录,最终编译文件会在llvm_release文件夹路径下

  1. cd llvm_build
  2. cmake -G Ninja ../llvm -DCMAKE_INSTALL_PREFIX=安装路径
  • 本机为/Users/xxx/xxx/LLVM/llvm_release,注意DCMAKE_INSTALL_PREFIX后面不能有空格

依次执行编译、安装指令

  1. ninja
  2. ninja install

3.4 创建插件

/llvm/tools/clang/tools目录下,新建插件HKPlugin
image.png

修改/llvm/tools/clang/tools目录下的CMakeLists.txt文件
image.png

新增add_clang_subdirectory(HKPlugin)
image.png

HKPlugin目录下,新建HKPlugi.cppCMakeLists.txt文件
image.png

打开CMakeLists.txt文件,写入以下内容:

  1. add_llvm_library( HKPlugin MODULE BUILDTREE_ONLY
  2. HKPlugin.cpp
  3. )

利用cmake重新生成Xcode项目,在build_xcode目录中执行cmake命令

  1. cmake -G Xcode ../llvm

最后,可以在LLVMXcode项目中,在Loadable modules目录下找到自定义Plugin目录
image.png

  • 打开HKPlugi.cpp文件,可以在里面编写插件代码

3.5 编写插件代码

3.5.1 文件和顶级节点的解析

导入插件使用的头文件和命名空间

  1. #include <iostream>
  2. #include "clang/AST/AST.h"
  3. #include "clang/AST/DeclObjC.h"
  4. #include "clang/AST/ASTConsumer.h"
  5. #include "clang/ASTMatchers/ASTMatchers.h"
  6. #include "clang/Frontend/CompilerInstance.h"
  7. #include "clang/ASTMatchers/ASTMatchFinder.h"
  8. #include "clang/Frontend/FrontendPluginRegistry.h"
  9. using namespace clang;
  10. using namespace std;
  11. using namespace llvm;

定义命名空间、定义HKASTAction类,继承自系统的PluginASTAction

  1. namespace HKPlugin {
  2. class HKASTAction:public PluginASTAction{
  3. };
  4. }

注册插件

  1. static FrontendPluginRegistry::Add<HKPlugin::HKASTAction> X("HKPlugin","this is the description");
  • 参数1:插件名称
  • 参数2:插件描述

现有的需求分为三个步骤:

  • 【第一步】读取代码

  • 【第二步】找到目标类型定义的属性和修饰符

  • 【第三步】不符合标准,提示警告

实现需求的第一步读取代码,需要用到AST语法树,然后对AST节点进行解析

我们可以使用以下两个函数:

  • CreateASTConsumer

  • ParseArgs

HKASTAction类中,重写CreateASTConsumerParseArgs函数

  1. namespace HKPlugin {
  2. class HKASTAction:public PluginASTAction{
  3. public:
  4. std::unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &CI, StringRef InFile) {
  5. return unique_ptr<ASTConsumer> (new ASTConsumer);
  6. }
  7. bool ParseArgs(const CompilerInstance &CI, const std::vector<std::string> &arg) {
  8. return true;
  9. }
  10. };
  11. }

ASTConsumer是系统提供的基类,作为基类,它的作用大多有两种

  • 抽取代码

  • 由开发者继承,实现它的子类,对其进行扩展

所以,我们不能直接使用ASTConsumer,需要对其进行继承,实现自定义子类

  1. namespace HKPlugin {
  2. class HKConsumer:public ASTConsumer{
  3. public:
  4. bool HandleTopLevelDecl(DeclGroupRef D) {
  5. cout<<"正在解析..."<<endl;
  6. return true;
  7. }
  8. void HandleTranslationUnit(ASTContext &Ctx) {
  9. cout<<"文件解析完成..."<<endl;
  10. }
  11. };
  12. class HKASTAction:public PluginASTAction{
  13. public:
  14. std::unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &CI, StringRef InFile) {
  15. return unique_ptr<HKConsumer> (new HKConsumer);
  16. }
  17. bool ParseArgs(const CompilerInstance &CI, const std::vector<std::string> &arg) {
  18. return true;
  19. }
  20. };
  21. }

重写HandleTopLevelDeclHandleTranslationUnit函数

  • HandleTopLevelDecl:顶级节点解析回调函数,顶级节点,例如:全局变量、函数定义、属性

  • HandleTranslationUnit:整个文件解析完成后的回调

编译HKPlugin项目,在项目的Products目录下,找到编译出的clang可执行文件
image.png

同样在Products目录下,找到HKPlugin.dylib
image.png

使用插件,测试文件和顶级节点的解析

创建hello.m文件,写入以下代码:

  1. int sum(int a);
  2. int a;
  3. int sum(int a){
  4. int b = 10;
  5. return 10 + b;
  6. }
  7. int sum2(int a,int b){
  8. int c = 10;
  9. return a + b + c;
  10. }

使用以下命令,测试插件

  1. //自己编译的clang路径 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.5.sdk/ -Xclang -load -Xclang 插件(.dylib)路径 -Xclang -add-plugin -Xclang 插件名称 -c 源码路径
  2. /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 -Xclang HKPlugin -c hello.m
  3. -------------------------
  4. //输出以下内容:
  5. 正在解析...
  6. 正在解析...
  7. 正在解析...
  8. 正在解析...
  9. 文件解析完成...
  • 共解析出四个顶级节点

3.5.2 分析OC代码

搭建App项目,打开ViewController.m文件,写入以下代码:

  1. #import "ViewController.h"
  2. @interface ViewController ()
  3. @property(nonatomic, strong) NSString* name;
  4. @property(nonatomic, strong) NSArray* arrs;
  5. @end
  6. @implementation ViewController
  7. - (void)viewDidLoad {
  8. [super viewDidLoad];
  9. }
  10. @end

生成AST代码,找到属性的声明
image.png

  • ObjCPropertyDecl节点中,可以找到属性的声明,包含属性的类型和修饰符

3.5.3 AST节点的过滤

系统API提供MatchFinder,用于AST语法树节点的查找

其中addMatcher函数,可以查找指定节点

  1. void addMatcher(const DeclarationMatcher &NodeMatch,
  2. MatchCallback *Action);
  • 参数1:设置指定节点
  • 参数2:执行回调,此处并非使用回调函数,而是一个回调类。需要继承MatchCallback系统类,实现自己的子类

添加MatchFinder所在命名空间

  1. using namespace clang::ast_matchers;

实现HKMatchHandler回调类,继承自MatchCallback

  1. class HKMatchHandler:public MatchFinder::MatchCallback{
  2. public:
  3. void run(const MatchFinder::MatchResult &Result) {
  4. const ObjCPropertyDecl *propertyDecl = Result.Nodes.getNodeAs<ObjCPropertyDecl>("objcPropertyDecl");
  5. if(propertyDecl){
  6. string typeStr = propertyDecl->getType().getAsString();
  7. cout<<"------拿到了:"<<typeStr<<endl;
  8. }
  9. }
  10. };
  • 必须实现run函数,它就是真正的回调函数
  • 通过Result结果,获取节点对象
  • 通过节点对象的getType().getAsString(),以字符串的形式返回属性类型

HKConsumer类中,定义私有MatchFinderHKMatchHandler,重写构造方法,添加AST节点过滤器

  1. class HKConsumer:public ASTConsumer{
  2. private:
  3. MatchFinder matcher;
  4. HKMatchHandler handler;
  5. public:
  6. HKConsumer(){
  7. matcher.addMatcher(objcPropertyDecl().bind("objcPropertyDecl"), &handler);
  8. }
  9. };
  • 解析语法树,查找objcPropertyDecl节点

在文件解析完成的回调函数中,调用matchermatchAST函数,将文件的语法树传入过滤器

  1. void HandleTranslationUnit(ASTContext &Ctx) {
  2. cout<<"文件解析完成..."<<endl;
  3. matcher.matchAST(Ctx);
  4. }

测试插件
image.png

  • 通过语法树分析,可以找到属性的声明,包含属性的类型和修饰符
  • 但也存在一些问题,在预处理阶段,头文件会被展开,我们可能会获取到系统头文件中的属性,所以我们要想办法过滤掉系统文件中的代码

3.5.4 过滤系统文件

可以通过文件路径判断系统文件,因为系统文件都存在于/Applications/Xcode.app/开头的目录中

PluginASTAction类中,存在CompilerInstance类型的CI参数

  1. std::unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &CI,
  2. StringRef InFile) override = 0;
  • CI为编译器实例对象,可以通过它获取到文件路径,以及警告的提示

重写HKConsumer的构造函数,增加CI参数

  1. HKConsumer(CompilerInstance &CI){
  2. matcher.addMatcher(objcPropertyDecl().bind("objcPropertyDecl"), &handler);
  3. }

HKASTAction类中,创建ASTConsumer时,将CI传入

  1. std::unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &CI, StringRef InFile) {
  2. return unique_ptr<HKConsumer> (new HKConsumer(CI));
  3. }

重写HKMatchHandler的构造函数,增加CI参数。定义私有CompilerInstance,通过构造函数对其赋值

  1. class HKMatchHandler:public MatchFinder::MatchCallback{
  2. private:
  3. CompilerInstance &CI;
  4. public:
  5. HKMatchHandler(CompilerInstance &CI):CI(CI){
  6. }
  7. };

HKConsumer的构造函数中,对HKMatchHandler中的CI进行传递

  1. HKConsumer(CompilerInstance &CI):handler(CI){
  2. matcher.addMatcher(objcPropertyDecl().bind("objcPropertyDecl"), &handler);
  3. }

HKMatchHandler使用CI,获取文件路径并进行过滤

  1. class HKMatchHandler:public MatchFinder::MatchCallback{
  2. private:
  3. CompilerInstance &CI;
  4. bool isUserSourceCode(const string fileName){
  5. if(fileName.empty()){
  6. return false;
  7. }
  8. if(fileName.find("/Applications/Xcode.app/")==0){
  9. return false;
  10. }
  11. return true;
  12. }
  13. public:
  14. HKMatchHandler(CompilerInstance &CI):CI(CI){
  15. }
  16. void run(const MatchFinder::MatchResult &Result) {
  17. const ObjCPropertyDecl *propertyDecl = Result.Nodes.getNodeAs<ObjCPropertyDecl>("objcPropertyDecl");
  18. string fileName = CI.getSourceManager().getFilename(propertyDecl->getSourceRange().getBegin()).str();
  19. if(propertyDecl && isUserSourceCode(fileName)){
  20. string typeStr = propertyDecl->getType().getAsString();
  21. cout<<"------拿到了:"<<typeStr<<endl;
  22. }
  23. }
  24. };
  • 通过CI.getSourceManager().getFilename获取文件名称,包含文件路径
  • 需要传入SourceLocation,可以通过节点的propertyDecl->getSourceRange().getBegin()获得
  • 实现isUserSourceCode函数,判断路径非空,并且非/Applications/Xcode.app/目录开头,视为自定义文件

测试插件

  1. 文件解析完成...
  2. ------拿到了:NSString *
  3. ------拿到了:NSArray *
  • 成功过滤系统文件,获取到自定义文件中的两个属性

3.5.5 判断属性的类型

实现isShouldUseCopy函数,传入属性类型,判断当前类型是否为必须使用copy修饰的类型

  1. class HKMatchHandler:public MatchFinder::MatchCallback{
  2. private:
  3. CompilerInstance &CI;
  4. bool isUserSourceCode(const string fileName){
  5. if(fileName.empty()){
  6. return false;
  7. }
  8. if(fileName.find("/Applications/Xcode.app/")==0){
  9. return false;
  10. }
  11. return true;
  12. }
  13. bool isShouldUseCopy(const string typeStr){
  14. if(typeStr.find("NSString") != string::npos ||
  15. typeStr.find("NSArray") != string::npos ||
  16. typeStr.find("NSDictionary") != string::npos){
  17. return true;
  18. }
  19. return false;
  20. }
  21. public:
  22. HKMatchHandler(CompilerInstance &CI):CI(CI){
  23. }
  24. void run(const MatchFinder::MatchResult &Result) {
  25. const ObjCPropertyDecl *propertyDecl = Result.Nodes.getNodeAs<ObjCPropertyDecl>("objcPropertyDecl");
  26. string fileName = CI.getSourceManager().getFilename(propertyDecl->getSourceRange().getBegin()).str();
  27. if(propertyDecl && isUserSourceCode(fileName)){
  28. string typeStr = propertyDecl->getType().getAsString();
  29. if(isShouldUseCopy(typeStr)){
  30. cout<<"------拿到了:"<<typeStr<<endl;
  31. }
  32. }
  33. }
  34. };

ViewController.m中,增加其他类型的属性声明

  1. #import "ViewController.h"
  2. @interface ViewController ()
  3. @property(nonatomic, strong) NSString* name;
  4. @property(nonatomic, strong) NSArray* arrs;
  5. @property(nonatomic, strong) id objc;
  6. @property(nonatomic, strong) NSSet *sets;
  7. @property(nonatomic, strong) NSDictionary * dict;
  8. @end
  9. @implementation ViewController
  10. - (void)viewDidLoad {
  11. [super viewDidLoad];
  12. }
  13. @end

测试插件

  1. 文件解析完成...
  2. ------拿到了:NSString *
  3. ------拿到了:NSArray *
  4. ------拿到了:NSDictionary *
  • 成功过滤其他类型的属性

3.5.6 判断属性的修饰符

通过propertyDecl->getPropertyAttributes()获取属性修饰符,和OBJC_PR_copy进行位与运算

  1. void run(const MatchFinder::MatchResult &Result) {
  2. const ObjCPropertyDecl *propertyDecl = Result.Nodes.getNodeAs<ObjCPropertyDecl>("objcPropertyDecl");
  3. string fileName = CI.getSourceManager().getFilename(propertyDecl->getSourceRange().getBegin()).str();
  4. if(propertyDecl && isUserSourceCode(fileName)){
  5. string typeStr = propertyDecl->getType().getAsString();
  6. ObjCPropertyDecl::PropertyAttributeKind attr = propertyDecl->getPropertyAttributes();
  7. if(isShouldUseCopy(typeStr) && !(attr & ObjCPropertyDecl::OBJC_PR_copy)){
  8. cout<<"------请使用copy修饰:"<<typeStr<<endl;
  9. }
  10. }
  11. }

测试插件:

  1. 文件解析完成...
  2. ------请使用copy修饰:NSString *
  3. ------请使用copy修饰:NSArray *
  4. ------请使用copy修饰:NSDictionary *

3.5.7 提示警告信息

当判断目标类型使用非copy修饰,目前只是内容打印,正确的做法在Xcode中提示警告信息

使用编译器实例对象CI提示警告信息

  1. void run(const MatchFinder::MatchResult &Result) {
  2. const ObjCPropertyDecl *propertyDecl = Result.Nodes.getNodeAs<ObjCPropertyDecl>("objcPropertyDecl");
  3. string fileName = CI.getSourceManager().getFilename(propertyDecl->getSourceRange().getBegin()).str();
  4. if(propertyDecl && isUserSourceCode(fileName)){
  5. string typeStr = propertyDecl->getType().getAsString();
  6. ObjCPropertyDecl::PropertyAttributeKind attr = propertyDecl->getPropertyAttributes();
  7. if(isShouldUseCopy(typeStr) && !(attr & ObjCPropertyDecl::OBJC_PR_copy)){
  8. DiagnosticsEngine &diag = CI.getDiagnostics();
  9. diag.Report(propertyDecl->getLocation(), diag.getCustomDiagID(DiagnosticsEngine::Warning, "请使用copy修饰"));
  10. }
  11. }
  12. }
  • 通过CIgetDiagnostics函数,获取诊断引擎,需要传入位置和DiagID
  • 通过节点获取位置,使用propertyDecl->getLocation()获得当前节点的位置
  • 通过diag.getCustomDiagID获取DiagID,设置提示级别和文案

测试插件

  1. 文件解析完成...
  2. ViewController.m:12:40: warning: 请使用copy修饰
  3. @property(nonatomic, strong) NSString* name;
  4. ^
  5. ViewController.m:13:39: warning: 请使用copy修饰
  6. @property(nonatomic, strong) NSArray* arrs;
  7. ^
  8. ViewController.m:16:45: warning: 请使用copy修饰
  9. @property(nonatomic, strong) NSDictionary * dict;
  10. ^
  11. 3 warnings generated.

3.5.8 Xcode集成插件

打开测试项目,在Xcode中注册插件,来到Build SettingsOther C Flags
image.png

  1. //-Xclang -load -Xclang (.dylib)插件路径 -Xclang -add-plugin -Xclang 插件名称
  2. -Xclang -load -Xclang /Volumes/study/Source/llvm-hk/build_xcode/Debug/lib/HKPlugin.dylib -Xclang -add-plugin -Xclang HKPlugin

Xcode中替换Clang,来到Build Settings中新增两项用户自定义设置
image.png

分别添加CCCXX
image.png

  • CC对应自己编译的Clang绝对路径
  • CXX对应自己编译的Clang++绝对路径
  1. /Volumes/study/Source/llvm-hk/build_xcode/Debug/bin/clang
  2. /Volumes/study/Source/llvm-hk/build_xcode/Debug/bin/clang++

Build Settings中,将Enable Index-Wihle-Building Functionality设置为NO
image.png

测试插件
image.png

总结

LLVM

  • LLVM是一个编译器

  • 优势:前后端分离,可扩展性强

编译型语言 & 解释型语言:

  • 解释型语言:直接读取源码,使用解释器即可直接执行

  • 编译型语言:通过编译器,将源码编译成机器代码才可执行

编译器:

  • 前端:经过词法分析、语法分析,生成AST抽象语法树,LLVM还会生成IR中间代码,开启Bitcode会生成bc代码

  • 优化器:在LLVM的前端和后端都会对代码进行优化,根据一个又一个Pass进行优化。前端优化IR代码,后端优化汇编代码

  • 后台:生成汇编代码,生成目标文件,经过链接,根据不同的机构生成对应的可执行文件


编译流程:

  • 读取源码

  • 预处理阶段

    • 展开宏和头文件
  • 编译阶段

    • 词法分析

    • 语法分析

    • 生成IR中间代码

    • IR代码优化

    • Bitcode代码

  • 生成汇编代码

  • 生成目标文件(汇编器)

  • 生成可执行文件(链接)

Clang插件:

  • 编译LLVM工程

  • 创建插件

  • 编写插件代码

    • 文件和顶级节点的解析

    • 分析OC代码

    • AST节点的过滤

    • 过滤系统文件

    • 判断属性的类型

    • 判断属性的修饰符

    • 提示警告信息

    • Xcode集成插件