Этот ответ скорее в стиле «Сделай сам», и, хотя он не является интересным, вам следует обратиться к моему другому ответу для получения краткого ответа.
Этот ответ является хакерским и немного чрезмерным, он работает только для Linux64 и, вероятно, его не следует рекомендовать - однако я просто не могу удержаться от публикации.
На самом деле существует четыре версии:
- насколько простой может быть жизнь, если API будет учитывать возможность закрытия
- использование глобального состояния для создания одного замыкания [также рассматривается вами]
- использование нескольких глобальных состояний для одновременного создания нескольких замыканий [также рассматривается вами]
- использование jit-скомпилированных функций для одновременного создания произвольного числа замыканий
Ради простоты я выбрал более простую подпись func_t
- int (*func_t)(void)
.
Я знаю, вы не можете изменить API. И все же я не могу отправиться в путешествие, полное боли, не говоря уже о том, как это просто может быть ... Существует довольно распространенная уловка - имитировать замыкания с помощью указателей функций - просто добавьте дополнительный параметр в свой API (обычно void *
), т.е. :
#version 1: Life could be so easy
# needs Cython >= 0.28 because of verbatim C-code feature
%%cython
cdef extern from *: #fill some_t with life
"""
typedef int (*func_t)(void *);
static int some_f(func_t fun, void *params){
return fun(params);
}
"""
ctypedef int (*func_t)(void *)
int some_f(func_t myFunc, void *params)
cdef int fun(void *obj):
print(<object>obj)
return len(<object>obj)
def doit(s):
cdef void *params = <void*>s
print(some_f(&fun, params))
Мы в основном используем void *params
, чтобы передать внутреннее состояние замыкания на fun
, и поэтому результат fun
может зависеть от этого состояния.
Поведение такое, как и ожидалось:
>>> doit('A')
A
1
Но, увы, API такой, какой он есть. Мы могли бы использовать глобальный указатель и оболочку для передачи информации:
#version 2: Use global variable for information exchange
# needs Cython >= 0.28 because of verbatim C-code feature
%%cython
cdef extern from *:
"""
typedef int (*func_t)();
static int some_f(func_t fun){
return fun();
}
static void *obj_a=NULL;
"""
ctypedef int (*func_t)()
int some_f(func_t myFunc)
void *obj_a
cdef int fun(void *obj):
print(<object>obj)
return len(<object>obj)
cdef int wrap_fun():
global obj_a
return fun(obj_a)
cdef func_t create_fun(obj):
global obj_a
obj_a=<void *>obj
return &wrap_fun
def doit(s):
cdef func_t fun = create_fun(s)
print(some_f(fun))
с ожидаемым поведением:
>>> doit('A')
A
1
create_fun
- это просто удобство, которое устанавливает глобальный объект и возвращает соответствующую оболочку вокруг исходной функции fun
.
NB. Было бы безопаснее сделать obj_a
Python-объектом, потому что void *
может стать висящим - но чтобы держать код ближе к версиям 1 и 4, мы используем void *
вместо object
.
Но что, если одновременно используется более одного замыкания, скажем, 2? Очевидно, что для достижения нашей цели нам потребуется 2 глобальных объекта и две функции-оболочки:
#version 3: two function pointers at the same time
%%cython
cdef extern from *:
"""
typedef int (*func_t)();
static int some_f(func_t fun){
return fun();
}
static void *obj_a=NULL;
static void *obj_b=NULL;
"""
ctypedef int (*func_t)()
int some_f(func_t myFunc)
void *obj_a
void *obj_b
cdef int fun(void *obj):
print(<object>obj)
return len(<object>obj)
cdef int wrap_fun_a():
global obj_a
return fun(obj_a)
cdef int wrap_fun_b():
global obj_b
return fun(obj_b)
cdef func_t create_fun(obj) except NULL:
global obj_a, obj_b
if obj_a == NULL:
obj_a=<void *>obj
return &wrap_fun_a
if obj_b == NULL:
obj_b=<void *>obj
return &wrap_fun_b
raise Exception("Not enough slots")
cdef void delete_fun(func_t fun):
global obj_a, obj_b
if fun == &wrap_fun_a:
obj_a=NULL
if fun == &wrap_fun_b:
obj_b=NULL
def doit(s):
ss = s+s
cdef func_t fun1 = create_fun(s)
cdef func_t fun2 = create_fun(ss)
print(some_f(fun2))
print(some_f(fun1))
delete_fun(fun1)
delete_fun(fun2)
После компиляции, как и ожидалось:
>>> doit('A')
AA
2
A
1
Но что, если нам нужно одновременно указать произвольное количество указателей на функции?
Проблема в том, что нам нужно создавать функции-оболочки во время выполнения, потому что нет способа узнать, сколько нам понадобится во время компиляции, поэтому единственное, о чем я могу думать, это собрать эти компиляторы jit функции-обертки, когда они необходимы.
Функция-обёртка выглядит довольно просто, здесь на ассемблере:
wrapper_fun:
movq address_of_params, %rdi ; void *param is the parameter of fun
movq address_of_fun, %rax ; addresse of the function which should be called
jmp *%rax ;jmp instead of call because it is last operation
Адреса params
и fun
будут известны во время выполнения, поэтому нам просто нужно связать - заменить заполнитель в полученном машинном коде.
В моей реализации я более или менее следую этой замечательной статье: https://eli.thegreenplace.net/2017/adventures-in-jit-compilation-part-4-in-python/
#4. version: jit-compiled wrapper
%%cython
from libc.string cimport memcpy
cdef extern from *:
"""
typedef int (*func_t)(void);
static int some_f(func_t fun){
return fun();
}
"""
ctypedef int (*func_t)()
int some_f(func_t myFunc)
cdef extern from "sys/mman.h":
void *mmap(void *addr, size_t length, int prot, int flags,
int fd, size_t offset);
int munmap(void *addr, size_t length);
int PROT_READ # #define PROT_READ 0x1 /* Page can be read. */
int PROT_WRITE # #define PROT_WRITE 0x2 /* Page can be written. */
int PROT_EXEC # #define PROT_EXEC 0x4 /* Page can be executed. */
int MAP_PRIVATE # #define MAP_PRIVATE 0x02 /* Changes are private. */
int MAP_ANONYMOUS # #define MAP_ANONYMOUS 0x20 /* Don't use a file. */
# |-----8-byte-placeholder ---|
blue_print = b'\x48\xbf\x00\x00\x00\x00\x00\x00\x00\x00' # movabs 8-byte-placeholder,%rdi
blue_print+= b'\x48\xb8\x00\x00\x00\x00\x00\x00\x00\x00' # movabs 8-byte-placeholder,%rax
blue_print+= b'\xff\xe0' # jmpq *%rax ; jump to address in %rax
cdef func_t link(void *obj, void *fun_ptr) except NULL:
cdef size_t N=len(blue_print)
cdef char *mem=<char *>mmap(NULL, N,
PROT_READ | PROT_WRITE | PROT_EXEC,
MAP_PRIVATE | MAP_ANONYMOUS,
-1,0)
if <long long int>mem==-1:
raise OSError("failed to allocated mmap")
#copy blueprint:
memcpy(mem, <char *>blue_print, N);
#inject object address:
memcpy(mem+2, &obj, 8);
#inject function address:
memcpy(mem+2+8+2, &fun_ptr, 8);
return <func_t>(mem)
cdef int fun(void *obj):
print(<object>obj)
return len(<object>obj)
cdef func_t create_fun(obj) except NULL:
return link(<void *>obj, <void *>&fun)
cdef void delete_fun(func_t fun):
munmap(fun, len(blue_print))
def doit(s):
ss, sss = s+s, s+s+s
cdef func_t fun1 = create_fun(s)
cdef func_t fun2 = create_fun(ss)
cdef func_t fun3 = create_fun(sss)
print(some_f(fun2))
print(some_f(fun1))
print(some_f(fun3))
delete_fun(fun1)
delete_fun(fun2)
delete_fun(fun3)
А теперь ожидаемое поведение:
>>doit('A')
AA
2
A
1
AAA
3
Посмотрев на это, может быть, есть изменения, которые можно изменить API?