LLVM概述
LLVM是构架编译器的框架系统,以C++编写而成,用于优化任意程序语言编写的程序编译时间,链接时间,运行时间以及空闲时间,对开发者保持开发并兼容已有脚本。
LLVM编译器设计有前端、优化器和后端:
1、前端:
编译器前端任务是解析源代码,它会进行词法分析,语法分析,语义分析检查院代码是否存在错误,然后构建抽象语法树(Abstract Syntax Tree AST),LLVM的前端还会生成中间代码(intermediate representation IR)。
2、优化器:
优化器负责进行各种优化。改善代码的运行时间等。
3、后端:
将代码映射到目标指令集。生成机器语言,并且进行机器相关的代码优化。
在iOS的编译器架构中,Objective-c/C/C++使用的编译器前端是Clang,Swift是Swift,后端则都是LLVM。
Clang
Clang是LLVM中的一个子项目,它是基于LLVM架构的轻量级编译器,是为了取代GCC,提供更快的编译速度。它是负责编译C,C++,OC语言的编译器,属于LLVM编译器的前端。
下面使用Clang来编译一个c文件。
C文件代码:
#include<stdio.h>
int main(int argc,char * argv[]){
printf("hello world!\n");
return 0;
}
通过终端进行编译:
看下图,创建并编写代码完之后,通过clang hello.c
就成功的编译了C文件,编译完成之后会产生一个a.out
文件,尝试的查看这个文件,它是一个Mach-O 64-bit
的文件,并且是x86_64
架构的。
然后执行a.out文件,输出hello world
;
Clang
会将代码进行各种分析生成中间代码IR
,而优化器则是拿到前端生成的IR
进行优化,优化之后生成的还是IR
,而后端则将优化后的IR
生成目标指令集。LLVM架构的优点是进行了前后端分离,它的中间代码都是生成IR
,如果它要适配一门新语言,只需要独立开发一个前端,就可以生成相应的后端代码。
Clang编译流程
通过clang -ccc-print-phases main.m
可以打印源码的编译流程,看下图所示:
其中:
1、代表预处理阶段;
2、代表编译阶段IR
;
3、代表后端处理;
4、代表汇编,生成目标文件,它是.o
文件;
5、表示链接多个镜像文件,生成完整的可执行文件;
6、代表生成不同架构的可执行文件;
下面通过一段代码来演示一下这些流程:
#import <stdio.h>
#define C 30
int main(int argc, const char * argv[]) {
@autoreleasepool {
int a = 10;
int b = 20;
printf("%d",a+b+C);
}
return 0;
}
预处理阶段
通过在终端执行clang -E main.m
:
就会生成之后的代码很多,下图是截取的main
函数的代码,可以看到宏C
在预处理阶段就替换掉了
当将int
通过typedef
取别名之后,预处理阶段没有做处理:
编译阶段
它使用的指令是clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m
预处理完就会进行词法分析,这边会将代码切割成一个个Token,看下图所示:
在词法分析完,就会进行语法分析,它的任务是验证语法是否正确,使用指令:clang -fmodules -fsyntax-only -Xclang -ast-dump main.m
:
看下图这种五颜六色的代码,就是所有节点组成的抽象语法树,在FunctionDecl
之后就是main
函数的代码语法树,CallExpr
表示调用函数:
这些操作通过指令很容易完成,有兴趣的同学可以自己去玩一下,在进行这一步时,可能出现找不到头文件的情况,那就需要我们指定SDK:
clang -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/ iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator12.2.sdk(自己的SDK路径) -fmodules -fsyntax-only -Xclang -ast-dump main.m
。
接下来就是生成中间代码IR:
使用指令:clang -S -fobjc-arc -emit-llvm main.m
;
在执行完之后,在目录文件下会生成一个main.ll
文件:
oc代码会在这一步进行runtime的桥接:peoperty合成,ARC处理等IR基本语法
@ 全局标识
% 局部标识
alloca 开辟空间
align 内存对齐
i32 32bit4,4字节
store 写入内存
load 读取数据
call 调用函数
ret 返回
IR的优化:
LLVM的优化级别分别是 - O0 -O1 -O2 -O3 -Os
指令clang -Os -S -fobjc-arc -emit-llvm main.m -o main.ll
优化之后,代码少很多:
bitCode是苹果对代码的进一步优化,生成.bc的中间代码。在iOS开发中,应该有很多人用过某些第三方库,在运行后,显示不支持bitCode
,这时候就需要关闭它。
生成bc文件指令:clang -emit-llvm -c main.ll -o main.bc
生成汇编代码
在经过上面的步骤之后,最终的.bc或者.ll代码生成汇编代码
通过指令:clang -S -fobjc-arc main.bc -o main.s
或者clang -S -fobjc-arc main.ll -o main.s
在上面有一步编译器优化,对汇编代码生成没优化过的汇编文件和优化过的汇编文件里面的代码都是一样的。
生成汇编代码也可以进行优化:
使用指令clang -Os -S -fobjc-arc main.m -o main.s
。
生成目标文件(汇编器)
目标文件生成,使用指令clang -fmodules -c main.s -o main.o
,就会在目录文件下生成main.o文件。
可以看到,main.s还是汇编文件,在经过目标文件生成之后,就变成了Mach-o
文件。
生成可执行文件:
可以通过clang main.o -o main
:
查看链接符号表:xcrun nm -nm main.o
自定义Clang插件
准备工作
1、下载LLVM项目:
`git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm/llvm.git`
2、在LLVM的tools目录下下载Clang:
`
cd llvm/tools
git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm/clang.git
`
3、在 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
`
4、在 Clang的tools下安装extra工具
`
cd ../tools/clang/tools
git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm/clang-tools-e xtra.git
`
LLVM编译:
1、安装cmake:
brew install cmake
2、cmake编译成xcode项目
mkdir build_xcode cd build_xcode cmake -G Xcode ../llvm
3、使用Xcode编译Clang:
打开build_xcode
里面的LLVM项目,选择下图的两个,进行编译,时间大概在1小时内,但是编译完之后的文件大小可能接近20G。
创建插件
既然需要自定义插件,那肯定需要了解插件所需要的功能;
来实现一个简单点的功能,例如如果NSString类型不适用copy
修饰,就提示警告信息这个功能。
在/llvm/tools/clang/tools目录下创建自己的插件WXPlugin
;
修改目录下的CMakeLists.txt
文件,在最底下新增add_clang_subdirectory(WXPlugin)
在WXPlugin
目录下创建一个WXPlugin.cpp
和CMakeLists.txt
文件;
在CMakeLists.txt
里面添加add_llvm_library( WXPlugin MODULE BUILDTREE_ONLY WXPlugin )
接下里使用cmake重新编译下xcode项目,使用指令cmake -G Xcode ../llvm
;
由于它是增量编译,所以所需时间也比较少;
编译完之后,它又要你选择target,这就需要选择你创建的插件WXPlugin
,进行编译;
之后就可以在Loadable modules
目录下找到WXPlugin
;
编写代码
1、首先导入相应的头文件和命名空间:
#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 WXPlugin {
}
2、实现基本的解析功能:
namespace WXPlugin {
//自定义WXConsumer
class WXConsumer: public ASTConsumer{
public:
//解析完一个顶级的声明就回调一次
bool HandleTopLevelDecl(DeclGroupRef D) {
cout<<"正在解析……"<<endl;
return true;
}
//整个文件都会解析完成的回调
void HandleTranslationUnit(ASTContext &Ctx) {
cout<<"文件解析完毕!"<<endl;
}
};
//继承PluginASTAction 实现我们自定义的Action
class WXASTACtion:public PluginASTAction{
public:
bool ParseArgs(const CompilerInstance &CI,const std::vector<std::string> &arg){
return true;
}
unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &CI,StringRef InFile){
return unique_ptr<WXConsumer>(new WXConsumer);
}
};
}
//注册插件
static FrontendPluginRegistry::Add<WXPlugin::WXASTACtion>WX("WXPlugin","this is WXPlugin");
接下来使用终端来测试:
自己编译的clang路径 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.1.sdk/ -Xclang -load -Xclang 插件(.dylib)路径 -Xclang -add-plugin -Xclang 插件名 -c 源码路径
;
例如我使用的路径:/Users/pengwenxi/Desktop/build_xcode/Debug/bin/clang -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.1.sdk/ -Xclang -load -Xclang /Users/pengwenxi/Desktop/build_xcode/Debug/lib/WXPlugin.dylib -Xclang -add-plugin -Xclang WXPlugin -c /Users/pengwenxi/Desktop/hello.c
执行完就如下图所示:
同样的也会生成main.o文件
接下来可以来创建一个iOS项目,感兴趣的可以去看看ViewController的语法树代码,里面有一个ObjCCategoryDecl
,这个对写插件有很大的作用。
下面添加部分代码,来获取节点:
class WXMatchCallback: public MatchFinder::MatchCallback{
public:
void run(const clang::ast_matchers::MatchFinder::MatchResult &Result) {
//通过result拿到节点
const ObjCPropertyDecl * propertyDecl = Result.Nodes.getNodeAs<ObjCPropertyDecl>("objcPropertyDecl");
if(propertyDecl){
string typeStr = propertyDecl->getType().getAsString();
cout<<"----获取到了:"<<typeStr<<"------"<<endl;
}
};
};
通过编译ViewController.m
,就会编译很多节点,其中有系统的也有我们自己写的:
下面过滤系统的节点:
过滤系统节点需要掌握它的规律,到现在为止,可以获取xcode中的源码和源码所在路径;
那么系统的源码路径在某一路径下肯定是固定的,因此可以过滤这部分代码;
下面附上完整的代码来,就不分节来解释了:
namespace WXPlugin {
class WXMatchCallback: public MatchFinder::MatchCallback{
private:
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){
if(typeStr.find("NSString") != string::npos || typeStr.find("NSArray") != string::npos || typeStr.find("NSDictionary") != string::npos){
return true;
}
return false;
}
public:
WXMatchCallback(CompilerInstance &CI):CI(CI){}
//真正的回调
void run(const clang::ast_matchers::MatchFinder::MatchResult &Result) {
//通过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 attrKind = propertyDecl->getPropertyAttributes();
//判断应该使用copy但是没有使用copy
if(isShouldUseCopy(typeStr) && !(attrKind & ObjCPropertyDecl::OBJC_PR_copy)){
//cout<<typeStr<<"应该使用;copy修饰!但是你没有!"<<endl;
//诊断引擎
DiagnosticsEngine &diag = CI.getDiagnostics();
//报告
diag.Report(propertyDecl->getBeginLoc(), diag.getCustomDiagID(DiagnosticsEngine::Warning, "%0这个地方推荐使用copy!!"))<<typeStr;
}
cout<<"----获取到了:"<<typeStr<<"------"<<"属于----"<<fileName<<"------"<<endl;
}
};
};
//自定义WXConsumer
class WXConsumer: public ASTConsumer{
private:
//AST节点的查找过程
MatchFinder matcher;
WXMatchCallback callback;
public:
WXConsumer(CompilerInstance &CI):callback(CI){
//添加一个MatchFinder去匹配objcPropertyDecl节点
//回调在WXMatchCallback里面run方法!
matcher.addMatcher(objcPropertyDecl().bind("objcPropertyDecl"), &callback);
}
//解析完一个顶级的声明就回调一次
bool HandleTopLevelDecl(DeclGroupRef D) {
// cout<<"正在解析……"<<endl;
return true;
}
//整个文件都会解析完成的回调
void HandleTranslationUnit(ASTContext &Ctx) {
// cout<<"文件解析完毕!"<<endl;
matcher.matchAST(Ctx);
}
};
//继承PluginASTAction 实现我们自定义的Action
class WXASTACtion:public PluginASTAction{
public:
bool ParseArgs(const CompilerInstance &CI,const std::vector<std::string> &arg){
return true;
}
unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &CI,StringRef InFile){
return unique_ptr<WXConsumer>(new WXConsumer(CI));
}
};
}
//注册插件
static FrontendPluginRegistry::Add<WXPlugin::WXASTACtion>WX("WXPlugin","this is WXPlugin");
接下来就是验证了:
看下图所示,代码警告提示成功了:
下面介绍如何将自定义的clang用在xcode中:
将自定义插件集成xcode
1、加载插件
打开iOS项目,在Build Settings -> Other C Flags添加dylib
的动态路径
-Xclang -load -Xclang (.dylib) -Xclang -add-plugin -Xclang WXPlugin
2、在Build Settings新增两项用户自定义:
CC对应自己编译的clang绝对路径
CXX对应自己编译的clang++的绝对路径
3、在Build Settings中搜索index,将Enable Index-Wihle-Building Functionality的Default改为NO。
三部弄完了,就完整了xcode的集成: