原文地址:How to get started with the LLVM C API。
我喜欢做一些玩具编程语言来理解编译器(和最底层的,机器)如何工作以及测试一些我没 有掌握的技术。LLVM 很棒,因为我可以修修改改,然后将它作为后端来生成可以在大多数 平台运行的效率很高的代码。如果我只是想让我的代码能执行,可以简单地手写一个解释器, 但有了 LLVM 的 JIT,优化套件和平台支持就像有了超能力──你的小小的玩具也可以有非 常不错的性能。而且,LLVM 是像 Emscripten 和 Rust 的基础,我喜欢让自己对我感兴趣 的技术如何实现有一些直觉上的认识。
我将会向你展示如何在程序中使用 LLVM API 来构建一个函数,你可以像其他(函数)一样 被调用,并让它变成机器语言直接运行在你的平台上。
在这个例子中,我会使用 C API,因为它和 C++ API 一起都包含在了 LLVM 的发行版中, 所以由此开始是最容易的。其他语言也有 LLVM API 的绑定──Python, OCaml, Go, Rust── 但背后使用 LLVM 来生成代码的概念是一样的。
这个例子从某种程度上说是直接跳到了编译器构建过程中的某一阶段。假设前端(记法分析 器,解析器,类型检查器)已经构建好了一个 AST,我们现在只需要遍历它来生成代码的中 间表示,并交给后端来优化并生成机器代码。
这里,我们将只直接写出一个简单函数的过程代码(procedural code),而正常情况下应该 是在 AST 的遍历函数中在遇到特定结点时调用相应的 API 动态获得的。
为了举例,我们会建立一个简单的加法函数,它接受两个整型作为参数并返回它们的和,在 C 语言中等价于:
1 | int sum(int a, int b) { |
再澄清一下我们要做的事:使用 LLVM 动态地在内存中构建这个函数的表示,使用它的 API 来设置好像函数入口和出口,返回和参数类型,以及真正的整数相加指令等事情。一旦这个 表示在内存中构建完成,就可以让 LLVM 跳转到它并根据我们提供的参数来执行,就跟它是 由 C 语言编译成可执行文件一样。
Modules
第一步先创建一个模块(module)。模块在 LLVM 中是全局变量,函数,外部引用和其他数 据的集合。这里的模块跟 Python 中的模块不太一样,它们并不提供独立的命名空间。但 它们是在 LLVM 中创建的所有东西的顶层容器,所以我们就从创建它开始。
1 | LLVMModuleRef mod = LLVMModuleCreateWithName("my_module"); |
传入给模块的工厂函数的字符串是一个由你给定的标识符(identifier)。
注意在你浏览 LLVM C API 文档的时候,不同方面的内容被组织在不同的头文件中。我在
此详述的大部分内容,如模块和函数,都包含在了 Core.h
这个头文件中,随着我们继
续深入我会再包含其他的。
Types
接下来,我创建了 sum
函数并把它加入到模块中。一个函数包含:
- 它的类型(返回类型)
- 一个包含参数类型的 vector
- 一个基本块的集合
我很快会再细说基本块。首先,我们会处理这个函数的类型和参数类型──或者用 C 的术 语叫原型──并将它回到模块中。
1 | LLVMTypeRef param_types[] = { LLVMInt32Type(), LLVMInt32Type() }; |
LLVM 类型对应于你目标平台的原始类型,如固定位宽的整型和浮点型,指针,结构体和
数组。(没有类似于 C 中平台相关的实际大小依赖于底层架构的 int
类型。)
LLVM 类型有构造函数,它的形式遵从于 “LLVM*TYPE*Type()”。在我们的例子中,传递给
sum
函数的参数和函数的类型本身都 32-bit 整型,因此我们都用了 LLVMInt32Type()
.
LLVMFunctionType()
的参数依次是:
- 函数的类型(返回类型)
- 函数的参数类型向量(函数的参数个数应该和数组中的类型个数匹配)
- 函数的参数个数(arity)
- 一个 boolean 值来表示该函数是否是 variadic,即参数数量是否可变
注意函数类型的构造函数返回了一个类型引用。这又加强了我们这里所做的等价于 C 中 声明一个函数原型的概念。
第三行表示将这个函数类型加入到模块中,并取名为 sum
. 我们得到的返回值是一个值
的引用,可以把它理解为代码中(最终是内存中)的某个具体位置,我们把下面将会构建
的函数体放在上面。
Basic blocks
下一步是将基础块加到函数中。基础块是代码的一部分,它只有一个入口和出口──换句话 说,执行的过程只可能是一步步走过一系列指令。没有 if/else, while, loops, 或者任 何种类的跳转。基础块是对控制流建模和进行后续优化的关键,因此 LLVM 对添加我们的进 行中的模块有一等的支持。
1 | LLVMBasicBlockRef entry = LLVMAppendBasicBlock(sum, "entry") |
注意函数名字中的“append”:它有助于帮助我们将正在做的事看作是不断地向这个模块增 加代码块,而我们的基础块是附加在之前加入到该模块的函数后面的。
Instruction builders
不断添加代码块这种记法与指令构建器(instruction builder)相匹配,构建器是向我们 函数唯一的基础块添加指令的方式。
1 | LLVMBuilderRef builder = LLVMCreateBuilder(); |
跟向函数中附加基础块类似,我们把构建器安装好,这样就可以接着刚才的基础块的入口 写指令了。
LLVM IR
LLVM 的主要卖点是 LLVM 的中间表示,或者叫 IR。我见过它被认为是介于汇编和 C 之 间的一个中间点。LLVM IR 是一个非常严格定义的语言,为了给 LLVM 为人们所孰知的优 化和平台间的移植性提供便利。如果你观察 IR,就能看到单独的指令如何被翻译为最终 被生成的汇编指令中的 load, store, 和 jump。该 IR 有 3 种表示:
- 表示为内存中的一组对象的集合,这正是我们在例子中用到的
- 表示为类似于汇编的文本语言(textual language)
- 表示为紧凑的二进制编码的字节串,称谓 bitcode
你可以看到 clang 或其他工具将 LLVM IR 输出为文本或 bitcode。
回到我们的例子中。现在是我们函数的关键点,真正将两个作为参数传入的整数相加并将 得到的值返回给调用者的指令。
1 | LLVMValueRef tmp = LLVMBuildAdd(builder, LLVMGetParam(sum, 0), LLVMGetParam(sum, 1), "tmp"); |
LLVMBuildAdd()
需要一个指向某个构建器的引用,要相加的整数,以及一个名字来保
存结果。(名字是必须的,因为 LLVM IR 要求所有的指令都生成中间结果。这可以在以
后被 LLVM 化简或优化掉,但在生成 IR 时,我们遵循它的要求。)因为要相加的数字是
由调用者提供给函数的参数,我们可以使用 LLVMGetParam()
以函数参数的形式把它们
取回:第二个参数是我们要从函数中查找的参数的索引。
调用 LLVMBuildRet()
来生成返回语句,并把加法指令的临时结果作为要返回的值。
Analysis & execution
创建函数中的构建指令阶段到此结束;该模块现在已经完成了。例子的下一步就要为执行 进行设置。
首先,让我们验证一下这个模块。这可以确保模块被正确构建,如果缺少或弄乱了某些步 骤就会退出。
1 | char *error = NULL; |
LLVM 会提供一个 JIT 或一个解释器来执行我们构建好的 IR。如果它会先尝试为目标平 台构建一个 JIT,如果做不到就再尝试解释器。不管哪种情况,运行我们代码的东西都被 称作执行引擎(execution engine)。
1 | LLVMExecutionEngineRef engine; |
我们可以硬编码一些整数来进行求和,但让我们的程序从命令行接收参数也很容易。
1 | if (argc < 3) { |
现在我们有了两个在宿主语言中表示的整数,还需要把它们转换为 LLVM 中类似的表示。 LLVM 提供了工厂方法可以将这些值转换为我们需要传递给函数的类型:
1 | LLVMGenericValueRef args[] = { |
现在到了被事实检验的时刻:调用我们的(JIT'd)函数!
1 | LLVMGenericValueRef res = LLVMRunFunction(engine, sum, 2, args); |
我们得到了一个结果,不过它还是由 LLVM 表示的。我们将基恢复为 C 类型,也就是上 面操作的逆操作,并打印相加结果:
1 | printf("%d\n", (int)LLVMGenericValueToInt(res, 0)); |
这样就完成了。我们已经通过编程从头构建了一个函数,并让它直接以原生于我们使用的 平台的机器码运行了。LLVM 还有很多内容,包括流程控制(例如,实现 if/else)和优 化 pass,但我们已经覆盖了所有 LLVM-IR-to-code 程序都会用到的基础知识。
Compiling
为了编译这个程序,我们需要引用 LLVM 的头文件并链接到它的库。尽管我们写的是 C 程序,链接时还是需要一个 C++ 链接器。(LLVM 是一个 C++ 项目,C API 只是它的一 层封装。)
1 | $ cc `llvm-config --cflags` -c sum.c |
Bitcode
最后一件事。我前面提到 LLVM IR 有三种表示方式,其中有 bitcode。当你有一个完整 的模块时,就可以输出 bitcode 并写到一个文件中。
1 | if (LLVMWriteBitcodeToFile(mod, "sum.bc") != 0) { |
这样,你就可以使用工具来处理它,像用 llvm-dis
把 bitcode 反汇编为 LLVM IR 汇
编语言。
1 | $ llvm-dis sum.bc |
Source code of example
以下是前面程序的完整源码:
1 | /** |
想了解如何在你的机器上构建这个例子,请查看 GitHub 仓库中的 Makefile 及其他细节。
译注:原文发表在 2015 年,我本机下载的 LLVM 源码自己编译出的 clang 7.0 只能编 译通过并不能正常运行。为了编译通过及尝试修改使其能够运行,对代码做了部分,但还 是不能运行,原代码请参考原文。