【问题标题】:Making a Cython wrapper around Python function围绕 Python 函数制作 Cython 包装器
【发布时间】:2018-06-26 13:28:55
【问题描述】:

我有一个 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)

但是,我无法想出一种方法将其真正“放入”到 wrapping_f 的体内。

这个问题有一个非常糟糕的解决方案:我可以使用一个全局对象变量,但这迫使我复制粘贴多个本质上相同的包装函数实例,这些函数将使用不同的全局函数(我计划使用多个Python 函数同时运行)。

【问题讨论】:

  • 看看这个答案的后半部分:stackoverflow.com/a/34900829/4657412。困难在于您希望将一些状态附加到您的函数指针(因此它知道要调用什么 python 对象)并且 C 函数指针不能存储状态。因此,我相信在标准 C 中这真的是不可能的。我发现这样做的唯一方法是使用 ctypes 或 cffi 设法使用一些(隐藏的)非标准黑客(在运行时生成代码)来做到这一点

标签: python c function callback cython


【解决方案1】:

出于历史原因,我保留了另一个答案 - 它表明,如果没有 jit 编译,就无法做你想做的事,并帮助我理解 @DavidW 在 this answer 中的建议有多棒。

为了简单起见,我使用了一个稍微简单的函数签名,并相信您可以根据自己的需要进行更改。

这是一个闭包的蓝图,它让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

【讨论】:

    【解决方案2】:

    这个答案更像是自己动手做的风格,虽然不是很有趣,但你应该参考我的另一个答案以获得简洁的感受。


    这个答案是一个 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 个全局对象和两个包装函数来实现我们的目标:

    #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 部分(使用 void* 传递“未知数据”的“标准”方式)和第 4 部分(很高兴了解您的实际情况去创建一个运行时函数)。我认为应该对void* 方法的“引用计数”发出警告 - 回调和数据指针保存以备后用并不罕见,您确实需要小心确保对象在此之前仍然存在
    猜你喜欢
    • 2016-10-25
    • 1970-01-01
    • 2019-05-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-06-11
    • 2010-09-15
    • 1970-01-01
    相关资源
    最近更新 更多