【问题标题】:Pass a closure from Cython to C++将一个闭包从 Cython 传递给 C++
【发布时间】:2016-08-19 16:44:15
【问题描述】:

我有一个接受回调的 C++ 函数,如下所示:

void func(std::function<void(A, B)> callback) { ... }

我想通过给它一个闭包来从 Cython 调用这个函数,也就是说,如果我从 C++ 调用它,我会用 lambda 做的事情。如果这是一个 C 函数,它会有一些额外的 void* 参数:

typedef void(*callback_t)(int, int, void*);

void func(callback_t callback, void *user_data) {
    callback(1, 2, user_data);
}

然后我会将PyObject* 传递为user_data(还有更详细的example here)。

有没有办法以 C++ 的方式做更多的事情,而不必诉诸明确的user_data

【问题讨论】:

  • 谷歌搜索cython std function 给出了一个似乎相关的example(不是库)。
  • @bereal 请让您的问题自成一体(关于示例),这对未来的研究不是很有用。
  • @πάνταῥεῖ 很公平,一旦我回到键盘就会更新。

标签: python c++ cython


【解决方案1】:

我相信您的目标是将可调用的 Python 对象传递给接受 std::function 的对象。您需要创建一些 C++ 代码来实现它,但它相当简单。

首先尽可能简单地定义“accepts_std_function.hpp”以提供说明性示例:

#include <functional>
#include <string>

inline void call_some_std_func(std::function<void(int,const std::string&)> callback) {
    callback(5,std::string("hello"));
}

然后诀窍是创建一个包含PyObject* 并定义operator() 的包装类。定义operator() 允许将其转换为std::function。大部分课程只是引用计数。 “py_obj_wrapper.hpp”:

#include <Python.h>
#include <string>
#include "call_obj.h" // cython helper file

class PyObjWrapper {
public:
    // constructors and destructors mostly do reference counting
    PyObjWrapper(PyObject* o): held(o) {
        Py_XINCREF(o);
    }

    PyObjWrapper(const PyObjWrapper& rhs): PyObjWrapper(rhs.held) { // C++11 onwards only
    }

    PyObjWrapper(PyObjWrapper&& rhs): held(rhs.held) {
        rhs.held = 0;
    }

    // need no-arg constructor to stack allocate in Cython
    PyObjWrapper(): PyObjWrapper(nullptr) {
    }

    ~PyObjWrapper() {
        Py_XDECREF(held);
    }

    PyObjWrapper& operator=(const PyObjWrapper& rhs) {
        PyObjWrapper tmp = rhs;
        return (*this = std::move(tmp));
    }

    PyObjWrapper& operator=(PyObjWrapper&& rhs) {
        held = rhs.held;
        rhs.held = 0;
        return *this;
    }

    void operator()(int a, const std::string& b) {
        if (held) { // nullptr check 
            call_obj(held,a,b); // note, no way of checking for errors until you return to Python
        }
    }

private:
    PyObject* held;
};

此文件使用一个非常短的 Cython 文件来执行从 C++ 类型到 Python 类型的转换。 “call_obj.pyx”:

from libcpp.string cimport string

cdef public void call_obj(obj, int a, const string& b):
    obj(a,b)

然后您只需要创建包装这些类型的 Cython 代码。编译这个模块并调用test_func 来运行它。 ("simple_version.pyx":)

cdef extern from "py_obj_wrapper.hpp":
    cdef cppclass PyObjWrapper:
        PyObjWrapper()
        PyObjWrapper(object) # define a constructor that takes a Python object
             # note - doesn't match c++ signature - that's fine!

cdef extern from "accepts_std_func.hpp":
    void call_some_std_func(PyObjWrapper) except +
            # here I lie about the signature
            # because C++ does an automatic conversion to function pointer
            # for classes that define operator(), but Cython doesn't know that


def example(a,b):
    print(a,b)

def test_call():
    cdef PyObjWrapper f = PyObjWrapper(example)

    call_some_std_func(f)

上面的版本可以工作,但有一些限制,如果你想用不同的std::function 特化来做这件事,你需要重写一些(并且从 C++ 到 Python 类型的转换自然不适合模板实现)。解决这个问题的一种简单方法是使用 Boost Python 库 object 类,它有一个模板化的 operator()。这是以引入额外的库依赖为代价的。

首先定义头文件“boost_wrapper.hpp”以简化the conversion from PyObject* to boost::python::object

#include <boost/python/object.hpp>

inline boost::python::object get_as_bpo(PyObject* o) {
    return boost::python::object(boost::python::handle<>(boost::python::borrowed(o)));
}

然后你只需要 Cython 代码来包装这个类(“boost_version.pyx”)。再次致电test_func

cdef extern from "boost_wrapper.hpp":
    cdef cppclass bpo "boost::python::object":
        # manually set name (it'll conflict with "object" otherwise
        bpo()

    bpo get_as_bpo(object)


cdef extern from "accepts_std_func.hpp":
    void call_some_std_func(bpo) except + # again, lie about signature

def example(a,b):
    print(a,b)

def test_call():
    cdef bpo f = get_as_bpo(example)

    call_some_std_func(f)

一个“setup.py”

from distutils.core import setup, Extension
from Cython.Build import cythonize

extensions = [
    Extension(
           "simple_version",                       # the extension name
           sources=["simple_version.pyx", "call_obj.pyx" ],
           language="c++",                        # generate and compile C++ code
      ),
    Extension(
           "boost_version",                       # the extension name
           sources=["boost_version.pyx"],
           libraries=['boost_python'],
           language="c++",                        # generate and compile C++ code
      )
    ]

setup(ext_modules = cythonize(extensions))

(最后一个选项是使用ctypes 从 Python 可调用对象生成 C 函数指针。请参阅Using function pointers to methods of classes without the gil(答案的下半部分)和http://osdir.com/ml/python-cython-devel/2009-10/msg00202.html。我不会对此进行详细介绍在这里。)

【讨论】:

  • 太好了,特别感谢 operator() 模板的提升。实际上,我已经按照@user2357112 所指的方式做了一些事情,但是必须为每个新的回调签名重写包装器真的很烦人。该示例还缺少引用计数部分。非常感谢!
  • Boost 显然是要走的路(几乎不费吹灰之力!)。我想展示你如何在没有 boost 的情况下做到这一点,但我实际上不会使用该代码,除非 boost 真的不是一个选项。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-09-13
  • 1970-01-01
  • 2018-12-16
相关资源
最近更新 更多