拓冰建站拓冰建站
首页 / 资讯中心 / 正文

CPython MemoryView C API 详解:零拷贝缓冲区访问与 PyMemoryView_* 函数全集

CPython MemoryView C API 详解零拷贝缓冲区访问与 PyMemoryView_* 函数全集【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython本文基于 CPython 官方文档Doc/c-api/memoryview.rst展开系统讲解 C 扩展中操作 memoryview 对象的全部 C-API类型对象PyMemoryView_Type、四个构造函数PyMemoryView_FromObject、PyMemoryView_FromMemory、PyMemoryView_FromBuffer、PyMemoryView_GetContiguous、类型检查PyMemoryView_Check以及底层访问宏PyMemoryView_GET_BUFFER/PyMemoryView_GET_BASE。读完本文你将能在 C 扩展中安全地创建、检查和读取 memoryview并结合 Objects/memoryobject.c 的源码理解“受管缓冲区快照”机制——CPython 用来保证链式 memoryview 行为一致的核心设计。memoryview 在缓冲区协议中的位置memoryview对象将 C 层面的 buffer protocol 暴露为普通的 Python 对象使其可以像其他对象一样被传递、存储和索引。这一角色在缓冲区协议文档中被明确定义buffer 结构Py_buffer本身是简单的 C 结构体而非PyObject指针可以廉价地创建和复制当需要一个通用的 buffer 包装器时就可以创建 memoryview 对象。缓冲区协议有两个方向导出方producer类型如bytes、bytearray、array.array以及第三方扩展类型通过getbufferproc暴露底层 buffer 信息消费方consumer通过PyObject_GetBuffer()或PyArg_ParseTuple的y*/w*/s*格式码获取原始数据指针用完必须调用PyBuffer_Release()。memoryview是消费方在 Python 层的具象化PyMemoryView_FromObject()内部请求PyBUF_FULL_RO级别的完整信息见 Objects/memoryobject.c#L853-L857因此 memoryview 总是拥有完整的 shape/strides/format 描述这也是它能支持多维切片和tolist()等操作的底层原因。PyMemoryView_Type 与 PyMemoryView_CheckPyMemoryView_Type是代表 Python 层memoryview类型的PyTypeObject实例——它就是你在解释器中type(memoryview(b))得到的那个类型对象C 扩展可以拿它做类型注册、isinstance判断等。文档中同时给出检查函数int PyMemoryView_Check(PyObject *obj);返回真值当且仅当obj是 memoryview 对象。文档特别指出目前不允许创建memoryview的子类因此该检查总是成功的不会抛错。这一说法与当前仓库的宏定义完全吻合Include/memoryobject.h#L11 中它就是严格相等判断#define PyMemoryView_Check(op) Py_IS_TYPE((op), PyMemoryView_Type)即Py_TYPE(op) PyMemoryView_Type。正因为没有子类CPython 可以直接用“类型指针相等”代替更昂贵也更宽松的PyType_IsSubtype检查——这是一种性能与语义的折中任何伪造或子类化的对象都会被拒绝。在 C 扩展中典型的用法是static PyObject * demo_show(PyObject *module, PyObject *obj) { if (!PyMemoryView_Check(obj)) { PyErr_SetString(PyExc_TypeError, expected a memoryview); return NULL; } Py_buffer *view PyMemoryView_GET_BUFFER(obj); printf(buf%p len%zd ndim%d readonly%d\n, view-buf, view-len, view-ndim, view-readonly); Py_RETURN_NONE; }构造函数之一PyMemoryView_FromObjectPyObject *PyMemoryView_FromObject(PyObject *obj);从一个实现了缓冲区协议的对象创建 memoryview。如果obj支持可写 buffer 导出得到的 memoryview 就是可读写read/write的否则它可能是只读的也可能是可写的——由导出方自行决定。从 Objects/memoryobject.c#L853-L857 的实现看该函数直接委托给内部函数PyMemoryView_FromObjectAndFlags(v, PyBUF_FULL_RO)后者有两条路径v本身是 memoryview不创建新的受管缓冲区而是调用mbuf_add_view()注册到同一个managed buffer 上Objects/memoryobject.c#L800-L825。这保证了对 memoryview 再套 memoryview 时所有链式视图共享同一份 buffer 快照避免了 PEP-3118 允许的“底层对象在视图导出期间发生变化”带来的不一致问题v是其他支持缓冲区协议的对象通过_PyManagedBuffer_FromObject()调用PyObject_GetBuffer()生成一份 master 快照再挂上 memoryview。如果对象既不是 memoryview 也不支持缓冲区协议抛出TypeError: memoryview: a bytes-like object is required, not xxx——这正是你在 Python 层执行memoryview(42)时看到的错误Python 层构造函数最终就是走到这条 C 路径。构造函数之二PyMemoryView_FromMemory 与 PyBUF_READ/PyBUF_WRITEPyObject *PyMemoryView_FromMemory(char *mem, Py_ssize_t size, int flags);Python 3.3 引入。用裸内存块mem作为底层 buffer 创建 memoryview。flags只能取两个值PyBUF_READ0x100请求只读 bufferPyBUF_WRITE0x200请求可写 buffer。这两个宏定义在 Include/pybuffer.h#L137-L138。它们与PyArg_ParseTuple的y*只读/w*可写格式码语义对应。实现上Objects/memoryobject.c#L740-L762该函数readonly (flags PyBUF_WRITE) ? 0 : 1; (void)PyBuffer_FillInfo(mbuf-master, NULL, mem, size, readonly, PyBUF_FULL_RO); mv mbuf_add_view(mbuf, NULL);即通过PyBuffer_FillInfo()以“连续的 unsigned bytes格式串B”语义填充 master bufferobj字段为NULL——因为这块内存并不属于任何 Python 对象。注意其内部断言flags PyBUF_READ || flags PyBUF_WRITE传入其他标志是未定义行为。由此可以得出两条实践要点用PyBUF_WRITE创建的可写 memoryview 会直接写穿到你提供的mem区域生命周期完全由 C 侧负责必须保证内存块在 memoryview 存活期间有效由于没有导出对象后续PyMemoryView_GET_BASE()会返回NULL这正是文档对GET_BASE语义的说明来源之一。构造函数之三PyMemoryView_FromBufferPyObject *PyMemoryView_FromBuffer(const Py_buffer *view);包装一个已经填好的Py_buffer结构来创建 memoryview。文档明确对简单的字节 buffer优先使用PyMemoryView_FromMemory()FromBuffer是处理多维、带 strides/格式信息等复杂情况的入口。实现细节值得注意Objects/memoryobject.c#L769-L794if (info-buf NULL) { PyErr_SetString(PyExc_ValueError, PyMemoryView_FromBuffer(): info-buf must not be NULL); return NULL; } mbuf-master *info; mbuf-master.obj NULL; /* info-obj is a borrowed reference, do NOT decrement */info-buf为NULL时抛ValueError整个结构体被按值拷入 master buffer且obj被强制置NULL——源码注释指出传入的info-obj是借用引用PyBuffer_Release()不应该对其减引用。这是调用方最容易踩的引用计数坑由于这是唯一可以创建“信息不完整”的 master buffer 的入口内部init_shape_strides()需要能够重建缺失的 shape/strides例如只给了ndim、itemsize、len的简易 buffer。Py_buffer结构本身的定义见 Include/pybuffer.h#L20-L33其布局与大小自 Python 3.11 起属于稳定 ABI 的一部分不得随意变更——使用 Limited API 时这一点尤其关键。构造函数之四PyMemoryView_GetContiguousPyObject *PyMemoryView_GetContiguous(PyObject *obj, int buffertype, char order);从任意支持缓冲区协议的对象创建指向连续内存块的 memoryvieworder指定连续性方向CC 顺序或FFortran 顺序。文档给出的核心行为如果内存本身已经按目标方向连续memoryview直接指向原内存零拷贝否则做一次拷贝返回的 memoryview 指向一个新建的bytes对象。buffertype取PyBUF_READ或PyBUF_WRITE。Objects/memoryobject.c#L965-L1000 的实现精确对应了文档描述并补充了两种错误情形if (buffertype PyBUF_WRITE view-readonly) PyErr_SetString(PyExc_BufferError, underlying buffer is not writable); if (PyBuffer_IsContiguous(view, order)) return (PyObject *)mv; /* 已连续原样返回零拷贝 */ if (buffertype PyBUF_WRITE) PyErr_SetString(PyExc_BufferError, writable contiguous buffer requested for a non-contiguous object.); ret memory_from_contiguous_copy(view, order); /* 只读且非连续拷贝到 bytes */也就是说请求可写连续 buffer 但源既不可写又非连续时会抛BufferError而不是静默拷贝——因为拷贝出来的是 bytes 副本写它并不能写回源对象语义上必须显式失败。order传AAny时C/A会按 C 序重排F按 Fortran 序重排见memory_from_contiguous_copy()中init_strides_from_shape()/init_fortran_strides_from_shape()的选择。这个函数的典型用途是扩展代码需要一个“按某方向连续”的内存块传给只接受连续内存的第三方 C 库同时不想自己实现拷贝逻辑。底层访问宏PyMemoryView_GET_BUFFER 与 PyMemoryView_GET_BASE拿到 memoryview 后两个宏提供直接访问Py_buffer *PyMemoryView_GET_BUFFER(PyObject *mview);返回指向该 memoryview私有的、导出方 buffer 副本的指针。文档用加粗强调了约束mview必须是 memoryview 实例该宏不检查类型检查是你自己的责任否则可能崩溃。PyObject *PyMemoryView_GET_BASE(PyObject *mview);返回 memoryview 所基于的导出对象若该 memoryview 由PyMemoryView_FromMemory()或PyMemoryView_FromBuffer()创建则返回NULL。mview同样必须是 memoryview 实例。在当前仓库中这两个“宏”实际上是 Include/cpython/memoryobject.h#L41-L50 中的static inline函数外加兼容宏static inline Py_buffer* PyMemoryView_GET_BUFFER(PyObject *op) { return (_PyMemoryView_CAST(op)-view); } static inline PyObject* PyMemoryView_GET_BASE(PyObject *op) { return _PyMemoryView_CAST(op)-view.obj; }它们直接读取PyMemoryViewObject的view字段。从 Include/cpython/memoryobject.h#L27-L36 的结构体定义可以看清 memoryview 的完整内部布局typedef struct { PyObject_VAR_HEAD _PyManagedBufferObject *mbuf; /* managed buffer */ Py_hash_t hash; /* hash value for read-only views */ int flags; /* state flags */ Py_ssize_t exports; /* number of buffer re-exports */ Py_buffer view; /* private copy of the exporters view */ PyObject *weakreflist; Py_ssize_t ob_array[1]; /* shape, strides, suboffsets */ } PyMemoryViewObject;头文件开头有一段重要警告这些结构体之所以在此声明只是为了让宏能工作不应被视为公共接口不要直接访问字段应使用宏和函数。hash字段也顺带解释了 Python 层“只读 memoryview 可哈希、可作字典键”的特性来源。源码纵深受管缓冲区managed buffer机制文档只描述了各函数的表面行为而 Objects/memoryobject.c 开头的设计注释解释了更深层的机制managed buffer_PyManagedBufferObject定义于 Include/cpython/memoryobject.h#L11-L16。PEP-3118 允许底层导出对象在视图导出期间发生变化例如bytearray被原地修改后重新导出 buffer。如果链式 memoryview 每次都把 buffer 请求重定向到原始 base 对象可能得到意外结果。CPython 的解法是构造函数_PyManagedBuffer_FromObject()在第一次导出时生成唯一的 buffer 快照master buffer之后所有链式 memoryview包括对 memoryview 再取 memoryview都注册到这同一个 managed buffer 上共享这份快照master 中的 shape/strides/suboffsets/format 对所有消费者只读而每个 memoryview 持有自己私有的view副本其中的 shape/strides/suboffsets 归该 memoryview 所有、可写用于切片时重建布局。引用计数规则同样在注释中写明Py_buffer.obj若非NULL必须指向导出 base 对象且持有新引用PyBuffer_Release()负责对其减引用并置NULL因此各类型的releasebufferproc不得重复对view.obj减引用——这是写 C 扩展 buffer 导出方时最经典的 double-free 来源。managed buffer 还维护exports计数与释放标记_Py_MANAGED_BUFFER_RELEASED。当底层关系被清理时对已释放的 memoryview 再操作会触发ValueError: operation forbidden on released memoryview见 Objects/memoryobject.c#L180-L196 的CHECK_RELEASED宏——如果你在处理大对象析构周期中遇到这个报错根源就在这里。相关 flag 速查与延伸阅读memoryview文档只用到PyBUF_READ/PyBUF_WRITE但 Include/pybuffer.h#L104-L134 定义了完整的 flag 体系写缓冲区协议代码时值得对照维度上限PyBUF_MAX_NDIM为 64见 Include/pybuffer.h#L105宏值含义PyBUF_SIMPLE0仅要连续字节串PyBUF_WRITABLE0x0001要求可写PyBUF_FORMAT0x0004要格式串PyBUF_ND0x0008要 shapePyBUF_STRIDES0x0010 \| PyBUF_ND要 strides隐含 ndimPyBUF_C_CONTIGUOUS/PyBUF_F_CONTIGUOUS0x0020/0x0040均含 STRIDES要求 C/F 连续PyBUF_ANY_CONTIGUOUS0x0080 \| PyBUF_STRIDES任一方向连续即可PyBUF_INDIRECT0x0100 \| PyBUF_STRIDES允许 suboffsetsPIL 风格PyBUF_CONTIG/PyBUF_CONTIG_ROPyBUF_ND \| PyBUF_WRITABLE/PyBUF_ND连续可写/只读PyBUF_RECORDS/PyBUF_RECORDS_ROSTRIDES[ \| WRITABLE] \| FORMAT完整记录语义PyBUF_FULL/PyBUF_FULL_ROINDIRECT \| [WRITABLE] \| FORMAT全部信息PyBUF_READ/PyBUF_WRITE0x100/0x200memoryview 构造用的读/写标记组合规则是“请求越详细flag 越多位”例如PyBUF_RECORDS已经隐含 strides 与 ndim调用PyObject_GetBuffer()时按此组合即可。延伸阅读与验证材料Doc/c-api/buffer.rst缓冲区协议的完整 C-API 文档Py_buffer各字段语义、PyObject_GetBuffer、PyBuffer_FillInfo等是 memoryview 文档的姊妹篇文中标记为bufferobjects的引用锚点即出自此文件Include/memoryobject.hPyMemoryView_Check与PyMemoryView_FromObject的公共声明Include/pybuffer.hPy_buffer结构、PyBuffer_*工具函数族与全部 flag 定义Objects/memoryobject.cmanaged buffer 与 memoryview 的完整实现包括多维拷贝copy_buffer()、C/F 连续 strides 初始化等Lib/test/test_buffer.py缓冲区协议与 memoryview 行为的测试用例集覆盖PyMemoryView_FromMemory、PyMemoryView_FromBuffer、PyMemoryView_GetContiguous等的边界行为Lib/test/test_stable_abi_ctypes.py通过 ctypes 验证上述函数在稳定 ABI 下的可用性。小结memoryview 的 C-API 表面上只有七个符号职责却划分得很清晰PyMemoryView_Type提供类型身份PyMemoryView_Check做严格类型判定FromObject/FromMemory/FromBuffer分别对应“包装 Python 对象”“包装裸内存”“包装现成Py_buffer”三种来源GetContiguous解决“必须要连续内存”的常见诉求并以零拷贝为默认路径GET_BUFFER/GET_BASE则是在确认类型后提取底层数据的最后一步。理解了 managed buffer 的快照语义与obj字段的引用计数规则你就能在扩展模块中安全地跨语言共享大块内存而不必在 Python 层反复复制数据。【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门