我正在寻求关于如何处理C++和NumPy/Python之间复杂的生命周期问题的建议.很抱歉给你留了一大堆文字,但我想提供尽可能多的内容.
我开发了cvnp,这是一个库,它在cv::Mat
到py::array
个对象之间提供绑定之间的强制转换,因此在使用pybind11时,内存在两者之间共享.
它最初是基于a SO answer×
Dan Mašek
的.一切都很顺利,该库被用于几个项目,包括robotpy,这是第一届机器人大赛的Python库.
然而,an issue是由用户引发的,它处理链接的cv::Mat
和py::array
对象的生存期.
- 在方向
cv::Mat
-py::array
上,一切都很好,因为mat_to_nparray将创建一个py::array
,它通过"capsule"(一个python句柄)保持对链接的cv::Mat的引用. - 然而,在方向
py::array
->;cv::Mat
,nparray_to_mat中,cv::mat将访问py::数组的数据,而不引用该数组(因此,不能保证py::数组的生存期与cv::mat相同)
请参见mat_to_nparray:
py::capsule make_capsule_mat(const cv::Mat& m)
{
return py::capsule(new cv::Mat(m)
, [](void *v) { delete reinterpret_cast<cv::Mat*>(v); }
);
}
pybind11::array mat_to_nparray(const cv::Mat& m)
{
return pybind11::array(detail::determine_np_dtype(m.depth())
, detail::determine_shape(m)
, detail::determine_strides(m)
, m.data
, detail::make_capsule_mat(m)
);
}
和nparray_to_mat:
cv::Mat nparray_to_mat(pybind11::array& a)
{
...
cv::Mat m(size, type, is_not_empty ? a.mutable_data(0) : nullptr);
return m;
}
到目前为止,这个方法运行得很好,直到一位用户写道:
- 返回作为参数传递的相同cv::mat的绑定c++函数
m.def("test", [](cv::Mat mat) { return mat; });
- 使用此函数的一些Python代码
img = np.zeros(shape=(480, 640, 3), dtype=np.uint8)
img = test(img)
在这种情况下,可能会发生分段错误,因为py::array
对象在cv::Mat
对象之前被销毁,并且cv::Mat
对象试图访问py::array
对象的数据.然而,分段故障不是系统性的,并且取决于OS+PYTHON版本.
我使用ASAN通过this commit在CI中复制了它. 重现代码相当简单:
void test_lifetime()
{
// We need to create a big array to trigger a segfault
auto create_example_array = []() -> pybind11::array
{
constexpr int rows = 1000, cols = 1000;
std::vector<pybind11::ssize_t> a_shape{rows, cols};
std::vector<pybind11::ssize_t> a_strides{};
pybind11::dtype a_dtype = pybind11::dtype(pybind11::format_descriptor<int32_t>::format());
pybind11::array a(a_dtype, a_shape, a_strides);
// Set initial values
for(int i=0; i<rows; ++i)
for(int j=0; j<cols; ++j)
*((int32_t *)a.mutable_data(j, i)) = j * rows + i;
printf("Created array data address =%p\n%s\n",
a.data(),
py::str(a).cast<std::string>().c_str());
return a;
};
// Let's reimplement the bound version of the test function via pybind11:
auto test_bound = [](pybind11::array& a) {
cv::Mat m = cvnp::nparray_to_mat(a);
return cvnp::mat_to_nparray(m);
};
// Now let's reimplement the failing python code in C++
// img = np.zeros(shape=(480, 640, 3), dtype=np.uint8)
// img = test(img)
auto img = create_example_array();
img = test_bound(img);
// Let's try to change the content of the img array
*((int32_t *)img.mutable_data(0, 0)) = 14; // This triggers an error that ASAN catches
printf("img data address =%p\n%s\n",
img.data(),
py::str(img).cast<std::string>().c_str());
}
我在寻求如何处理这个问题的建议.我看到了几个 Select :
理想的解决方案是
- 在
nparray_to_mat
内部构造cv::mat时调用pybind11::array.inc_ref()
- 确保在销毁此特定实例时调用
pybind11::array.dec_ref()
. 然而,我不知道该怎么做.
注意:我知道cv::mat可以使用自定义分配器,但在这里没用,因为cv::mat本身不会分配内存,但会使用py::数组对象的内存.
感谢您阅读到目前为止,并提前感谢您的任何建议!