如何在本机回调中使用 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_fun
或 std::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&
更改为 int
)。对于 boost 版本,您不必这样做。
void register(std::function<void(int)> 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<T>
调用的,其中 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 类成员方法的主要内容,如果未能解决你的问题,请参考以下文章