Вызвать код Python из LLVM JIT
Я пишу лексер / парсер / компилятор языка на Python, который должен работать в LLVM JIT-VM (используя llvm-py
) потом. Первые два шага на данный момент довольно просты, но (даже если я еще не запускал задачу компиляции) я вижу проблему, когда мой код хочет вызвать Python-Code (в целом) или взаимодействовать с лексером Python /parser/compiler (в особом порядке) соответственно. Моя главная проблема заключается в том, что код должен иметь возможность динамически загружать дополнительный код в виртуальную машину во время выполнения, и, таким образом, он должен запускать всю цепочку лексера / синтаксического анализатора / компилятора в Python изнутри виртуальной машины.
Прежде всего: возможно ли это, или виртуальная машина не отключается после запуска?
Если это так, я в настоящее время вижу 3 возможных решения (я открыт для других предложений)
- "Выключение" виртуальной машины и возможность прямого вызова функций Python основного процесса (возможно, путем регистрации ее как LLVM-функции, которая каким-либо образом перенаправляет на основной процесс). Я ничего не нашел по этому поводу и в любом случае я не уверен, если это хорошая идея (безопасность и тому подобное).
- Скомпилируйте среду выполнения (статически или динамически во время выполнения) в LLVM-Assembly/-IR. Это требует, чтобы IR-код мог изменять виртуальную машину, в которой он работает.
- Скомпилируйте среду выполнения (статически) в библиотеку и загрузите ее непосредственно в виртуальную машину. Опять же, он должен иметь возможность добавлять функции (и т. Д.) В виртуальную машину, в которой он работает.
2 ответа
Как сказал Илай, вас не остановит вызов Python C-API. Когда вы вызываете внешнюю функцию из JL LLVM, она эффективно использует 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 с функцией, выделите строку с некоторым источником Python с помощью ctypes и передайте указатель на ExecutionEngine, выполняющий функцию JIT'd из нашего модуля, который, в свою очередь, передает источник Python в C-функцию, которая вызывает Python C-API, а затем возвращает обратно в 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!
Вы можете вызывать внешние функции C из JL-кода LLVM. Что еще тебе нужно?
Эти внешние функции будут найдены в процессе выполнения, это означает, что если вы связываете Python с вашей виртуальной машиной, вы можете вызывать функции Python C API.
"ВМ", вероятно, не так волшебно, как вы думаете:-) В конце концов, это просто машинный код, который генерируется во время выполнения в буфер и исполняется оттуда. В той степени, в которой этот код имеет доступ к другим символам в процессе, в котором он выполняется, он может делать все, что может делать любой другой код в этом процессе.