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

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

2026-09-04 21:52:48作者:滕妙奇

本文基于 CPython 官方文档 Doc/c-api/memoryview.rst 展开,系统讲解 C 扩展中操作 memoryview 对象的全部 C-API:类型对象 PyMemoryView_Type、四个构造函数(PyMemoryView_FromObjectPyMemoryView_FromMemoryPyMemoryView_FromBufferPyMemoryView_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):类型(如 bytesbytearrayarray.array,以及第三方扩展类型)通过 getbufferproc 暴露底层 buffer 信息;
  • 消费方(consumer):通过 PyObject_GetBuffer()PyArg_ParseTupley*/w*/s* 格式码获取原始数据指针,用完必须调用 PyBuffer_Release()

memoryview 是消费方在 Python 层的具象化:PyMemoryView_FromObject() 内部请求 PyBUF_FULL_RO 级别的完整信息(见 Objects/memoryobject.c#L853-L857),因此 memoryview 总是拥有完整的 shape/strides/format 描述,这也是它能支持多维切片和 tolist() 等操作的底层原因。

PyMemoryView_Type 与 PyMemoryView_Check

PyMemoryView_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_FromObject

PyObject *PyMemoryView_FromObject(PyObject *obj);

从一个实现了缓冲区协议的对象创建 memoryview。如果 obj 支持可写 buffer 导出,得到的 memoryview 就是可读写(read/write)的;否则它可能是只读的,也可能是可写的——由导出方自行决定。

Objects/memoryobject.c#L853-L857 的实现看,该函数直接委托给内部函数 PyMemoryView_FromObjectAndFlags(v, PyBUF_FULL_RO),后者有两条路径:

  1. v 本身是 memoryview:不创建新的受管缓冲区,而是调用 mbuf_add_view() 注册到同一个 managed buffer 上(Objects/memoryobject.c#L800-L825)。这保证了对 memoryview 再套 memoryview 时,所有链式视图共享同一份 buffer 快照,避免了 PEP-3118 允许的“底层对象在视图导出期间发生变化”带来的不一致问题;
  2. 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_WRITE

PyObject *PyMemoryView_FromMemory(char *mem, Py_ssize_t size, int flags);

(Python 3.3 引入。)用裸内存块 mem 作为底层 buffer 创建 memoryview。flags 只能取两个值:

  • PyBUF_READ0x100):请求只读 buffer;
  • PyBUF_WRITE0x200):请求可写 buffer。

这两个宏定义在 Include/pybuffer.h#L137-L138。它们与 PyArg_ParseTupley*(只读)/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 buffer,obj 字段为 NULL——因为这块内存并不属于任何 Python 对象。注意其内部断言 flags == PyBUF_READ || flags == PyBUF_WRITE,传入其他标志是未定义行为。

由此可以得出两条实践要点:

  • PyBUF_WRITE 创建的可写 memoryview 会直接写穿到你提供的 mem 区域,生命周期完全由 C 侧负责,必须保证内存块在 memoryview 存活期间有效
  • 由于没有导出对象,后续 PyMemoryView_GET_BASE() 会返回 NULL,这正是文档对 GET_BASE 语义的说明来源之一。

构造函数之三:PyMemoryView_FromBuffer

PyObject *PyMemoryView_FromBuffer(const Py_buffer *view);

包装一个已经填好的 Py_buffer 结构来创建 memoryview。文档明确:对简单的字节 buffer,优先使用 PyMemoryView_FromMemory()FromBuffer 是处理多维、带 strides/格式信息等复杂情况的入口。

实现细节值得注意(Objects/memoryobject.c#L769-L794):

if (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->bufNULL 时抛 ValueError
  • 整个结构体被按值拷入 master buffer,且 obj 被强制置 NULL——源码注释指出传入的 info->obj借用引用PyBuffer_Release() 不应该对其减引用。这是调用方最容易踩的引用计数坑;
  • 由于这是唯一可以创建“信息不完整”的 master buffer 的入口,内部 init_shape_strides() 需要能够重建缺失的 shape/strides(例如只给了 ndimitemsizelen 的简易 buffer)。

Py_buffer 结构本身的定义见 Include/pybuffer.h#L20-L33,其布局与大小自 Python 3.11 起属于稳定 ABI 的一部分,不得随意变更——使用 Limited API 时这一点尤其关键。

构造函数之四:PyMemoryView_GetContiguous

PyObject *PyMemoryView_GetContiguous(PyObject *obj, int buffertype, char order);

从任意支持缓冲区协议的对象创建指向连续内存块的 memoryview,order 指定连续性方向:'C'(C 顺序)或 'F'(Fortran 顺序)。文档给出的核心行为:

  • 如果内存本身已经按目标方向连续,memoryview 直接指向原内存(零拷贝);
  • 否则做一次拷贝,返回的 memoryview 指向一个新建的 bytes 对象。

buffertypePyBUF_READPyBUF_WRITEObjects/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'A'(Any)时,'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() 创建,则返回 NULLmview 同样必须是 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;
}

它们直接读取 PyMemoryViewObjectview 字段。从 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 exporter's 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-L196CHECK_RELEASED 宏)——如果你在处理大对象析构周期中遇到这个报错,根源就在这里。

相关 flag 速查与延伸阅读

memoryview 文档只用到 PyBUF_READ/PyBUF_WRITE,但 Include/pybuffer.h#L104-L134 定义了完整的 flag 体系,写缓冲区协议代码时值得对照(维度上限 PyBUF_MAX_NDIM 为 64,见 Include/pybuffer.h#L105):

含义
PyBUF_SIMPLE 0 仅要连续字节串
PyBUF_WRITABLE 0x0001 要求可写
PyBUF_FORMAT 0x0004 要格式串
PyBUF_ND 0x0008 要 shape
PyBUF_STRIDES 0x0010 | PyBUF_ND 要 strides(隐含 ndim)
PyBUF_C_CONTIGUOUS / PyBUF_F_CONTIGUOUS 0x0020 / 0x0040(均含 STRIDES) 要求 C/F 连续
PyBUF_ANY_CONTIGUOUS 0x0080 | PyBUF_STRIDES 任一方向连续即可
PyBUF_INDIRECT 0x0100 | PyBUF_STRIDES 允许 suboffsets(PIL 风格)
PyBUF_CONTIG / PyBUF_CONTIG_RO PyBUF_ND | PyBUF_WRITABLE / PyBUF_ND 连续,可写/只读
PyBUF_RECORDS / PyBUF_RECORDS_RO STRIDES[ | WRITABLE] | FORMAT 完整记录语义
PyBUF_FULL / PyBUF_FULL_RO INDIRECT | [WRITABLE] | FORMAT 全部信息
PyBUF_READ / PyBUF_WRITE 0x100 / 0x200 memoryview 构造用的读/写标记

组合规则是“请求越详细,flag 越多位”:例如 PyBUF_RECORDS 已经隐含 strides 与 ndim,调用 PyObject_GetBuffer() 时按此组合即可。

延伸阅读与验证材料:

  • Doc/c-api/buffer.rst:缓冲区协议的完整 C-API 文档(Py_buffer 各字段语义、PyObject_GetBufferPyBuffer_FillInfo 等),是 memoryview 文档的姊妹篇,文中标记为 bufferobjects 的引用锚点即出自此文件;
  • Include/memoryobject.hPyMemoryView_CheckPyMemoryView_FromObject 的公共声明;
  • Include/pybuffer.hPy_buffer 结构、PyBuffer_* 工具函数族与全部 flag 定义;
  • Objects/memoryobject.c:managed buffer 与 memoryview 的完整实现,包括多维拷贝(copy_buffer())、C/F 连续 strides 初始化等;
  • Lib/test/test_buffer.py:缓冲区协议与 memoryview 行为的测试用例集,覆盖 PyMemoryView_FromMemoryPyMemoryView_FromBufferPyMemoryView_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 层反复复制数据。

登录后查看全文
热门项目推荐
相关项目推荐