PyBind11暴露C++20协程为Python迭代器调用iter崩溃排查
问题原因
调用iter()直接崩溃的核心原因有3个:
- pybind11迭代器适配不支持异构哨兵的不对称比较:你用
std::default_sentinel_t作为生成器end迭代器,只实现了Iterator == default_sentinel_t的成员比较,没有实现反向的default_sentinel_t == Iterator,pybind11内部做迭代器合法性校验时会触发非法内存访问,生成野指针直接段错误,这是调用iter()立刻崩溃的直接诱因。 - 自定义迭代器不符合C++ LegacyInputIterator规范:pybind11的
make_iterator要求包装的迭代器满足传统输入迭代器要求:迭代器拷贝后和原实例完全等价、独立有效。但你的Iterator内部只存了协程句柄的裸指针,所有拷贝的迭代器都共享同一个协程状态,任意一个迭代器执行++操作resume协程,其他所有迭代器持有的帧引用会立刻失效,触发未定义行为。 - 多余的opaque类型声明阻塞数据转换:你加了
PYBIND11_MAKE_OPAQUE(std::vector<unsigned char>),会让pybind11完全不识别字节vector的内容,就算迭代器不崩,Python侧也拿不到帧数据,更转不成numpy数组。
修复方案
不要硬套pybind11::make_iterator,C20协程生成器本身是一次性遍历的,和Python迭代器协议完全匹配,直接实现__iter__和__next__即可,绕开C传统迭代器适配的所有坑。
第一步:修改Generator类,加简单的拉取帧方法
在你的Generator类里加启动标记位,防止重复启动协程:
// 私有成员加启动标记 private: Handle m_coroutine; bool m_started = false; // 新增:标记协程是否已经启动 // 公共成员加拉取帧的方法 public: bool pull_next(T& out) { if (!m_coroutine) return false; if (!m_started) { m_coroutine.resume(); m_started = true; } else { m_coroutine.resume(); } if (m_coroutine.done()) return false; out = *m_coroutine.promise().m_value; return true; }
- 把原来
begin()里的首次resume逻辑移到这里,避免重复调用启动协程。
第二步:删除多余的opaque声明
把PYBIND11_MAKE_OPAQUE(std::vector<unsigned char>);这行直接删掉,你不需要把C++ vector暴露给Python,直接转numpy数组即可。
第三步:重写pybind11绑定逻辑
替换原来的__iter__实现,直接实现Python迭代器协议,顺便完成numpy数组转换:
PYBIND11_MODULE (test, m) { m.doc () = "Camera utils"; pybind11::class_<Generator<stFrame>>(m, "FrameReader") .def(pybind11::init([](std::string filename, std::array<size_t, 2> resolution) { return frame(filename, resolution); })) .def("__iter__", [](Generator<stFrame>& self) -> Generator<stFrame>& { // 迭代器返回自身即可,pull_next内部已经做了启动防重 return self; }, py::keep_alive<0, 1>()) // 保证迭代器存活时Generator不被销毁 .def("__next__", [](Generator<stFrame>& self) { stFrame frame; if (!self.pull_next(frame)) { throw py::stop_iteration(); // 遍历结束抛Python标准异常 } // 直接构造numpy数组,拷贝帧数据避免野指针 // 单通道灰度图用shape (height, width),RGB就加第三个通道维度 py::array_t<unsigned char> np_frame( {942, 1824}, // 传入的分辨率[1824,942]是width*height,numpy是行优先,高在前 {1824 * sizeof(unsigned char), sizeof(unsigned char)} ); std::memcpy(np_frame.mutable_data(), frame.data.data(), frame.data.size()); // 返回帧序号和numpy数组,不需要暴露stFrame类型给Python return py::make_tuple(frame.frameIndex, np_frame); }); // 不需要再绑定stFrame和vector,Python侧直接拿到(index, ndarray)元组即可 }
额外说明
- 这里直接拷贝帧数据到numpy数组是最稳妥的方案,协程resume后上一帧的buffer会被释放,numpy自己持有内存不会出现野指针,性能开销也极小。
- 如果需要多通道(比如BGR/RGB),只需要修改numpy的shape参数为
{height, width, 3},stride对应调整即可。 - 协程内部如果抛出异常,会在
pull_next调用resume()的时候直接抛出,pybind11会自动转成对应的Python异常,不需要额外处理。
内容的提问来源于stack exchange,提问作者jafido
相关产品推荐
相关产品推荐

