什么是LLVM
- 官网 LLVM
- LLVM项目是模块化、可重用的编译器以及工具链技术的集合。
- LLVM最早的时候是Illinois的一个研究项目,主要负责人是Chris Lattner(Swift之父),他现在就职于Apple. Apple 目前也是
LLVM
项目的主要赞助者之一。
传统编译器架构
1
source code -> Frontend -> Optimizer -> backend -> Machine code
Frontend
: 前端- 词法分析、语法分析、语义分析、生成中间代码
Optimizer
: 优化器- 中间代码优化
Backend
: 后端- 生成机器码
LLVM架构
- 不同的前端后端使用同一的中间代码
LLVM Intermediate Representation(LLVM IR)
- 如果需要支持一种新的编程语言,那么只需要实现一个新的前端。
- 如果需要支持一种新的硬件设备,那么只需要实现一个新的后端。
- 优化阶段是一个通用的阶段,它针对的是同一的
LLVM IR
,不论是支持新的编程语言还是支持新的硬件设备,都不需要对优化阶段做修改。 - 相比之下,GCC的前端和后端没分开,前端后端耦合在一起。所以GCC为了支持一门新的语言,或者为了支持一个新的目标平台,就变得特别苦难。
LLVM
现在被作为实现各种静态和运行时编译语言的通用基础结构(GCC家族、Java、.NET、Python、Ruby、Scheme、Haskell、D等)。
Clang
什么是
Clang
LLVM
项目的一个子项目- 基于LLVM架构的
C
、C++
、Objective-C
编译器前端 - 官网
Clang
优点- 编译速度快:在某些平台上,
Clang
的编译速度显著的快过GCC(Debug模式下编译OC速度比GGC快3倍)
- 占用内存小:
Clang
生成的AST
所占用的内存是GCC
的五分之一左右 - 模块化设计:
Clang
采用基于库的模块化设计,易于 IDE 集成及其他用途的重用 - 诊断信息可读性强:在编译过程中,Clang 创建并保留了大量详细的元数据 (metadata),有利于调试和错误报告
- 设计清晰简单,容易理解,易于扩展增强
- 编译速度快:在某些平台上,
Clang
与LLVM
文件编译
OC源文件的编译过程
词法分析
语法树-AST
LLVM IR
LLVM IR
有3种表示形式(但本质是等价的,就好比水可以有气体、液体、固体3种形态)text
:便于阅读的文本格式,类似于汇编语言,拓展名.ll,$ clang -S -emit-llvm main.m
memory
: 内存格式bitcode
: 二进制格式,拓展名.bc
,$ clang -c -emit-llvm main.m
IR基本语法
- 注释以分号 ; 开头
- 全局标识符以
@
开头,局部标识符以%
开头 alloca
,在当前函数栈帧中分配内存i32
,32bit
,4个字节的意思align
,内存对齐store
,写入数据load
,读取数据
1 2 3 4 5 6 7 8 9 10 11 12 13
define void @test(i32, i32) #0 { %3 = alloca i32, align 4 %4 = alloca i32, align 4 %5 = alloca i32, align 4 store i32 %0, i32* %3, align 4 store i32 %1, i32* %4, align 4 %6 = load i32, i32* %3, align 4 %7 = load i32, i32* %4, align 4 %8 = add nsw i32 %6, %7 %9 = sub nsw i32 %8, 3 store i32 %9, i32* %5, align 4 ret void }
源码下载
- 下载
LLVM
$ git clone https://git.llvm.org/git/llvm.git/
- 大小 648.2 M,仅供参考
- 下载
clang
$ cd llvm/tools
$ git clone https://git.llvm.org/git/clang.git/
- 大小240.6M,仅供参考
- 下载
源码编译
- 安装
cmake
和ninja
(先安装brew,https://brew.sh/)$ brew install cmake
$ brew install ninja
ninja
如果安装失败,可以直接从github获取release版放入/usr/local/bin
中https://github.com/ninja-build/ninja/releases
- 在LLVM源码同级目录下新建一个
llvm_build
目录(最终会在llvm_build
目录下生成build.ninja
)$ cd llvm_build
$ cmake -G Ninja ../llvm -DCMAKE_INSTALL_PREFIX=LLVM
的安装路径- 更多
cmake
相关选项,可以参考: https://llvm.org/docs/CMake.html
- 依次执行编译、安装指令
$ ninja
- 编译完毕后,
llvm_build
目录大概 21.05 G(仅供参考) $ ninja install
- 安装完毕后,安装目录大概 11.92 G(仅供参考)
- 安装
源码编译
应用与实践
libclang
、libTooling
- 官方参考
- 应用:语法树分析、语言转换等
Clang
插件开发- 官方参考
- https://clang.llvm.org/docs/ClangPlugins.html
- https://clang.llvm.org/docs/ExternalClangExamples.html
- https://clang.llvm.org/docs/RAVFrontendAction.html
- 应用:代码检查(命名规范、代码规范)等
- 官方参考
Pass
开发- 官方参考
- 应用:代码优化、代码混淆等
- 开发新的编程语言
- https://llvm-tutorial-cn.readthedocs.io/en/latest/index.html
- https://kaleidoscope-llvm-tutorial-zh-cn.readthedocs.io/zh_CN/latest/
clang插件开发
插件目录
插件必要文件
- 在
jv-plugin
目录下新建一个CMakeLists.txt
,文件内容是:add_llvm_loadable_module(JVPlugin JVPlugin.cpp)
TBPlugin
是插件名,TBPlugin.cpp
是源代码文件
1
add_llvm_loadable_module(JVPlugin JVPlugin.cpp)
- 在
编写插件源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
#include <iostream> #include "clang/AST/AST.h" #include "clang/AST/ASTConsumer.h" #include "clang/ASTMatchers/ASTMatchers.h" #include "clang/ASTMatchers/ASTMatchFinder.h" #include "clang/Frontend/CompilerInstance.h" #include "clang/Frontend/FrontendPluginRegistry.h" using namespace clang; using namespace std; using namespace llvm; using namespace clang::ast_matchers; namespace JVPlugin { class Handler : public MatchFinder::MatchCallback { private: CompilerInstance &ci; public: JVHandler(CompilerInstance &ci) :ci(ci) {} void run(const MatchFinder::MatchResult &Result) { if (const ObjCInterfaceDecl *decl = Result.Nodes.getNodeAs<ObjCInterfaceDecl>("ObjCInterfaceDecl")) { size_t pos = decl->getName().find('_'); if (pos != StringRef::npos) { DiagnosticsEngine &D = ci.getDiagnostics(); SourceLocation loc = decl->getLocation().getLocWithOffset(pos); D.Report(loc, D.getCustomDiagID(DiagnosticsEngine::Error, "Jovins:类名中不能带有下划线")); } } } }; class JVASTConsumer: public ASTConsumer { private: MatchFinder matcher; JVHandler handler; public: JVASTConsumer(CompilerInstance &ci) :handler(ci) { matcher.addMatcher(objcInterfaceDecl().bind("ObjCInterfaceDecl"), &handler); } void HandleTranslationUnit(ASTContext &context) { matcher.matchAST(context); } }; class JVASTAction: public PluginASTAction { public: unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &ci, StringRef iFile) { return unique_ptr<JVASTConsumer> (new JVASTConsumer(ci)); } bool ParseArgs(const CompilerInstance &ci, const vector<string> &args) { return true; } }; } static FrontendPluginRegistry::Add<JVPlugin::JVASTAction> X("JVPlugin", "The JVPlugin is my first clang-plugin.");
编译插件
- 利用
cmake
生成的Xcode
项目来编译插件(第一次编写完插件,需要利用cmake重新生成一下Xcode项目) - 插件源代码在
Sources/Loadable modules
目录下可以找到,这样就可以直接在Xcode
里编写插件代码 - 选择
JVPlugin
这个target进行编译,编译完会生成一个动态库文件
生成的动态库可以在项目Products -> show in Finder找到
JVPlugin.dylib
- 利用
加载插件
- 在Xcode项目中指定加载插件动态库:
BuildSettings > OTHER_CFLAGS
-Xclang -load -Xclang
: 动态库路径-Xclang -add-plugin -Xclang
: 插件名称
- 在Xcode项目中指定加载插件动态库:
Hack Xcode
首先要对
Xcode
进行Hack
,才能修改默认的编译器下载
XcodeHacking.zip
,解压,修改HackedClang.xcplugin/Contents/Resources/HackedClang.xcspec
的内容,设置一下自己编译好的clang的路径
然后在XcodeHacking目录下进行命令行,将XcodeHacking的内容剪切到Xcode内部
$ sudo mv HackedClang.xcplugin
xcode-select-print-path
/../PlugIns/Xcode3Core.ideplugin/Contents/SharedSupport/Developer/Library/Xcode/Plug-ins
$ sudo mv HackedBuildSystem.xcspec
xcode-select-print- path/Platforms/iPhoneSimulator.platform/Developer/Library/Xcode/Specifications
修改Xcode的编译器
编译项目
- 编译项目后,会在编译日志看到JVPlugin插件的打印信息(如果插件更新了,最好先Clean一下项目)
更多
- 想要实现更复杂的插件功能,就需要利用clang的API针对语法树(AST)进行相应的分析和处理
- 关于AST的资料
- https://clang.llvm.org/doxygen/namespaceclang.html
- https://clang.llvm.org/doxygen/classclang_1_1Decl.html
- https://clang.llvm.org/doxygen/classclang_1_1Stmt.html