如何开始使用 LLVM C API

我热衷于把玩有意思的编程语言,以求能更好的理解编译器(并且最终深入其所依赖的机器底层)是如何运作的,也会去尝试那些不在我拿手技艺之列的技术。LLVM 非常棒,因为摆弄它并将其作为一个后端进行连接,以使其生成能在很多的平台上快速运行的代码. 如果我仅仅只是想看看我的代码时怎么执行的,我可能只会去用一用简单的手动解释器, 但一上手 LLVM 的 JIT, 它的优化套件,以及对平台的支持就像一台超级跑车 — 你的小玩意儿的表现也能令人印象深刻。另外,LLVM 是诸如 Emscripten 和 Rust 这些东西的基础, 因此我喜欢靠直觉来了解我所感兴趣的新技术是如何实现的.

我将要向你展示的,是如何使用 LLVM API 去以编程的方式构建一个函数,你可以想调用其它函数一样调用它,并且可以让其直接以你所用平台的机器语言形式运行。

在本例中,我将使用 C 的 API, 因为它在 LLVM 中是可以使用的,再加上一个 C++ 的 API, 就是起步的最简单方式了.  其它的语言也有到 LLVM API 的绑定 — Python, OCaml, Go, Rust — 但 LLVM 生成代码这一过程后面的原理在所有封装的API中都是一样的.

本例会稍微跳到编译器构建的中间阶段. 假设前端 (词法、语法分析器,类型检查器) 已经构建了一个 AST 并且我们现在正在为后端遍历中间形式的代码,让机器代码得到优化后被生成出来.

在这种情况,我们就只是输入了直线形式的过程代码,得到的函数会在一个AST遍历函数中被动态的拼凑在一起, 当遇到树中特定的节点时就会调用 LLVM 的 API.

针对本例,我们将会构建一个简单的加法函数,它会以两个整型数作为入参并返回他们的和,同等的C标识方式如下:

int sum(int a, int b) {
    return a + b;
}

我们要清除现在正在做的事情: 我们正使用 LLVM 构建这个函数在内存中的表现形式, 使用它的 API 来设置项函数的进入和退出,返回和参数类型,以及实际的加法指令这些东西. 一旦这一内存表现形式构建完成, 我们就可以指示 LLVM 跳转到它,并使用我们提供的参数来执行它, 就好像它是一个从像C这样的语言编译而来的可执行的东西.

点击这里查询最终的代码.

模块

第一步是要去创建一个模块. LLVM中一个模块就是一个由全局变量,函数,外部引用以及其它数据组成的集合. 这里的模块不怎么样比方说Python这样的语言中的模块, 它们并不提供独立的命名空间. 但它们是所有构建在LLVM中的东西的顶层容器, 因此我们从创建一个这样的模块开始.

LLVMModuleRef mod = LLVMModuleCreateWithName("my_module");

传入模块工厂函数的字符串 "my_module" 是你所选择的模块标识.

请注意当你正在浏览 LLVM C API 文档 时, 不同的方面会在不同的头包含下被组织在一起. 我在这里详细介绍的大多数东西,比如模块和函数,都包含在 Core.h头下, 而随着我们继续深入,我也将会涵盖其它的东西.

类型

接下来,我创建sum函数,并将其添加到模块中。一个函数会包含如下元素:

稍后我会解释基础块. 首先,我们要处理函数的类型和参数类型 — 用C的术语说,就它的原型 — 并将其添加到模块中.

LLVMTypeRef param_types[] = { LLVMInt32Type(), LLVMInt32Type() };
LLVMTypeRef ret_type = LLVMFunctionType(LLVMInt32Type(), param_types, 2, 0);
LLVMValueRef sum = LLVMAddFunction(mod, "sum", ret_type);

LLVM 的类型对应我们的目标平台上的类型, 比如固定位宽的整型和浮点数, 指针, 结构,以及数组. (没有像C中那样的平台独立的类型,在C中,整型的大小, 32- 或者 64-位,是依赖于机器的架构的。)

LLVM类型拥有构造函数,并遵循“LLVM*TYPE*Type()”格式。在我们的例子中,传递到sum函数的参数,以及函数类型本身都是32位整型,所以我们可以使用LLVMInt32Type()。

按照顺序,传递到LLVMFunctionType()的参数如下:

1.函数的类型(返回类型)

2.函数的参数类型向量(函数的参数个数应该和数组中的类型个数相匹配)

3.函数的参数个数

4.一个boolean类型,表示函数是否是可变的,或者接受一个可变的参数

请注意,函数类型构造函数返回一个类型引用。这强化了一个概念,即我们在LLVM里面所做的等同于C中的函数原型声明。

这里的第三行增加了函数类型到模块,并命名为sum。我们获取到一个值引用,可以将它认作是代码(实际上是内存)中的固定位置,在它之上可以增加函数体,这是我们下面要做的。


基本块

下一步是增加基本块到函数。基本块是只有一个入口点和出口点的部分代码,换句话说,除了一步步按照一系列指令执行外,没有其它的方式来执行。没有if/else,while,loop,或任意类型的jump。基本块是模型控制流以及后续优化的关键,因此,LLVM具备增加这些到进展中的模块的一流支持。

LLVMBasicBlockRef entry = LLVMAppendBasicBlock(sum, "entry");

注意函数名中的“append”:它有助于我们了解,当运行中的代码块不断增加的时候,我们正在做什么。由此,相对于我们之前增加到模块中的函数,基本块是增加的。

指令创建者

与指令创建者相符合的概念,即我们如何增加指令到函数基本块。

LLVMBuilderRef builder = LLVMCreateBuilder();
LLVMPositionBuilderAtEnd(builder, entry);

类似于增加基本块到函数,我们设定创建者在基本块留下的入口编写指令。

LLVM中间表示

补充说明:LLVM的主要用途就是使用LLVM实现中间表示,或者简写为IR。我认为LLVM的中间表示是C语言和汇编语言之间的中间表示。LLVM中间表示采用的是一种定义非常严格的语言,这就意味着这种语言优化程度高,与平台无关,LLVM就是因这两个特性而出名的。你再查看一下中间表示,你就会明白每个指令是如何转换为最终生成汇编语言的装载、存储和跳转指令的。LLVM的中间表示可看作以下三种东西:

你可以把clang或者其他工具所生成的LLVM中间表示看做文本语言或者位码。

回到我们刚才的示例。现在来到了我们加法函数的核心部分:最终将传递过来的两个整型的参数进行相加并返回结果给调用者的指令。

LLVMValueRef tmp = LLVMBuildAdd(builder, LLVMGetParam(sum, 0), LLVMGetParam(sum, 1), "tmp");
LLVMBuildRet(builder, tmp);

LLVMBuildAdd()持有编译器的引用,其中包括两个待相加的整数,一个提供返回结果的名字。(结果的名字是必须的,因为LLVM IR严格要求全部指令必须产生一个中间结果。这一点稍候可以在LLVM进一步简化或者被优化掉,但在当前生成指令过程中,我们先遵循它的约定。)显然我们希望进行相加的个数就是我们后面提供给编译器的参数,并且可以通过使用函数的参数中的LLVMGetParam()来获取:即对应我们所看到此方法中的第二个、第三个参数。

调用LLVMBuildRet()即可生成返回声明,以及相加指令执行后返回临时结果的序列。

分析 & 执行

指令编译阶段会生成我们的函数,至此模块已经完成。本例的下一个阶段将是关于如何设置让其运行。

首先,要进行模块验证。以确保我们的模块已经被成功创建,或者在缺少/颠倒某些步骤的情况下提示错误并中止。

char *error = NULL;
LLVMVerifyModule(mod, LLVMAbortProcessAction, &error);
LLVMDisposeMessage(error);

LLVM提供了两种执行我们所创建指令的途径:JIT(Just-in-time Compiler:即时编译)和解析器。优先采用与目标系统平台匹配的JIT,如果不行则退而求其次用解析器。不管怎样,执行我们代码的东东,我们称之为:

LLVMExecutionEngineRef engine;
error = NULL;
LLVMLinkInJIT();
LLVMInitializeNativeTarget();
if (LLVMCreateExecutionEngineForModule(&engine, mod, &error) != 0) {
    fprintf(stderr, "failed to create execution engine\n");
    abort();
}
if (error) {
    fprintf(stderr, "error: %s\n", error);
    LLVMDisposeMessage(error);
    exit(EXIT_FAILURE);
}

我们既可以通过硬编码的方式来指定待相加的整数,也可以简单地通过命令行的方式指定。

if (argc < 3) {
    fprintf(stderr, "usage: %s x y\n", argv[0]);
    exit(EXIT_FAILURE);
}
long long x = strtoll(argv[1], NULL, 10);
long long y = strtoll(argv[2], NULL, 10);

正如你看到的,我们需要将在宿主语言中表示的两个整数传递给LLVM中的匿名表示。LLVM提供了可以将进行参数类型转换的工厂方法:

LLVMGenericValueRef args[] = {
    LLVMCreateGenericValueOfInt(LLVMInt32Type(), x, 0),
    LLVMCreateGenericValueOfInt(LLVMInt32Type(), y, 0)
};

现在,见证奇迹的时刻到了:JIT调用!

LLVMGenericValueRef res = LLVMRunFunction(engine, sum, 2, args);

我们可以获得了结果,但仍然还处于LLVM的范畴。现在需要将其转换成C的风格,将上面的反转并输出相加的和:

printf("%d\n", (int)LLVMGenericValueToInt(res, 0))

搞掂收工!我们结合编程示例从头到尾进行了讲解,并且让其在我们本地的机器上运行。虽然对拥有控制流(如:if/else的实现)和传值优化的LLVM来说只是冰山一角,但我们也涵盖了在任何LLVM-IR-to-code程序中的都有的基础部分。

编译

引用LLVM的头文件,来链接相应的库,我们就可以编译这个程序了。尽管我们写了一个C语言程序,在链接阶段还是需要C++的连接器。(LLVM是一个C++的工程,C语言的API是其中的一个封装。)

$ cc `llvm-config --cflags` -c sum.c$ c++ `llvm-config --cxxflags --ldflags --libs core executionengine jit interpreter analysis native bitwriter --system-libs` sum.o -o sum$ ./sum 42 99141

Bitcode 位代码

最后,我前面提到过,LLVM IR有三种表示方式,包括字节码在内。你可以在编译好的模块上,将位代码写到文件中。

if (LLVMWriteBitcodeToFile(mod, "sum.bc") != 0) {
    fprintf(stderr, "error writing bitcode to file, skipping\n");
}

在这里,你可以使用工具来对其进行操作,例如使用 llvm-dis这样的工具将位代码反汇编程LLVM IR的文本形式汇编语言。

$ llvm-dis sum.bc$ cat sum.ll; ModuleID = 'sum.bc'target datalayout = "e-m:o-i64:64-f80:128-n8:16:32:64-S128"define i32 @sum(i32, i32) {entry:  %tmp = add i32 %0, %1  ret i32 %tmp}

代码示例

以上程序的完整代码如下:

/**
 * LLVM equivalent of:
 *
 * int sum(int a, int b) {
 *     return a + b;
 * }
 */

#include <llvm-c/Core.h>
#include <llvm-c/ExecutionEngine.h>
#include <llvm-c/Target.h>
#include <llvm-c/Analysis.h>
#include <llvm-c/BitWriter.h>

#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char const *argv[]) {
    LLVMModuleRef mod = LLVMModuleCreateWithName("my_module");

    LLVMTypeRef param_types[] = { LLVMInt32Type(), LLVMInt32Type() };
    LLVMTypeRef ret_type = LLVMFunctionType(LLVMInt32Type(), param_types, 2, 0);
    LLVMValueRef sum = LLVMAddFunction(mod, "sum", ret_type);

    LLVMBasicBlockRef entry = LLVMAppendBasicBlock(sum, "entry");

    LLVMBuilderRef builder = LLVMCreateBuilder();
    LLVMPositionBuilderAtEnd(builder, entry);
    LLVMValueRef tmp = LLVMBuildAdd(builder, LLVMGetParam(sum, 0), LLVMGetParam(sum, 1), "tmp");
    LLVMBuildRet(builder, tmp);

    char *error = NULL;
    LLVMVerifyModule(mod, LLVMAbortProcessAction, &error);
    LLVMDisposeMessage(error);

    LLVMExecutionEngineRef engine;
    error = NULL;
    LLVMLinkInJIT();
    LLVMInitializeNativeTarget();
    if (LLVMCreateExecutionEngineForModule(&engine, mod, &error) != 0) {
        fprintf(stderr, "failed to create execution engine\n");
        abort();
    }
    if (error) {
        fprintf(stderr, "error: %s\n", error);
        LLVMDisposeMessage(error);
        exit(EXIT_FAILURE);
    }

    if (argc < 3) {
        fprintf(stderr, "usage: %s x y\n", argv[0]);
        exit(EXIT_FAILURE);
    }
    long long x = strtoll(argv[1], NULL, 10);
    long long y = strtoll(argv[2], NULL, 10);

    LLVMGenericValueRef args[] = {
        LLVMCreateGenericValueOfInt(LLVMInt32Type(), x, 0),
        LLVMCreateGenericValueOfInt(LLVMInt32Type(), y, 0)
    };
    LLVMGenericValueRef res = LLVMRunFunction(engine, sum, 2, args);
    printf("%d\n", (int)LLVMGenericValueToInt(res, 0));

    // Write out bitcode to file
    if (LLVMWriteBitcodeToFile(mod, "sum.bc") != 0) {
        fprintf(stderr, "error writing bitcode to file, skipping\n");
    }

    LLVMDisposeBuilder(builder);
    LLVMDisposeExecutionEngine(engine);
}