带有类方法的 Cython 回调

Posted

技术标签:

【中文标题】带有类方法的 Cython 回调【英文标题】:Cython callback with class method 【发布时间】:2019-09-10 16:39:48 【问题描述】:

我开始使用 Cython 将一些 C++ 代码与 Python 连接起来。由于我需要在 C++ 中进行一些异步数据采集,因此我计划使用从 C++ 代码到 Python 的回调。到目前为止,我已经能够成功地从 C++ 调用 Python 函数,但是,我不能从 C++ 调用 Python 类方法。我在 *** 上看到过几个关于它的问题,但没有人为看似简单的事情提供简单的答案。这是一个工作玩具示例。

myclass.h:

    #ifndef MYCLASS_H
    #define MYCLASS_H

    #define PY_SSIZE_T_CLEAN
    #include <Python.h>

    typedef void (*callbackfun)(PyObject* obj);

    class MyClass
    
    public:
        void start(callbackfun user_func, PyObject* obj);
    ;

    #endif // MYCLASS_H

myclass.cpp:

    #include "myclass.h"

    void MyClass::start(callbackfun user_func, PyObject* obj)
    
        user_func(obj); 
    

PyMyClass.pyx:

    # distutils: language = c++
    # cython: language_level=3

    cdef extern from "myclass.h":
        ctypedef void (*callbackfun)(object obj)

        cdef cppclass MyClass:
            MyClass()
            void start(callbackfun user_func, object obj)

    cdef void callback(self):
        print('Called back')

    cdef class PyMyClass:
        cdef MyClass *c_myClass_ptr

        def __cinit__(self):
            self.c_myClass_ptr = new MyClass()

        def __dealloc__(self):
            del self.c_myClass_ptr

        def start(self):
           self.c_myClass_ptr.start(callback, self)

ma​​in.py:

    from PyMyClass import PyMyClass

    def main():
        myClass = PyMyClass()
        myClass.start()

    if __name__ == "__main__":
        main()

现在,如果我将PyMyClass.pyx中的callback函数转换为方法,如下:

PyMyClass.pyx:

# distutils: language = c++
# cython: language_level=3

cdef extern from "myclass.h":
    ctypedef void (*callbackfun)(object obj)

    cdef cppclass MyClass:
        MyClass()
        void start(callbackfun user_func, object obj)

cdef class PyMyClass:
    cdef MyClass *c_myClass_ptr

    def __cinit__(self):
        self.c_myClass_ptr = new MyClass()

    def __dealloc__(self):
        del self.c_myClass_ptr

    cdef void callback(self):
        print('Called back')

    def start(self):
       self.c_myClass_ptr.start(self.callback, self)

现在编译器抱怨:

Compiling PyMyClass.pyx because it changed.
[1/1] Cythonizing PyMyClass.pyx

Error compiling Cython file:
------------------------------------------------------------
...

    cdef void callback(self):
        print('Called back')

    def start(self):
       self.c_myClass_ptr.start(self.callback, self)                                   ^
------------------------------------------------------------

PyMyClass.pyx:24:36: Cannot assign type 'void (PyMyClass)' to 'callbackfun'
Traceback (most recent call last):
  File "setup.py", line 22, in <module>
    setup(ext_modules = cythonize(ext_modules))
  File "/Users/andreac/anaconda3/lib/python3.7/site-packages/Cython/Build/Dependencies.py", line 1097, in cythonize
    cythonize_one(*args)
  File "/Users/andreac/anaconda3/lib/python3.7/site-packages/Cython/Build/Dependencies.py", line 1220, in cythonize_one
    raise CompileError(None, pyx_file)
Cython.Compiler.Errors.CompileError: PyMyClass.pyx

是否有(简单)解决此问题的方法? (是的,我知道使用PyObject* 也存在潜在的引用计数问题)

【问题讨论】:

【参考方案1】:

您有一些问题,但解决起来相对简单:

    self.callback 正在尝试将函数与self 指针绑定。我不能 100% 确定在这种情况下它是如何工作的——它最终可能会创建一个可调用的 Python object,但无论哪种方式,它都绝对不符合你想要的回调函数。将其更改为&amp;PyMyClass.callback。我还添加了&amp;,因为 Cython 似乎需要它来了解情况。

    &amp;PyMyClass.callback 仍然与callbackfun 的签名不匹配。 callbackfun 需要一个通用 Python 对象,而 PyMyClass.callback 的第一个参数专门作为指向 PyMyClass 的底层 C 结构的指针。这很容易通过演员来解决,给

    self.c_myClass_ptr.start(<callbackfun>&PyMyClass.callback, self)
    

    这种对基于PyObject 的结构的转换是使用Python C API 的常见习惯用法,所以很好。不幸的是,强制转换最终会隐藏错误,所以要小心。

    你有一个潜在的引用计数灾难正在发生:你将一个“借来的引用”传递给self。您在问题中提到了这一点,但我会重申以防未来的读者错过它(就像我在第一次浏览时所做的那样)。在这种情况下,这很好 - 您立即使用 self 然后丢弃它。但是,如果您将其保存在您的 C++ 代码中,那么它可能会被破坏,从而使 C++ 代码具有有效的指针。设置回调时确实需要Py_INCREF,而取消设置回调时需要Py_DECREF。确保这种情况发生可能很难做到……

【讨论】:

以上是关于带有类方法的 Cython 回调的主要内容,如果未能解决你的问题,请参考以下文章

使用 python-c-api 调用的 Cython 回调段错误

如何在 python 包装中使用 unicode 字符串用于带有 cython 的 c++ 类?

Cython 扩展模块类方法不可见/命名 提及时出错

一种将 c++ 对象传递给 cython 中另一个对象方法的方法

如何使用 cython 将 python 类公开给 c++

使用 Cython 时如何将一个 C++ 类(引用)传递给另一个?