LLVM 编译器 与 自定义Clang插件

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架构的。

iShot2020-11-18 15.38.26.png

然后执行a.out文件,输出hello world

iShot2020-11-18 15.45.25.png

Clang会将代码进行各种分析生成中间代码IR,而优化器则是拿到前端生成的IR进行优化,优化之后生成的还是IR,而后端则将优化后的IR生成目标指令集。LLVM架构的优点是进行了前后端分离,它的中间代码都是生成IR,如果它要适配一门新语言,只需要独立开发一个前端,就可以生成相应的后端代码。

Clang编译流程

通过clang -ccc-print-phases main.m可以打印源码的编译流程,看下图所示:

iShot2020-11-21 12.11.01.png

其中:
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在预处理阶段就替换掉了

iShot2020-11-21 12.20.48.png

当将int通过typedef取别名之后,预处理阶段没有做处理:

iShot2020-11-21 12.24.45.png

编译阶段

它使用的指令是clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m

预处理完就会进行词法分析,这边会将代码切割成一个个Token,看下图所示:


iShot2020-11-21 12.29.37.png

在词法分析完,就会进行语法分析,它的任务是验证语法是否正确,使用指令:clang -fmodules -fsyntax-only -Xclang -ast-dump main.m
看下图这种五颜六色的代码,就是所有节点组成的抽象语法树,在FunctionDecl之后就是main函数的代码语法树,CallExpr表示调用函数:

iShot2020-11-21 12.31.01.png

这些操作通过指令很容易完成,有兴趣的同学可以自己去玩一下,在进行这一步时,可能出现找不到头文件的情况,那就需要我们指定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文件:

iShot2020-11-21 12.48.23.png

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

优化之后,代码少很多:


iShot2020-11-21 12.52.45.png

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

iShot2020-11-21 12.59.16.png

在上面有一步编译器优化,对汇编代码生成没优化过的汇编文件和优化过的汇编文件里面的代码都是一样的。

生成汇编代码也可以进行优化:
使用指令clang -Os -S -fobjc-arc main.m -o main.s

生成目标文件(汇编器)

目标文件生成,使用指令clang -fmodules -c main.s -o main.o,就会在目录文件下生成main.o文件。

可以看到,main.s还是汇编文件,在经过目标文件生成之后,就变成了Mach-o文件。

iShot2020-11-21 13.09.36.png

生成可执行文件:
可以通过clang main.o -o main

查看链接符号表:xcrun nm -nm main.o

iShot2020-11-21 13.13.16.png

自定义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:

iShot2020-11-21 13.23.06.png

打开build_xcode里面的LLVM项目,选择下图的两个,进行编译,时间大概在1小时内,但是编译完之后的文件大小可能接近20G。

iShot2020-11-21 13.50.47.png

创建插件

既然需要自定义插件,那肯定需要了解插件所需要的功能;
来实现一个简单点的功能,例如如果NSString类型不适用copy修饰,就提示警告信息这个功能。

在/llvm/tools/clang/tools目录下创建自己的插件WXPlugin
修改目录下的CMakeLists.txt文件,在最底下新增add_clang_subdirectory(WXPlugin)

WXPlugin目录下创建一个WXPlugin.cppCMakeLists.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文件


iShot2020-11-21 14.36.03.png

接下来可以来创建一个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,就会编译很多节点,其中有系统的也有我们自己写的:

iShot2020-11-21 15.00.42.png

下面过滤系统的节点:
过滤系统节点需要掌握它的规律,到现在为止,可以获取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");

接下来就是验证了:
看下图所示,代码警告提示成功了:


iShot2020-11-21 15.27.52.png

下面介绍如何将自定义的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++的绝对路径

iShot2020-11-21 15.38.45.png

3、在Build Settings中搜索index,将Enable Index-Wihle-Building Functionality􏰘的Default改为NO。

三部弄完了,就完整了xcode的集成:


iShot2020-11-21 15.41.52.png
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,911评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,014评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 142,129评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,283评论 1 264
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,159评论 4 357
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,161评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,565评论 3 382
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,251评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,531评论 1 292
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,619评论 2 310
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,383评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,255评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,624评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,916评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,199评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,553评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,756评论 2 335

推荐阅读更多精彩内容