如何在本机回调中使用 Cython cdef 类成员方法

Posted

技术标签:

【中文标题】如何在本机回调中使用 Cython cdef 类成员方法【英文标题】:How to use a Cython cdef class member method in a native callback 【发布时间】:2017-09-13 00:56:25 【问题描述】:

我有一个大量回调驱动的 C++ 库。回调注册为std::function 实例。

回调注册函数可能类似于:

void register(std::function<void(int)> callback);

我可以通过创建libcpp.functional.function 对象来注册普通的cdef 函数。假设我有这个回调:

cdef void my_callback(int n):
    # do something interesting with n

我可以注册成功:

cdef function[void(int)]* f_my_callback = new function[void(int)](my_callback)
register(deref(f_my_callback))

问题是我想注册cdef class方法作为回调。假设我有一堂课:

cdef class MyClass:

    cdef function[void(int)]* f_callback
    py_callback = lambda x: None

    # some more init/implementation

    def __init__(self):
        # ** this is where the problem is **
        self.f_callback = new function[void(int)](self.member_callback)
        register(deref(self.f_callback))

    cdef void member_callback(self, int n) with gil:
        self.py_callback(n)

    def register_py(self, py_callback):
        self.py_callback = py_callback

self.f_callback = new function[void(int)](self.member_callback) 行不起作用,因为 function[T] 构造函数看到了 MyClass 参数 (self)。在普通的python中,做self.member_callback基本上相当于将self部分应用到MyClass.member_callback,所以我以为这样就可以了,但事实并非如此。

我做了一个typedef:

ctypedef void (*member_type)(int)

然后,如果我投射,我可以让它编译:

self.f_callback = new function[void(int)](<member_type>self.member_callback)

但是当回调真正到来时,一切都被打破了。使用self 执行任何操作都会导致段错误。

将 cython 类成员方法作为 C/C++ 回调传递的“正确”方式是什么?

编辑

澄清一下,这个问题专门关于将成员方法(即以self 作为第一个参数的函数)传递给需要std::function 类型参数的C++ 函数。

如果我用@staticmethod 装饰member_callback 并删除对self 的所有引用,则一切正常。不幸的是,该方法需要访问self 才能正确执行类实例的逻辑。

【问题讨论】:

你见过***.com/questions/39044063/… - 它不是完全重复,但我认为解决方案看起来非常相似。 我认为有两个问题:1)您必须确保重新计算有效,并且 Cython 对象在 c++ 需要时保持活动状态(并且您将其转换为 void* 的解决方案具有可能在这里成为灾难)2)您需要正确的语法来实际调用函数,这与链接答案中的不同(我不知道立即的答案) 嗨@DavidW,是的,我已经研究了你对这个问题的回答。这个问题只是关于将回调传递给带有 std::function 参数的 C++ 函数。自从您回答以来,已添加 Cython 包装器以直接支持 std::function。我的问题特别是关于传递一个 method,即使用 self 参数。仅供参考,我的回调被调用,但参数错误。 另外,我不会强制转换为 void*。 typedef 将 member_type 定义为一个返回 void 并采用 int 的函数。在我的天真中,我假设 self.member_callback 与该签名匹配(即“点”运算符应用的 self 参数——而不是 MyClass.member_callback ,我认为它是一个返回 void 并采用 MyClass 和一个 int 的函数.编译器的错误说明我对这一点的理解是错误的... 我认为std::function 支持比您希望的要简陋得多。我很确定可以做你想做的事,但它不是那么简单,并且会涉及一些 C++ 包装代码。我很快就会有一个适当的去处 【参考方案1】:

std::function 可以接受一系列参数。在其最简单的形式中,它采用一个直接映射到其模板类型的函数指针,而这正是 Cython 包装器真正为它设置的所有内容。为了包装 C++ 成员函数,您通常会使用 std::mem_funstd::mem_fun_ref 来获取相关类实例的副本或引用(在现代 C++ 中,您也可以选择 lambda 函数,但这实际上只是为您提供相同的选项)。

将其转换为 Python/Cython,您需要将 PyObject* 保存到您正在调用其成员的对象,并有责任自己处理引用计数。不幸的是,Cython 目前无法为您生成包装器,因此您需要自己编写一个持有者。本质上,您需要一个处理引用计数的可调用对象。

在a previous answer 中,我展示了创建这种包装器的两种方法(其中一种是手动的,另一种是通过使用 Boost Python 来节省工作量,它已经实现了一些非常相似的东西)。我在那里展示的方案适用于 任何与相关签名匹配的 Python 可调用。虽然我用标准的 Python 模块级函数对其进行了说明,但它同样适用于成员函数,因为 instance.memberfunction 生成了一个绑定的成员函数——一个 Python 可调用函数,它同样可以工作。

对于您的问题,唯一的区别是您使用的是cdef 成员函数而不是def 成员函数。这看起来应该可以工作,但没有(最近版本的 Cython 尝试自动转换为 Python 可调用但它失败并出现运行时错误)。您可以创建一个 lambda 函数作为一个非常简单的包装器。从速度的角度来看,这有点不太理想,但具有使用现有代码的优势。

您需要对之前的答案进行如下修改:

如果您尝试使用手动 PyObjectWrapper 版本,请更改参数类型以匹配您的签名(即将 int, string&amp; 更改为 int)。对于 boost 版本,您不必这样做。

void register(std::function&lt;void(int)&gt; callback); 的包装必须是:

cdef extern from "whatever.hpp":
  void register(PyObjWrapper)
  # or
  void register(bpo)

取决于您使用的版本。这是在向 Cython 撒谎,但由于这两个对象都是可调用的 C++ 对象,因此 C++ 编译器会自动将它们转换为 std::function

register 称为register(PyObjWrapper(lambda x: self.member_callback(x)))register(get_as_bpo(lambda x: self.member_callback(x)))


可以创建一个更高效的版本,专门针对使用cdef 函数。它将基于一个与PyObjWrapper 非常相似的对象。但是,我不愿意在没有明确理由的情况下这样做,因为我已经编写的代码可以工作。

【讨论】:

我的问题比我说的要复杂一些。回调实际上是用 C++ 类实例调用的,这些实例不能传递给 python 可调用对象。但是,您的回答使我朝着正确的方向前进。 +1【参考方案2】:

我相信@DavidW 的回答对于所问问题的简化版本是正确的。

为了清楚起见,我提出了我的问题的简化版本,但据我所知,我的问题的真实版本需要稍微不同的方法。

具体来说,在我的简化版本中,我表示将使用int 调用回调。如果是这种情况,我可以想象提供一个可调用的 python(boost python 或PyObjWrapper 版本)。

但是,回调实际上是使用 std::shared_ptr&lt;T&gt; 调用的,其中 T 是库中的模板类。 cdef 回调需要对该实例做一些事情并最终调用一个 python 可调用对象。据我所知,没有办法使用像std::shared_ptr 这样的 C++ 对象调用 python 函数。

@DavidW 的回复非常有帮助,它让我找到了一个适用于这种情况的解决方案。我做了一个 C++ 实用函数:

// wrapper.hpp

#include <memory>
#include <functional>

#include "my_cpp_project.hpp"

template<typename T>
using cy_callback = void (*)(void*, std::shared_ptr<my_cpp_class<T>>);

template<typename T>
class function_wrapper 
public:

    static
    std::function<void(std::shared_ptr<my_cpp_class<T>>)>
    make_std_function(cy_callback<T> callback, void* c_self)
    
        std::function<void(std::shared_ptr<my_cpp_class<T>>)>
        wrapper = [=](std::shared_ptr<my_cpp_class<T>> sample) -> void
        
            callback(c_self, sample);
        ;
        return wrapper;
    

;

我用 cython 包裹的:

cdef extern from "wrapper.hpp":
    cdef cppclass function_wrapper[T]:

        ctypedef void (*cy_callback) (void*, shared_ptr[my_cpp_class[T]])

        @staticmethod
        function[void(shared_ptr[my_cpp_class[T]])] make_std_function(
            cy_callback, void*)

基本上,我的想法是我将 cython 类回调方法传递给包装器,它返回带有正确类型签名的 std::function 版本。返回的函数实际上是一个 lambda,它将 self 参数应用于回调。

现在我可以创建我的 cdef 成员方法并且它们被正确调用:

    cdef void member_callback(self, shared_ptr[my_cpp_class[specific_type]] sample) with gil:
        # do stuff with the sample ...
        self.python_cb(sample_related_stuff)

注册看起来像:

register(function_wrapper[specific_type].make_std_function(<cb_type>self.member_callback, <void*>self))

请注意其中的cb_type。包装器需要void*,因此我们需要对其进行强制转换以匹配。这是 typedef:

ctypedef void (*cb_type)(void*, shared_ptr[my_cpp_class[specific_type]])

希望这对在类似船上的人有用。感谢@DavidW 让我走上正轨。

【讨论】:

以上是关于如何在本机回调中使用 Cython cdef 类成员方法的主要内容,如果未能解决你的问题,请参考以下文章

如何在 cdef 中等待?

如何分析 cdef 函数?

Cython 中的这个声明是啥? cdef PyObject **工人。它是指向指针的指针吗?

在cdef类中混合使用cdef和常规python属性

『Python CoolBook』Cython

Cython:具有自定义参数类型的 std::function 回调