I have a C function which signature looks like this:
我有一个C函数,签名看起来像这样:
typedef double (*func_t)(double*, int)
int some_f(func_t myFunc);
I would like to pass a Python function (not necessarily explicitly) as an argument for some_f. Unfortunately, I can't afford to alter declaration of some_f, that's it: I shouldn't change C code.
我想传递一个Python函数(不一定是显式的)作为some_f的参数。不幸的是,我无法改变some_f的声明,就是这样:我不应该改变C代码。
One obvious thing I tried to do is to create a basic wrapping function like this:
我试图做的一件显而易见的事情就是创建一个这样的基本包装函数:
cdef double wraping_f(double *d, int i /*?, object f */):
/*do stuff*/
return <double>f(d_t)
However, I can't come up with a way to actually "put" it inside wrapping_f's body.
但是,我无法想出一种方法来实际将其“放入”wrap_f的主体中。
There is a very bad solution to this problem: I could use a global object variable, however this forces me copy-n-paste multiple instances of essentially same wrapper function that will use different global functions (I am planning to use multiple Python functions simultaneously).
这个问题有一个非常糟糕的解决方案:我可以使用全局对象变量,但这会强制我复制粘贴基本相同的包装函数的多个实例,这些实例将使用不同的全局函数(我计划同时使用多个Python函数) )。
2 个解决方案
#1
0
I keep my other answer for historical reasons - it shows, that there is no way to do what you want without jit-compilation and helped me to understand how great @DavidW's advise in this answer was.
出于历史原因,我保留了我的另一个答案 - 它表明,没有jit-compilation就没有办法做你想做的事情,并帮助我理解@DavidW在这个答案中的建议是多么伟大。
For the sake of simplicity, I use a slightly simpler signature of functions and trust you to change it accordingly to your needs.
为简单起见,我使用稍微简单的功能签名,并相信您可以根据需要进行更改。
Here is a blueprint for a closure, which lets ctypes
do the jit-compilation behind the scenes:
这是一个闭包的蓝图,它让ctypes在幕后进行jit-compilation:
%%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()))
And now using it:
现在使用它:
>>> cl1, cl2=Closure(lambda x:2*x), Closure(lambda x:3*x)
>>> use_closure(cl1)
84
>>> use_closure(cl2)
126
#2
0
This answer is more in Do-It-Yourself style and while not unintersting you should refer to my other answer for a concise recept.
这个答案更多的是“自己动手”的风格,虽然没有取得联系,但你应该参考我的另一个答案,以获得简洁的接收。
This answer is a hack and a little bit over the top, it only works for Linux64 and probably should not be recommended - yet I just cannot stop myself from posting it.
这个答案是一个黑客,有点超过顶部,它只适用于Linux64,可能不应该被推荐 - 但我不能阻止自己发布它。
There are actually four versions:
实际上有四个版本:
- how easy the life could be, if the API would take the possibility of closures into consideration
- using a global state to produce a single closure [also considered by you]
- using multiple global states to produce multiple closures at the same time [also considered by you]
- using jit-compiled functions to produce an arbitrary number of closures at the same time
如果API考虑到关闭的可能性,那么生命将会变得多么容易
使用全局状态生成单个闭包[也由您考虑]
使用多个全局状态同时生成多个闭包[也由您考虑]
使用jit-compiled函数同时生成任意数量的闭包
For the sake of simplicity I chose a simpler signature of func_t
- int (*func_t)(void)
.
为了简单起见,我选择了一个更简单的func_t - int(* func_t)(void)签名。
I know, you cannot change the API. Yet I cannot embark on a journey full of pain, without mentioning how simple it could be... There is a quite common trick to fake closures with function pointers - just add an additional parameter to your API (normally void *
), i.e:
我知道,你不能改变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))
We basically use void *params
to pass the inner state of the closure to fun
and so the result of fun
can depend on this state.
我们基本上使用void * params来将闭包的内部状态传递给fun,因此fun的结果可能取决于这种状态。
The behavior is as expected:
行为符合预期:
>>> doit('A')
A
1
But alas, the API is how it is. We could use a global pointer and a wrapper to pass the information:
但唉,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))
With the expected behavior:
有了预期的行为:
>>> doit('A')
A
1
create_fun
is just convenience, which sets the global object and return the corresponding wrapper around the original function fun
.
create_fun只是方便,它设置全局对象并返回原始函数fun的相应包装器。
NB: It would be safer to make obj_a
a Python-object, because void *
could become dangling - but to keep the code nearer to versions 1 and 4 we use void *
instead of object
.
注意:使obj_a成为Python对象会更安全,因为void *可能会变得悬空 - 但为了使代码更接近版本1和4,我们使用void *而不是object。
But what if there are more than one closure in use at the same time, let's say 2? Obviously with the approach above we need 2 global objects and two wrapper functions to achieve our goal:
但是,如果同时使用多个闭包,请说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)
After compiling, as expected:
编译后,如预期:
>>> doit('A')
AA
2
A
1
But what if we have to provide an arbitrary number of function-pointers at the same time?
但是如果我们必须同时提供任意数量的函数指针呢?
The problem is, that we need to create the wrapper-functions at the run time, because there is no way to know how many we will need while compiling, so the only thing I can think of is to jit-compile these wrapper-functions when they are needed.
问题是,我们需要在运行时创建包装器函数,因为在编译时无法知道我们需要多少,所以我唯一能想到的是jit-compile这些包装器函数什么时候需要它们。
The wrapper function looks quite simple, here in assembler:
包装器函数看起来很简单,在汇编程序中:
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
The addresses of params
and of fun
will be known at run time, so we just have to link - replace the placeholder in the resulting machine code.
params和fun的地址将在运行时知道,因此我们只需链接 - 在生成的机器代码中替换占位符。
In my implementation I'm following more or less this great article: https://eli.thegreenplace.net/2017/adventures-in-jit-compilation-part-4-in-python/
在我的实现中,我或多或少地关注这篇伟大的文章: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)
And now, the expected behavior:
现在,预期的行为:
>>doit('A')
AA
2
A
1
AAA
3
After looking at this, maybe there is a change the API can be changed?
看了这个之后,也许有一个改变API可以改变?
#1
0
I keep my other answer for historical reasons - it shows, that there is no way to do what you want without jit-compilation and helped me to understand how great @DavidW's advise in this answer was.
出于历史原因,我保留了我的另一个答案 - 它表明,没有jit-compilation就没有办法做你想做的事情,并帮助我理解@DavidW在这个答案中的建议是多么伟大。
For the sake of simplicity, I use a slightly simpler signature of functions and trust you to change it accordingly to your needs.
为简单起见,我使用稍微简单的功能签名,并相信您可以根据需要进行更改。
Here is a blueprint for a closure, which lets ctypes
do the jit-compilation behind the scenes:
这是一个闭包的蓝图,它让ctypes在幕后进行jit-compilation:
%%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()))
And now using it:
现在使用它:
>>> cl1, cl2=Closure(lambda x:2*x), Closure(lambda x:3*x)
>>> use_closure(cl1)
84
>>> use_closure(cl2)
126
#2
0
This answer is more in Do-It-Yourself style and while not unintersting you should refer to my other answer for a concise recept.
这个答案更多的是“自己动手”的风格,虽然没有取得联系,但你应该参考我的另一个答案,以获得简洁的接收。
This answer is a hack and a little bit over the top, it only works for Linux64 and probably should not be recommended - yet I just cannot stop myself from posting it.
这个答案是一个黑客,有点超过顶部,它只适用于Linux64,可能不应该被推荐 - 但我不能阻止自己发布它。
There are actually four versions:
实际上有四个版本:
- how easy the life could be, if the API would take the possibility of closures into consideration
- using a global state to produce a single closure [also considered by you]
- using multiple global states to produce multiple closures at the same time [also considered by you]
- using jit-compiled functions to produce an arbitrary number of closures at the same time
如果API考虑到关闭的可能性,那么生命将会变得多么容易
使用全局状态生成单个闭包[也由您考虑]
使用多个全局状态同时生成多个闭包[也由您考虑]
使用jit-compiled函数同时生成任意数量的闭包
For the sake of simplicity I chose a simpler signature of func_t
- int (*func_t)(void)
.
为了简单起见,我选择了一个更简单的func_t - int(* func_t)(void)签名。
I know, you cannot change the API. Yet I cannot embark on a journey full of pain, without mentioning how simple it could be... There is a quite common trick to fake closures with function pointers - just add an additional parameter to your API (normally void *
), i.e:
我知道,你不能改变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))
We basically use void *params
to pass the inner state of the closure to fun
and so the result of fun
can depend on this state.
我们基本上使用void * params来将闭包的内部状态传递给fun,因此fun的结果可能取决于这种状态。
The behavior is as expected:
行为符合预期:
>>> doit('A')
A
1
But alas, the API is how it is. We could use a global pointer and a wrapper to pass the information:
但唉,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))
With the expected behavior:
有了预期的行为:
>>> doit('A')
A
1
create_fun
is just convenience, which sets the global object and return the corresponding wrapper around the original function fun
.
create_fun只是方便,它设置全局对象并返回原始函数fun的相应包装器。
NB: It would be safer to make obj_a
a Python-object, because void *
could become dangling - but to keep the code nearer to versions 1 and 4 we use void *
instead of object
.
注意:使obj_a成为Python对象会更安全,因为void *可能会变得悬空 - 但为了使代码更接近版本1和4,我们使用void *而不是object。
But what if there are more than one closure in use at the same time, let's say 2? Obviously with the approach above we need 2 global objects and two wrapper functions to achieve our goal:
但是,如果同时使用多个闭包,请说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)
After compiling, as expected:
编译后,如预期:
>>> doit('A')
AA
2
A
1
But what if we have to provide an arbitrary number of function-pointers at the same time?
但是如果我们必须同时提供任意数量的函数指针呢?
The problem is, that we need to create the wrapper-functions at the run time, because there is no way to know how many we will need while compiling, so the only thing I can think of is to jit-compile these wrapper-functions when they are needed.
问题是,我们需要在运行时创建包装器函数,因为在编译时无法知道我们需要多少,所以我唯一能想到的是jit-compile这些包装器函数什么时候需要它们。
The wrapper function looks quite simple, here in assembler:
包装器函数看起来很简单,在汇编程序中:
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
The addresses of params
and of fun
will be known at run time, so we just have to link - replace the placeholder in the resulting machine code.
params和fun的地址将在运行时知道,因此我们只需链接 - 在生成的机器代码中替换占位符。
In my implementation I'm following more or less this great article: https://eli.thegreenplace.net/2017/adventures-in-jit-compilation-part-4-in-python/
在我的实现中,我或多或少地关注这篇伟大的文章: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)
And now, the expected behavior:
现在,预期的行为:
>>doit('A')
AA
2
A
1
AAA
3
After looking at this, maybe there is a change the API can be changed?
看了这个之后,也许有一个改变API可以改变?