如何在pybind11中创建对象与聚合返回值成员之间的保持活动关系?
我正在使用pybind11来为一个C++库生成Python的接口,并且我用pybind11::keep_alive来管理被其他对象引用的C++对象的生命周期。当引用者是直接返回值时,这个方法工作得很好,但当引用者是一个聚合返回值的一部分时,我就遇到了问题。
用一个简化但完整的例子来说明可能更容易:
#include <iostream>
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
namespace py = pybind11;
class Container {
public:
int some_value;
Container() {
std::cerr << "Container constructed\n";
some_value = 42;
}
~Container() {
some_value = -1;
std::cerr << "Container destructed\n";
}
// Container is not copyable.
Container(const Container &) = delete;
Container &operator=(const Container &) = delete;
// Iterator references the contents of Container.
struct Iterator {
Container *owner;
int value() const { return owner->some_value; }
};
Iterator iterator() { return Iterator{this}; }
std::pair<Iterator, bool> iterator_pair() { return {Iterator{this}, true}; }
};
PYBIND11_MODULE(example, module) {
py::class_<Container> owner(module, "Container");
py::class_<Container::Iterator>(owner, "Iterator")
.def_property_readonly("value", &Container::Iterator::value);
owner
.def(py::init())
.def("iterator", &Container::iterator, py::keep_alive<0, 1>())
.def("iterator_pair", &Container::iterator_pair, py::keep_alive<0, 1>());
}
在上面的例子中,我有一个容器对象,它返回一个迭代器,这个迭代器引用了容器的内容,所以这个迭代器必须保持容器的存活。对于iterator
方法,这个逻辑完全没问题,因为py::keep_alive<0, 1>
让迭代器对象保持对创建它的容器对象的引用。
但是,这个方法在iterator_pair
方法中就不奏效了。例如,像这样运行:
import example
def Test1():
it = example.Container().iterator()
print(it.value) # prints 42
def Test2():
it, b = example.Container().iterator_pair()
assert b == True
print(it.value)
Test1() # OK
Test2() # Fails!
会失败,并输出以下内容:
Owner constructed
42
Owner destructed
Owner constructed
Owner destructed
Traceback (most recent call last):
File "test.py", line 13, in <module>
Test2() # Fails!
^^^^^^^
File "test.py", line 8, in Test2
it, b = example.Container().iterator_pair()
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
TypeError: cannot create weak reference to 'tuple' object
而且我不能去掉py::keep_alive(0, 1)
,因为那样会导致容器被过早销毁:
Owner constructed
42
Owner destructed
Owner constructed
Owner destructed
269054512
(注意,269054512
只是内存中的随机垃圾,因为当迭代器引用它的值时,拥有者已经被释放了。)
在这种情况下,推荐的处理方式是什么呢?
我也遇到过类似的问题,比如返回必须保持其父对象存活的std::vector对象,所以我认为更一般的问题是,如何返回包含必须保持其父对象存活的子对象的聚合对象。
我能想到的一个解决方法是,把容器包装在一个std::shared_ptr<>中,并在迭代器中也使用它。这样,无论在Python中是否仍然被引用,C++容器对象都能保持存活。不过,我并不太想这样做,因为这需要在C++端进行大量的重构,而且感觉像是在重复Python已经具备的引用计数支持。
1 个回答
1
你可以自己写一个版本的 keep_alive
:
struct keep_alive_container {};
namespace pybind11::detail {
template <>
struct process_attribute<keep_alive_container>
: public process_attribute_default<keep_alive_container> {
static void precall(function_call&) {}
static void postcall(function_call& call, handle ret) {
keep_alive_impl(ret.attr("__getitem__")(0), call.args[0]);
}
};
}
// later on
.def("iterator_pair", &Container::iterator_pair, keep_alive_container())
这需要调用一个叫 keep_alive_impl
的函数,它是一个 私有的 pybind11
函数,所以可能不太推荐这样做。