Cython - 实现回调函数

11 投票
2 回答
5951 浏览
提问于 2025-04-16 13:19

我一直在使用Cython,想要和一个用C++写的库进行交互。到目前为止,进展还不错,我可以有效地使用这个库中的大部分功能。我的唯一问题是实现回调函数。这个库有4个函数定义,看起来大概是这样的:

typedef void (*Function1)(const uint16_t *data, 
                         unsigned width, unsigned height);
void SetCallBack(Function1);

为了实现这些回调,我想用Cython做一些事情:

ctypedef void (*Function1)(unsigned short *data, 
                         unsigned width, unsigned height);
cdef extern from "lib.hpp":
    void SetCallBack(Function1)

这个代码实际上编译得很正确,但我就是想不出该怎么实现回调函数,让它能正常工作。我首先尝试创建一个函数,像平常调用其他函数那样来调用它,写成这样:

def PySetCallBack(Func):
    SetCallBack(Func)

但是这给了我一个(可以预料的)错误:

“无法将Python对象转换为'Function1'”

所以,我现在就卡在这里。如果有人有在Cython中设置回调的经验,我会非常感激任何帮助。谢谢。

编辑: 根据你的建议,我创建了一个带有cdef的中间函数,代码看起来是这样的:

cdef void cSetCallBack(Function1 function):
    SetCallBack(function)

这似乎让我... 更接近了?至少现在得到了一个不同的错误:

error: invalid conversion from ‘void (*)(short unsigned int*, unsigned int, unsigned int)’ to ‘void (*)(const uint16_t*, unsigned int, unsigned int)’

现在,按照我的理解,这些类型是相同的,所以我搞不清楚发生了什么。

编辑2: 通过声明一个新的typedef解决了那个问题:

ctypedef unsigned short uint16_t

并把它作为参数来调用,但显然这并没有让我更接近目标,只是让我走了个弯路,因为当我尝试调用那个函数时,又得到了同样的“无法将Python对象转换为'Function1'”的错误。

所以,我基本上又回到了起点。现在我能想到的唯一办法就是明确地将传入的Python对象转换为C函数那样,但说实话,我完全不知道该怎么做。

第三次编辑: 好吧,在仔细分析你的回答后,我终于明白了,并且它有效,所以太好了。我最终做的是创建了一个这样的函数:

cdef void cSetCallback(Function1 function):
    SetCallback(function)
cdef void callcallback(const_ushort *data, unsigned width, unsigned height):
    global callbackfunc
    callbackfunc(data,width,height)
cSetCallback(callcallback)
def PySetCallback(callbackFunc):
    global callbackfunc
    callbackfunc = callbackFunc

所以现在唯一的问题是,它无法将const_ushort *data转换为Python对象,但那是另一个完全不同的问题,所以我想这个问题解决了,非常感谢。

2 个回答

9

最近,我遇到了一个需要把现有的C++库和Python结合起来的情况,使用了Cython,并且大量使用了事件和回调。找到相关资料并不容易,所以我想把这些内容整理在这里:

首先,我们需要包装一个C++的回调类(这个类是基于'double (METHOD)(void)'的格式,不过也可以使用模板,因为Cython可以处理模板):

ALabCallBack.h:

#ifndef ALABCALLBACK_H_
#define ALABCALLBACK_H_


#include <iostream>

using namespace std;

namespace elps {

//template < typename ReturnType, typename Parameter >
class ALabCallBack {
public:

    typedef double (*Method)(void *param, void *user_data);

    ALabCallBack();
    ALabCallBack(Method method, void *user_data);
    virtual ~ALabCallBack();

    double cy_execute(void *parameter);

    bool IsCythonCall()
    {
        return is_cy_call;
    }

protected:

    bool is_cy_call;

private:

    //void *_param;
    Method _method;
    void *_user_data;

};


} /* namespace elps */
#endif /* ALABCALLBACK_H_ */

ALabCallBack.cpp:

#include "ALabCallBack.h"

namespace elps {


ALabCallBack::ALabCallBack() {
    is_cy_call = true;
};

ALabCallBack::~ALabCallBack() {
};

ALabCallBack::ALabCallBack(Method method, void *user_data) {
    is_cy_call = true;
    _method = method;
    _user_data = user_data;
};

double ALabCallBack::cy_execute(void *parameter)
{
    return _method(parameter, _user_data);
};


} /* namespace elps */

这里:

  • 'callback' :: 这是一个模式/转换方法,用来从C类型的信息触发一个Python对象的方法(=Method)

  • 'method' :: 这是由Python用户传入的实际方法(=user_data)

  • 'parameter' :: 这是要传递给'method'的参数

接下来,我们需要实现.pyx文件……

我们的基本原型:

ctypedef double (*Method)(void *param, void *user_data)

然后,我们为C++类提供一个Cython的包装:

cdef extern from "../inc/ALabCallBack.h" namespace "elps" :
    cdef cppclass ALabCallBack:
        ALabCallBack(Method method, void *user_data)
        double cy_execute(void *parameter)

这是用来将C类型的格式转换为Python对象调用的模式/转换方法:

cdef double callback(void *parameter, void *method):
    return (<object>method)(<object>parameter)

现在让我们把这些功能嵌入到一个Cython类中:

cdef class PyLabCallBack:
    cdef ALabCallBack* thisptr

    def __cinit__(self, method):
        # 'callback' :: The pattern/converter method to fire a Python 
        #               object method from C typed infos
        # 'method'   :: The effective method passed by the Python user 
       self.thisptr = new ALabCallBack(callback, <void*>method)

    def __dealloc__(self):
       if self.thisptr:
           del self.thisptr

    cpdef double execute(self, parameter):
        # 'parameter' :: The parameter to be passed to the 'method'
        return self.thisptr.cy_execute(<void*>parameter)

编辑:对执行函数的类型进行更好的定义:def execute => cpdef double

就这样。调用它就像这样:

def func(obj):
    print obj 
    obj.Test()     # Call to a specific method from class 'PyLabNode'
    return obj.d_prop

n = PyLabNode()    # Custom class of my own
cb = PyLabCallBack(func)
print cb.execute(n)

由于Python是隐式类型的,我们可以在触发回调时访问与传入的对象类相关的'obj'对象的属性。

这个方法也可以很容易地适应纯C的实现。如果你能看到任何可能的改进,请告诉我(在我的情况下,性能非常关键,因为事件被频繁触发)。

5

如果你能修改这个库来定义:

typedef void (*Function1)(const uint16_t *data, 
                          unsigned width, unsigned height,
                          void *user_data);
void SetCallBack(Function1, void*);

那么我担心你会遇到麻烦。如果你有一个void*,那么你需要定义一个函数,这个函数可以用正确的参数调用一个Python可调用对象,然后用这个函数和Python可调用对象一起调用SetCallBack

如果你不能这样做,但回调是全局的(看起来是这样),你可以创建一个全局变量来存储这个Python对象。然后你再创建一个函数来调用这个Python对象,并把它传递给SetCallBack,这样你的PySetCallback就可以设置全局变量,并确保正确的函数被注册。

如果回调是特定于某个上下文的,但你没有办法传递一个“用户数据”指针,那我担心你在这里也会遇到麻烦。

我知道Python和C++,但不懂Cython,所以我不知道你是否可以在Cython中创建这个函数,或者你是否必须用C++来写。

撰写回答