llvmlite 学习笔记
通过µGo语言实现——从头开发一个迷你Go语言编译器 来学习llvmlite 的使用,以及将IR 编译为可执行程序。
Hello World
通过llvmlite 构建一个hello world!
程序,来展示代码的基本工作流程。下面是参考的C 代码:
#include<stdio.h>
char * fstr= "🦒 hello %s! \n\0";
int main(){
char * c_str = "world\0";
printf(fstr, c_str);
return 0;
}
但是要通过llvm 的IR 构建,就需要从类型声明开始、中间变量的创建与赋值都要按部就班地完成,调用printf
的部分参考自alendit/call_printf.py:
from llvmlite import ir
# 创建模块,模块名可以为空
module = ir.Module(name="main")
# 创建int32 类型
i32 = ir.IntType(32)
# 创建int8 * 指针类型
voidptr_ty = ir.IntType(8).as_pointer()
## 没有函数体的函数在IR 中会被写作declare
# 创建函数类型 int func(void * ...),支持可变长参数
printf_ty = ir.FunctionType(ir.IntType(32), [voidptr_ty], var_arg=True)
# 声明int printf(void * ...)
printf = ir.Function(module, printf_ty, name="printf")
# 注释的代码是不支持中文和emoji 的
# fmt = "hello %s!\n\0"
# c_fmt = ir.Constant(ir.ArrayType(ir.IntType(8), len(fmt)), bytearray(fmt.encode("utf8")))
fmt = bytearray("🦒 hello %s! \n\0".encode('utf-8')) # 创建一个可变字节数组对象
c_fmt = ir.Constant(ir.ArrayType(ir.IntType(8), len(fmt)),fmt) # 根据上面的对象创建一个字符串常量
global_fmt = ir.GlobalVariable(module, c_fmt.type, name="fstr") # 创建全局变量fstr
global_fmt.linkage = 'internal' # 链接方式
global_fmt.global_constant = True # 全局常量
global_fmt.initializer = c_fmt # 通过c_fmt 初始化
# 有函数体的函数会被写作define
# 创建函数类型int func(),无参数
fn_ty = ir.FunctionType(i32,())
# 创建int main() 函数
func = ir.Function(module, fn_ty, name='main')
# 添加函数体block
block = func.append_basic_block(name="entry")
# 构建函数体
builder = ir.IRBuilder(block)
## 创建函数体的局部变量
arg = "world\0" # 注意不要忘记\0
c_arg = ir.Constant(ir.ArrayType(ir.IntType(8), len(arg)), bytearray(arg.encode("utf8")))
# 分配空间
c_str = builder.alloca(c_arg.type)
# 赋值
builder.store(c_arg, c_str)
# 指针类型转换
fmt_arg = builder.bitcast(global_fmt, voidptr_ty)
builder.call(printf, [fmt_arg, c_str])
# 创建返回值
res = i32(0)
# 添加返回值
builder.ret(res)
生成IR
所有的代码都包含在module
变量中,可以将其以字符串的形式保存在文本文件中:
with open('./a.out.ll', 'w') as f:
f.write(str(module))
生成的中间代码如下:
; ModuleID = "main"
target triple = "unknown-unknown-unknown"
target datalayout = ""
declare i32 @"printf"(i8* %".1", ...)
@"fstr" = internal constant [17 x i8] c"\f0\9f\a6\92 hello %s! \0a\00"
define i32 @"main"()
{
entry:
%".2" = alloca [6 x i8]
store [6 x i8] c"world\00", [6 x i8]* %".2"
%".4" = bitcast [17 x i8]* @"fstr" to i8*
%".5" = call i32 (i8*, ...) @"printf"(i8* %".4", [6 x i8]* %".2")
ret i32 0
}
编译
因为llvmlite 暂时还没有集成lld
工具,所以只能通过系统命令将生成的.ll
源码或者.obj
文件编译成可执行程序。看Github 上的PR 898 应该在v4.0
正式版就将集成lld
工具。
import subprocess
p = subprocess.Popen("clang ./a.out.ll", shell=True, stdout=subprocess.PIPE)
r = p.stdout.read()
print(r)
测试
有以下三个命令来查看程序退出时的代码,ipynb
默认用的是cmd
。
*nix
:echo $?
powershell
:echo $LASTEXITCODE
cmd
:echo %errorlevel%
> ./a.exe
🦒 hello world!
> echo $LASTEXITCODE
0
# 编译C 到ir,用于对照代码
> clang -emit-llvm -Wimplicit-function-declaration -S -c main.c -o main.ll
从上面的实验结果可以得出:通过llvmlite 构建并编辑可执行程序是行得通的。但是真的要设计一门语言还要结合词法分析、语法分析、语义分析以及通过遍历AST 构建IR 的过程。此过程可以想象是比较繁琐的,通过面向对象的设计方法设计AST 节点可能能简化这一过程。但应该也不会太多。
一些细节
- 系统调用。最好创建一个标准库,重新封装各种系统调用如
printf
等,以避免引起不必要的bug; - 避免重名。创建模块时,可以给每个文件夹都创建一个
.ll
模块,而且变量名也以模块名为前缀,这样就能最大程度上避免重名问题; - 增量编译。可以检测
.ll
文件的创建时间或者标签,如果近期有修改则重新编译; - 外部变量(函数)。在每个
.ll
文件中都需要decalre
函数或者extern
变量然后通过函数或者变量的全名进行引用; - 语义分析。上面的的事情都需要在语义分析时处理。
坑
- 通过解析
.ll
得到的模块,其中的函数不能直接调用,所以在创建和使用标准库的时候需要在python 源码中定义,最好不要从.ll
文件中读取;