Вызвать код 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.

"ВМ", вероятно, не так волшебно, как вы думаете:-) В конце концов, это просто машинный код, который генерируется во время выполнения в буфер и исполняется оттуда. В той степени, в которой этот код имеет доступ к другим символам в процессе, в котором он выполняется, он может делать все, что может делать любой другой код в этом процессе.

Другие вопросы по тегам