从LLVM JIT调用Python代码
我正在用Python写一个语言的词法分析器、解析器和编译器,之后会在LLVM的即时编译虚拟机(JIT-VM)中运行(使用llvm-py
)。前两个步骤现在都比较简单,但我发现一个问题,就是我的代码想要调用Python代码(一般来说),或者特别地与Python的词法分析器、解析器和编译器进行交互时,会遇到麻烦。我的主要担忧是,代码应该能够在运行时动态加载额外的代码到虚拟机中,因此必须从虚拟机内部触发整个Python的词法分析器、解析器和编译器链。
首先:这可能吗?还是说一旦虚拟机启动就无法更改了?
如果可以的话,我目前想到三种可能的解决方案(我也欢迎其他建议):
- 从虚拟机“跳出来”,直接调用主进程的Python函数(也许可以把它注册为一个LLVM函数,某种方式重定向到主进程)。我没有找到关于这个的任何资料,而且我也不确定这是否是个好主意(涉及安全性等问题)。
- 将运行时编译成LLVM汇编或中间表示(IR),可以是静态的或动态的。这要求IR代码能够修改它运行的虚拟机。
- 将运行时静态编译成一个库,然后直接加载到虚拟机中。同样,它必须能够向运行的虚拟机添加函数等。
2 个回答
你可以在LLVM JIT编译的代码中调用外部的C函数。你还需要什么呢?
这些外部函数会在正在运行的程序中被找到,这意味着如果你把Python链接到你的虚拟机中,你就可以调用Python的C API函数。
所谓的“虚拟机”其实没有你想象中那么神秘 :-) 最终,它只是在运行时生成的一段机器代码,这段代码会被放到一个缓冲区中并从那里执行。只要这段代码能够访问到它运行的程序中的其他符号,它就可以做任何这个程序中其他代码能做的事情。
正如Eli所说,你可以随意调用Python的C-API。当你在LLVM的即时编译器(JIT)内部调用外部函数时,它实际上就是在进程空间中使用了dlopen()
。所以如果你是在llvmpy内部运行,你已经可以访问所有的Python解释器符号,甚至可以和调用了ExecutionEngine的活跃解释器进行交互,或者如果需要的话,可以启动一个新的Python解释器。
为了帮助你入门,先创建一个新的C文件,里面放我们的评估器。
#include <Python.h>
void python_eval(const char* s)
{
PyCodeObject* code = (PyCodeObject*) Py_CompileString(s, "example", Py_file_input);
PyObject* main_module = PyImport_AddModule("__main__");
PyObject* global_dict = PyModule_GetDict(main_module);
PyObject* local_dict = PyDict_New();
PyObject* obj = PyEval_EvalCode(code, global_dict, local_dict);
PyObject* result = PyObject_Str(obj);
// Print the result if you want.
// PyObject_Print(result, stdout, 0);
}
这里有一个小的Makefile来编译它:
CC = gcc
LPYTHON = $(shell python-config --includes)
CFLAGS = -shared -fPIC -lpthread $(LPYTHON)
.PHONY: all clean
all:
$(CC) $(CFLAGS) cbits.c -o cbits.so
clean:
-rm cbits.c
接下来,我们从LLVM的常规模板开始,但使用ctypes将我们的cbits.so
共享库加载到全局进程空间,这样我们就可以使用python_eval
这个符号。然后,创建一个简单的LLVM模块,里面有一个函数,使用ctypes分配一个包含一些Python源代码的字符串,并将指针传递给正在运行我们模块中JIT编译函数的ExecutionEngine,这样就可以将Python源代码传递给调用Python C-API的C函数,然后再返回给LLVM的JIT。
import llvm.core as lc
import llvm.ee as le
import ctypes
import inspect
ctypes._dlopen('./cbits.so', ctypes.RTLD_GLOBAL)
pointer = lc.Type.pointer
i32 = lc.Type.int(32)
i64 = lc.Type.int(64)
char_type = lc.Type.int(8)
string_type = pointer(char_type)
zero = lc.Constant.int(i64, 0)
def build():
mod = lc.Module.new('call python')
evalfn = lc.Function.new(mod,
lc.Type.function(lc.Type.void(),
[string_type], False), "python_eval")
funty = lc.Type.function(lc.Type.void(), [string_type])
fn = lc.Function.new(mod, funty, "call")
fn_arg0 = fn.args[0]
fn_arg0.name = "input"
block = fn.append_basic_block("entry")
builder = lc.Builder.new(block)
builder.call(evalfn, [fn_arg0])
builder.ret_void()
return fn, mod
def run(fn, mod, buf):
tm = le.TargetMachine.new(features='', cm=le.CM_JITDEFAULT)
eb = le.EngineBuilder.new(mod)
engine = eb.create(tm)
ptr = ctypes.cast(buf, ctypes.c_voidp)
ax = le.GenericValue.pointer(ptr.value)
print 'IR'.center(80, '=')
print mod
mod.verify()
print 'Assembly'.center(80, '=')
print mod.to_native_assembly()
print 'Result'.center(80, '=')
engine.run_function(fn, [ax])
if __name__ == '__main__':
# If you want to evaluate the source of an existing function
# source_str = inspect.getsource(mypyfn)
# If you want to pass a source string
source_str = "print 'Hello from Python C-API inside of LLVM!'"
buf = ctypes.create_string_buffer(source_str)
fn, mod = build()
run(fn, mod, buf)
你应该会看到以下输出:
=======================================IR=======================================
; ModuleID = 'call python'
declare void @python_eval(i8*)
define void @call(i8* %input) {
entry:
call void @python_eval(i8* %input)
ret void
}
=====================================Result=====================================
Hello from Python C-API inside of LLVM!