使用Python Capsules在Cython和Pybind11之间传递C++对象

3
我有两个C++库,它们分别使用pybind11和cython框架,暴露Python API。我需要使用Python胶囊对象在它们之间传递对象(双向)。由于cython和pybind11使用Python胶囊对象的方式不同,这样做是否可行?
库A定义了一个类Foo,并使用pybind11将其暴露给Python。库B使用cython暴露其API。LibB拥有shared_ptr,它是LibB中一个类Bar的成员。
Bar将shared_ptr成员作为PyCapsule返回,我在Foo类的pybind11中捕获它。我从胶囊中解包shared_ptr,将其返回给Python,并且用户可以在Python中使用pybind11绑定的Foo操作此对象。
接下来,我需要将其放回到pybind11的胶囊中并返回给Bar。
Bar的Python API操作PyObject和PyCapsule,因为cython允许这样做。而pybind11和Foo的API不接受这些类型,我被迫使用pybind11::object和pybind11::capsule。
一切都运行良好,直到我尝试在需要PyCapsule*的Bar类的cython方法中使用pybind11::capsule时,pybind11::capsule中的shared_ptr被破坏并导致应用程序崩溃。
有人尝试过使这两个库相互通信吗?
库A -> 类Foo
namespace foo{
    class Foo {
    public:
        void foo() {...}
    }
}

libB -> 类 Bar

namespace bar {
    class Bar {
    public:
        PyObject* get_foo() {
            const char * capsule_name = "foo_in_capsule";
            return PyCapsule_New(&m_foo, capsule_name, nullptr);
        }

        static Bar fooToBar(PyObject * capsule) {
            void * foo_ptr = PyCapsule_GetPointer(capsule, "foo_in_capsule");
            auto foo  = static_cast<std::shared_ptr<foo::Foo>*>(foo_ptr);
            // here the shared_ptr is corrupted (garbage numbers returned for use_count() and get() )
            std::cout << "checking the capsule: " << foo->use_count() << " " << foo->get() << std::endl

            Bar b;
            b.m_foo = *foo; //this is what I would like to get
            return b;
        }

        std::shared_ptr<Foo> m_foo;
    };
}

Pybind11适用于Foo

void regclass_foo_Foo(py::module m)
{
    py::class_<foo::Foo, std::shared_ptr<foo::Foo>> foo(m, "Foo");
    foo.def("foo", &foo::Foo::foo);
    foo.def_static("from_capsule", [](py::object* capsule) {
        auto* pycapsule_ptr = capsule->ptr();
        auto* foo_ptr = reinterpret_cast<std::shared_ptr<foo::Foo>*>(PyCapsule_GetPointer(pycapsule_ptr, "foo_in_capsule"));
        return *foo_ptr;
    });
    foo.def_static("to_capsule", [](std::shared_ptr<foo::Foo>& foo_from_python) {
        auto pybind_capsule = py::capsule(&foo_from_python, "foo_in_capsule", nullptr);
        return pybind_capsule;
    });
}

为Bar使用Cython

cdef extern from "bar.hpp" namespace "bar":
    cdef cppclass Bar:
        object get_foo() except +

def foo_to_bar(capsule):
    b = C.fooToBar(capsule)
    return b

将所有东西整合在Python中

from bar import Bar, foo_to_bar
from foo import Foo

bar = Bar(... some arguments ...)
capsule1 = bar.get_foo()

foo_from_capsule = Foo.from_capsule(capsule1)

// this is the important part - need to operate on foo using its python api
print("checking if foo works", foo_from_capsule.foo())
// and use it to create another bar object with a (possibly) modified foo object
capsule2 = Foo.to_capsule(foo_from_capsule)

bar2 = foo_to_bar(capsule2)

我的猜测是PyCapsule不是正确的方法 - 它被设计用于保存指针而不是共享指针,因此我猜测您正在从相同的C指针初始化两个shared_ptr。在Cython中,您可以将一个类设置为public,这将提供一个包含底层C结构的.h文件。那似乎是我开始的地方... - DavidW
关于[mre] - 我认为您不一定需要包含其中的Python胶囊部分(因为我认为这可能会导致回答尝试修复胶囊而不是正确地执行),但是您至少需要展示一个最简Cython和Pybind封装。 - DavidW
感谢您的回答,我已经添加了一些代码片段来展示我所处的情况。只是提醒一下,我可以修改这两个库,因为它们都是由我的公司开发的。问题在于我必须对shared_ptr<Foo>进行操作,而不是直接对Foo对象/指针进行操作。这是由于libA的构建方式。请再次查看并让我知道是否有任何可以在这里完成的工作。我被static Bar fooToBar(PyObject * capsule)方法卡住了,因为我在运行时遇到了段错误。 - tomdol
1
我无法运行您的代码,但一个明显的问题是从m_foo获取了一个原始指针。相反,您可以使用new从中复制构造一个共享指针(并在胶囊被销毁时释放它)。现在指针可能会变得悬空(这可能就是发生的情况)。 - ead
1个回答

4
你的代码中有太多未完成的细节,以至于我无法测试你的PyCapsule版本。我的看法是问题出在共享指针的生命周期上 - 你的胶囊指向一个与Bar相关联的共享指针的生命周期。然而,这个胶囊可能会比它更长寿。你应该创建一个新的shared_ptr<Foo>*(使用new),将其指向胶囊,并定义一个析构函数(用于胶囊)来删除它。
我认为更好的替代方法大纲如下:
纯粹使用C ++类型编写你的类,所以get_foofoo_to_bar只采取/返回shared_ptr<Foo>
PyBar定义为一个适当的Cython类,而不是使用胶囊:
cdef public class PyBar [object PyBarStruct, type PyBarType]:
    cdef shared_ptr[Bar] ptr

cdef public PyBar PyBar_from_shared_ptr(shared_ptr[Bar] b):
    cdef PyBar x = PyBar()
    x.ptr = b
    return x

这将生成一个包含 PyBarStructPyBarType(您可能不需要后者)定义的头文件。我还定义了一个基本的模块级函数,用于从共享指针创建 PyBar(并将其公开,以便它也出现在标头中)。

然后使用 PyBind11 来定义自定义类型转换器从/到 shared_ptr<Bar>load 大致是这样的:

bool load(handle src, bool) {
        auto bar_mod = py::import("bar");
        auto bar_type = py::getattr(bar_mod,"Bar");
        if (!py::isinstance(src,bar_type)) {
            return false;
        }

        // now cast to my PyBarStruct
        auto ptr = reinterpret_cast<PyBarStruct*>(src.ptr());

        value  = ptr->ptr; // access the shared_ptr of the struct
    }

而将C++转换为Python的类型转换器可能会类似于:

 static handle cast(std::shared_ptr<Bar> src, return_value_policy /* policy */, handle /* parent */) {
     auto bar_mod = py::import("bar"); // See note...
     return PyBar_from_shared_ptr(src);
 }

我已确保在两个函数中都包含了py::import("bar"),因为在模块被任何地方导入之前使用Cython定义的函数是不安全的,而在转换器中导入模块确保了这一点。

这段代码未经测试,几乎肯定会有错误,但应该比PyCapsule更清晰。


3
确实是对象生命周期的问题。在 to_capsule 绑定中,我在堆上创建了一个 shared_ptr,将其放入胶囊中,并使用自定义析构函数将 PyObject* 强制转换为 shared_ptr<foo::Foo>* 并将其删除。效果很好,谢谢!顺便说一句,我试图在这里粘贴代码,但是在评论中无法添加代码块,只能在问题和答案中添加。 - tomdol

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接