我有一个C函数,其签名如下所示:
typedef double (*func_t)(double*, int)
int some_f(func_t myFunc);
我想将Python函数(不一定显式)作为some_f的参数传递。不幸的是,我负担不起更改some_f的声明,就是这样:我不应该更改C代码。
我试图做的一件显而易见的事情是创建一个像这样的基本包装函数:
cdef double wraping_f(double *d, int i /*?, object f */):
/*do stuff*/
return <double>f(d_t)
但是,我无法提出一种将其实际“放入” wraping_f体内的方法。
此问题有一个非常糟糕的解决方案:我可以使用全局对象变量,但是这迫使我将多个基本相同的包装函数实例复制-粘贴到粘贴实例,这些实例将使用不同的全局函数(我打算使用多个Python同时运行)。
答案 0 :(得分:0)
这个答案更多的是自己动手做的风格,虽然不间断,但您应该参考我的其他答案以获取简洁的答案。
这个答案是一个hack,有点过分,它仅适用于Linux64,并且可能不建议使用-但我无法阻止自己发布它。
实际上有四个版本:
为简单起见,我选择了一个更简单的签名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
的相应包装。
注意:将obj_a
用作Python对象会更安全,因为void *
可能会变得悬而未决-但为了使代码更接近版本1和版本4,我们改用void *
的object
。
但是,如果同时使用多个闭包(假设2个)怎么办?显然,使用上面的方法,我们需要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了吗?
答案 1 :(得分:0)
出于历史原因,我保留了其他答案-它表明,不进行jit编译就无法完成您想要的事情,并帮助我了解了this answer中@DavidW的建议有多棒。
为简单起见,我使用功能的签名稍微简单一些,并相信您可以根据需要进行更改。
这是关闭的蓝图,它使ctypes
在后台进行jit编译:
%%cython
#needs Cython > 0.28 to run because of verbatim C-code
cdef extern from *: #fill some_t with life
"""
typedef int (*func_t)(int);
static int some_f(func_t fun){
return fun(42);
}
"""
ctypedef int (*func_t)(int)
int some_f(func_t myFunc)
#works with any recent Cython version:
import ctypes
cdef class Closure:
cdef object python_fun
cdef object jitted_wrapper
def inner_fun(self, int arg):
return self.python_fun(arg)
def __cinit__(self, python_fun):
self.python_fun=python_fun
ftype = ctypes.CFUNCTYPE(ctypes.c_int,ctypes.c_int) #define signature
self.jitted_wrapper=ftype(self.inner_fun) #jit the wrapper
cdef func_t get_fun_ptr(self):
return (<func_t *><size_t>ctypes.addressof(self.jitted_wrapper))[0]
def use_closure(Closure closure):
print(some_f(closure.get_fun_ptr()))
现在使用它:
>>> cl1, cl2=Closure(lambda x:2*x), Closure(lambda x:3*x)
>>> use_closure(cl1)
84
>>> use_closure(cl2)
126