LLVM编译流程和Clang插件开发

一.LLVM概述

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

1.传统编译器设计

  • 编译器前端(Frontend)
    编译器前端的任务是解析源代码。它会进行:词法分析,语法分析,语义分析,检查源代码是否存在错误,然后构建抽象语法树(AST),LLVM的前端还会生成中间代码(IR)
  • 优化器
    优化器负责进行各种优化,改善代码的运行时间,例如消除冗余计算等
  • 后端/代码生成器
    将代码映射到目标指令集,生成机器语言,并且进行机器相关的代码优化

2.iOS的编译器架构

Objective C/C/C++使用的编译器前端是Clang,Swift是Swift,后端都是LLVM

在这里插入图片描述

3.LLVM的设计

当编译器决定支持多种源语言或者多种硬件架构时,LLVM最核心的地方就在于此,LLVM使用的是通用的代码表示形式IR,它是用来在编译器中表示代码的形式,所以LLVM可以为任何编程语言编写前端,也可以为任意硬件架构独立编写后端

在这里插入图片描述

二.Clang编译流程

创建一个新工程,在main.m中添加代码

int main(int argc, const char * argv[]) {
    return 0;
}

通过指令clang -ccc-print-phases main.m,查看编译流程:

在这里插入图片描述
总结流程大致为:

  • 1.输入文件:找到源文件
  • 2.预处理阶段:这个过程处理包括宏的替换,头文件的导入
  • 3.编译阶段:进行词法分析、语法分析、检测语法是否正确,最终生成IR
  • 4.后端:这里LLVM会通过一个一个的Pass(节点)去优化,每个Pass做一些事情,最终生成汇编代码
  • 5.生成目标文件
  • 6.链接:链接需要的动态库和静态库,生成可执行文件
  • 7.通过不同的架构,生成对应的可行文件

当然呢,这些流程我也可以单独拆分执行,去验证,这里我就附上终端指令

1.预处理

clang -E main.m

2.编译阶段

clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m

3.后端生成汇编代码

//我们通过最终的.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 //生成汇编代码也可以进行优化

4.生成目标文件(汇编器)

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

这里的操作其实有很多地方可以研究,这里我就没有附上我操作的过程,详情可参看文章LLVM编译流程

二.clang插件开发

1.LLVM下载

由于国内的网络限制,我们需要借助镜像下载LLVM的源码。
https://mirror.tuna.tsinghua.edu.cn/help/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

注意:如果在clone时报xcrun: error: active developer path…错误,终端执行sudo xcode-select -switch/Applications/Xcode.app/Contents/Developer命令,然后再输入一次系统的密码,完成。

3.在LLVM的projects目录下下载compiler-rt,libcxx,libcxxabi

cd	../projects
git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm/compiler-rt.git
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-extra.git

5.安装cmake

编译最新的LLVM需要cmake来编辑,所以需要安装cmake,看自己是否需要安装
brew	list   //查看
brew	install	cmake //安装

6.LLVM编译

cmake编译成Xcode项目
mkdir build_xcode
cd build_xcode
cmake -G Xcode ../llvm

这个过程也有些漫长,建议可以睡个觉,看个小电影啥的😄

7.使用Xcode编译Clang

选择手动创建,不要选自动创建,否则会引入一些不必要的scheme,拖累Xcode速度。
原则:使用哪个scheme,就引入哪个。
在这里插入图片描述
自定义插件需要添加clang和clangTooling:
在这里插入图片描述
编译选择ALL_BUILD Secheme,进行编译,这个时间很漫长。。。。

8.使用ninja编译LLVM

$	cd	llvm_build
$	cmake	-G	Ninja	../llvm	-DCMAKE_INSTALL_PREFIX=.../LLVM/llvm_release 注意DCMAKE_INSTALL_PREFIX后面不能有空格
$	ninja
$	ninja	install
  • 先安装ninja,使用$ brew install ninja 命令安装
  • 在llvm源码目录下新建一个build_ninja目录
  • 在llvm源码目录下新建一个llvm_release目录

9.创建插件

  • 在/llvm/tools/clang/tools目录下心间插件YCXPlugin

在这里插入图片描述

  • 修改/llvm/tools/clang/tools目录下的CMakeLists.txt文件,新增add_clang_subdirectory(YCXPlugin)
    在这里插入图片描述

  • 在YCXPlugin目录下新建一个名为YCXPlugin.cpp的文件和CMakeList.txt的文件,在CMakeList.txt中写上
    在这里插入图片描述

  • 接下来利用cmake重新生成一下Xcode项目,在build_xcode中 cmake -G Xcode …/llvm

  • 最后可以在LLVM的Xcode项目中可以看到 Loadable modules目录下有自己的Plugin目录了,我们可以在里面编写插件代码

三.Clang插件编写

1.代码编写

在YCXPlugin.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;
using namespace clang::ast_matchers;

// 插件命名空间
namespace YCXPlugin {

    // 第三步:扫描完毕回调
    // 4、自定义回调类,继承自MatchCallback
    class XJMatchCallback : public MatchFinder::MatchCallback {

    private:
        // CI传递路径:YCXASTAction类中的CreateASTConsumer方法参数 -> XJASTConsumer的构造函数 -> XJMatchCallback的私有属性,通过构造函数从XJASTConsumer构造函数中获取
        CompilerInstance &CI;

        // 判断是否是自己的文件
        bool isUserSourceCode(const string fileName) {
            // 文件名不为空
            if (fileName.empty()) return false;
            // 非Xcode中的代码都认为是用户的
            if (0 == fileName.find("/Applications/Xcode.app/")) return false;
            return true;
        }

        // 判断是否应该用copy修饰
        bool isShouldUseCopy(const string typeStr) {
            // 判断类型是否是 NSString / NSArray / NSDictionary
            if (typeStr.find("NSString") != string::npos ||
                typeStr.find("NSArray") != string::npos ||
                typeStr.find("NSDictionary") != string::npos) {
                return true;
            }
            return false;
        }

    public:
        // 构造方法
        XJMatchCallback(CompilerInstance &CI):CI(CI) {}

        // 重载run方法
        void run(const MatchFinder::MatchResult &Result) {
            // 通过Result获取节点对象,根据节点id("objcPropertyDecl")获取(此id需要与XJASTConsumer构造方法中bind的id一致)
            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();
                // 节点的描述信息
                ObjCPropertyAttribute::Kind attrKind = propertyDecl->getPropertyAttributes();
                // 应该使用copy,但是没有使用copy
                if (isShouldUseCopy(typeStr) && !(attrKind & ObjCPropertyAttribute::kind_copy)) {
                    // 通过CI获取诊断引擎
                    DiagnosticsEngine &diag = CI.getDiagnostics();
                    // Report 报告
                    /**
                     错误位置:getLocation 节点位置
                     错误:getCustomDiagID(等级,提示)
                     */
                    diag.Report(propertyDecl->getLocation(), diag.getCustomDiagID(DiagnosticsEngine::Warning, "%0 - 这个属性推荐使用copy修饰!!"))<< typeStr;
                }
            }
        }
    };

    // 第二步:扫描配置完毕
    // 3、自定义XJASTConsumer,继承自抽象类 ASTConsumer,用于监听AST节点的信息 -- 过滤器
    class XJASTConsumer : public ASTConsumer {
    private:
        // AST 节点查找器(过滤器)
        MatchFinder matcher;
        // 回调对象
        XJMatchCallback callback;

    public:
        // 构造方法中创建MatchFinder对象
        XJASTConsumer(CompilerInstance &CI):callback(CI) { // 构造即将CI传递给callback
            // 添加一个MatchFinder,每个objcPropertyDecl节点绑定一个objcPropertyDecl标识(去匹配objcPropertyDecl节点)
            // 回调callback,其实是在CJLMatchCallback里面重写run方法(真正回调的是回调run方法)
matcher.addMatcher(objcPropertyDecl().bind("objcPropertyDecl"), &callback);
        }

        // 重载两个方法 HandleTopLevelDecl 和 HandleTranslationUnit

        // 解析完毕一个顶级的声明就回调一次(顶级节点,即全局变量,属性,函数等)
        bool HandleTopLevelDecl(DeclGroupRef D) {
//            cout<<"正在解析..."<<endl;
            return true;
        }

        // 当整个文件都解析完毕后回调
        void HandleTranslationUnit(ASTContext &Ctx) {
//            cout<<"文件解析完毕!!!"<<endl;
            // 将文件解析完毕后的上下文context(即AST语法树) 给 matcher
            matcher.matchAST(Ctx);
        }
    };

    //2、继承PluginASTAction,实现我们自定义的YCXASTAction,即自定义AST语法树行为
    class YCXASTAction : public PluginASTAction {
    public:

        // 重载ParseArgs 和 CreateASTConsumer方法

        /*
         解析给定的插件命令行参数
         - param CI 编译器实例,用于报告诊断。
         - return 如果解析成功,则为true;否则,插件将被销毁,并且不执行任何操作。该插件负责使用CompilerInstance的Diagnostic对象报告错误。
         */
        bool ParseArgs(const CompilerInstance &CI, const std::vector<std::string> &arg) {
            return true;
        }

        // 返回自定义的XJASTConsumer对象,抽象类ASTConsumer的子类
        unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &CI, StringRef InFile) {
            /**
             传递CI
             CI用于:
             - 判断文件是否是用户的
             - 抛出警告
             */
            return unique_ptr<XJASTConsumer>(new XJASTConsumer(CI));
        }
    };
}

// 第一步:注册插件,并自定义YCXASTAction类
// 1、注册插件
static FrontendPluginRegistry::Add<YCXPlugin::YCXASTAction> X("YCXPlugin", "this is YCXPlugin");

2.测试插件

命令格式
自己编译的clang文件路径 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.5.sdk(SDK路径)/ -Xclang -load -Xclang 插件(.dyld)路径 -Xclang -add-plugin -Xclang 插件名 -c 源码路径

// 例子
/Users/用户名/llvm-project/build_xcode/Debug/bin/clang -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.5.sdk/ -Xclang -load -Xclang /Users/用户名/llvm-project/build_xcode/Debug/lib/XJPlugin.dylib -Xclang -add-plugin -Xclang XJPlugin -c /Users/用户名/Desktop/DemoCode/PluginTestDemo/PluginTestDemo/ViewController.m

测试结果

3.插件集成

1.加载插件
打开测试项目,在Build Settings->Other C Flags 添加上如下内容

 -Xclang -load -Xclang (.dylib)插件动态库路径 -Xclang -add-plugin -Xclang 插件名

在这里插入图片描述

2.设置编译器

  • 由于clang插件需要使用对应的版本去加载,如果版本不一致会导致编译失败

  • 在Build Settings栏目中新增两项用户定义的设置

在这里插入图片描述
分别是CC和CXX
CC对应的是自己编译的clang的绝对路径
CXX对应的是自己编译的clang++的绝对路径

  • 接下来在Build Settings中搜索index,将Enable Index-Wihle-Building Functionality的Default改为NO

在这里插入图片描述

  • 最后,重新编译测试项目,会出现我们想要的效果了,警告的提示变成中文了哦😄

在这里插入图片描述

实现过程中间可能会发生很多意想不到的状态,需要大家耐心去找解决方案,我也只是把我遇到的情况进行了描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值