围绕Python函数制作Cython包装器

时间:2018-06-26 13:28:56

标签: python c function callback cython

我有一个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同时运行)。

2 个答案:

答案 0 :(得分:0)

这个答案更多的是自己动手做的风格,虽然不间断,但您应该参考我的其他答案以获取简洁的答案。


这个答案是一个hack,有点过分,它仅适用于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的相应包装。

注意:将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

在运行时将知道paramsfun的地址,因此我们只需要链接-在生成的机器代码中替换占位符。

在我的实现中,我或多或少关注这篇出色的文章: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