首页
/ CPython C API 详解:PickleBuffer 对象与 pickle 带外数据传输(Out-of-Band)机制

CPython C API 详解:PickleBuffer 对象与 pickle 带外数据传输(Out-of-Band)机制

2026-09-06 22:28:08作者:秋阔奎Evelyn

本文基于 CPython 官方 C API 文档 picklebuffer.rst 展开,完整讲解 PyPickleBuffer_TypePyPickleBuffer_CheckPyPickleBuffer_FromObjectPyPickleBuffer_GetBufferPyPickleBuffer_Release 五个 API 的语义与错误约定,并结合 Objects/picklebufobject.c 的类型实现与 Modules/_pickle.csave_picklebuffer 的序列化逻辑,说明 PickleBuffer 如何在 pickle 协议 5 下实现"带外(out-of-band)缓冲数据传输",帮助你在 C 扩展中直接构造和操作 PickleBuffer 对象。

PickleBuffer 是什么:为 pickle 带外传输设计的缓冲包装器

CPython 3.8 起,pickle 协议 5 引入了带外数据传输(out-of-band data transfer):序列化时不再把大块字节数据强行写进 pickle 流内部,而是只写入一条"缓冲区引用"(NEXT_BUFFER 操作码),真正的数据交给调用方通过 buffer_callback 另行传递。pickle.PickleBuffer 就是这条链路的核心角色——它包装任意实现了 buffer 协议(bytes、bytearray、memoryview、numpy.ndarray 等)的对象,在 pickle 流中充当"这段数据在别处"的占位符。

C API 文档将其定义为:

A pickle.PickleBuffer object wraps a buffer-providing object for out-of-band data transfer with the pickle module.(pickle.PickleBuffer 对象包装一个提供 buffer 的对象,用于与 pickle 模块进行带外数据传输。)

对 C 扩展开发者而言,PickleBuffer 的价值在于:当你自己实现了 buffer 协议、却希望该对象能参与 pickle 的带外传输时,不必在 Python 层转手,直接用 C API 构造一个 PickleBuffer 即可。文档给出的完整 API 面如下:

  • PyTypeObject PyPickleBuffer_Type —— pickle buffer 类型的 PyTypeObject,与 Python 层 pickle.PickleBuffer 是同一个对象;
  • int PyPickleBuffer_Check(PyObject *op) —— 判断 op 是否为 pickle buffer 实例,永远成功(返回 0/1);
  • PyObject *PyPickleBuffer_FromObject(PyObject *obj) —— 从任意对象创建 pickle buffer,等价于 Python 层 pickle.PickleBuffer(obj)obj 不支持 buffer 协议时失败并设置异常;
  • const Py_buffer *PyPickleBuffer_GetBuffer(PyObject *picklebuf) —— 获取被包装的底层 Py_buffer 视图指针;
  • int PyPickleBuffer_Release(PyObject *picklebuf) —— 释放底层 buffer,等价于 Python 层 release()

需要注意一个细节:虽然 pickle.PickleBuffer 由 pickle 模块导出(Lib/pickle.pyfrom _pickle import PickleBuffer),但文档同时声明 PyPickleBuffer_Type "与 Python 层的 :class:pickle.PickleBuffer 是同一对象"。从 Include/cpython/picklebufobject.h 顶部的注释可以印证这一点:

/* PickleBuffer object. This is built-in for ease of use from third-party
 * C extensions.
 */

即它被注册为 built-in 类型,第三方 C 扩展无需 PyImport_ImportModule("pickle") 取到模块再找类,直接链 PyPickleBuffer_Type 即可。

类型结构体与 C API 声明

PickleBuffer 类型的声明位于 Include/cpython/picklebufobject.h,全部 API 都包在 #ifndef Py_LIMITED_API 保护块内,属于 CPython 内部 API(CPython API)而非稳定有限 API:

#ifndef Py_LIMITED_API

PyAPI_DATA(PyTypeObject) PyPickleBuffer_Type;

#define PyPickleBuffer_Check(op) Py_IS_TYPE((op), &PyPickleBuffer_Type)

/* Create a PickleBuffer redirecting to the given buffer-enabled object */
PyAPI_FUNC(PyObject *) PyPickleBuffer_FromObject(PyObject *);
/* Get the PickleBuffer's underlying view to the original object
 * (NULL if released)
 */
PyAPI_FUNC(const Py_buffer *) PyPickleBuffer_GetBuffer(PyObject *);
/* Release the PickleBuffer.  Returns 0 on success, -1 on error. */
PyAPI_FUNC(int) PyPickleBuffer_Release(PyObject *);

#endif /* !Py_LIMITED_API */

头文件注释本身就点出了每个函数的关键契约:GetBuffer 返回的是"指向原始对象的底层视图",已 release 时视图为 NULL 语义(此时函数实际会报错,见下节);Release 成功返回 0、失败返回 -1。

头文件用 Py_IS_TYPE 宏实现 PyPickleBuffer_Check,这与文档中 "Return true if op is a pickle buffer instance. This function always succeeds." 严格对应——它是精确类型检查(Py_IS_TYPE 比较 Py_TYPE(op)&PyPickleBuffer_Type),不使用 PyIsInstance 式的继承查找,因此不会引发异常,恒返回 0 或 1。

PyPickleBuffer_FromObject:构造路径与 PyBUF_FULL_RO 标志

PyPickleBuffer_FromObject 的实现在 Objects/picklebufobject.c

PyObject *
PyPickleBuffer_FromObject(PyObject *base)
{
    PyTypeObject *type = &PyPickleBuffer_Type;
    PyPickleBufferObject *self;

    self = (PyPickleBufferObject *) type->tp_alloc(type, 0);
    if (self == NULL) {
        return NULL;
    }
    self->view.obj = NULL;
    self->weakreflist = NULL;
    if (PyObject_GetBuffer(base, &self->view, PyBUF_FULL_RO) < 0) {
        Py_DECREF(self);
        return NULL;
    }
    return (PyObject *) self;
}

这段实现揭示了三个重要事实:

  1. buffer 请求标志是 PyBUF_FULL_ROPyBUF_FULL_RO 展开为 PyBUF_ND | PyBUF_FORMAT | PyBUF_STRIDES | PyBUF_INDIRECT | PyBUF_READONLY,即 PickleBuffer 要求底层对象暴露完整 buffer 视图(含维度、格式、步长),但不要求可写。文档说 "This function will fail if obj doesn't support the buffer protocol",具体表现为 PyObject_GetBuffer 失败——对不支持 buffer 协议的对象会设置 TypeError,对支持但无法提供完整视图(例如只能提供部分格式)的对象会设置 BufferError,随后 PyPickleBuffer_FromObject 设置异常并返回 NULL,与文档承诺的 "On failure, set an exception and return NULL" 一致。

  2. 引用语义:PickleBuffer 持有底层对象一个强引用self->view.objPyObject_GetBuffer 置为源对象并持有引用计数,且对象结构体直接内嵌该视图:

typedef struct {
    PyObject_HEAD
    /* The view exported by the original object */
    Py_buffer view;
    PyObject *weakreflist;
} PyPickleBufferObject;

配合 picklebuf_traverse 中对 self->view.objPy_VISIT,可以确认 PickleBuffer 参与 GC 遍历(类型标志 Py_TPFLAGS_HAVE_GC 已设置)。这意味着:只要 PickleBuffer 活着,被包装对象就不会被回收,buffer 视图始终有效——这正是带外传输期间"数据必须仍然存活"的前提。

  1. C API 与 Python 构造函数共享同一代码路径。Python 层的 picklebuf_newObjects/picklebufobject.c)与 PyPickleBuffer_FromObject 逐行等价:同样 tp_alloc、同样把 view.obj 预置 NULL、同样以 PyBUF_FULL_RO 请求 buffer。文档中 "Analogous to calling :class:pickle.PickleBuffer with obj in Python" 的说法因此在源码层面成立,两者行为严格一致。

PyPickleBuffer_GetBuffer 与 PyPickleBuffer_Release:视图访问与释放的契约

文档对 PyPickleBuffer_GetBuffer 有三条契约:返回的指针在 picklebuf 存活且未 release 期间有效;调用者不得修改或释放该 Py_buffer;若已 release 则抛出 ValueError。对照 Objects/picklebufobject.c 的实现:

const Py_buffer *
PyPickleBuffer_GetBuffer(PyObject *obj)
{
    PyPickleBufferObject *self = (PyPickleBufferObject *) obj;

    if (!PyPickleBuffer_Check(obj)) {
        PyErr_Format(PyExc_TypeError,
                     "expected PickleBuffer, %.200s found",
                     Py_TYPE(obj)->tp_name);
        return NULL;
    }
    if (self->view.obj == NULL) {
        PyErr_SetString(PyExc_ValueError,
                        "operation forbidden on released PickleBuffer object");
        return NULL;
    }
    return &self->view;
}

实现细节补充了文档未展开的两个信息:

  • 输入类型错误时抛 TypeError 而非静默失败。文档只说 "On failure, set an exception and return NULL",源码表明传入非 PickleBuffer 对象时设置的是 TypeError("expected PickleBuffer, ..."),传入已释放的实例时才是文档明确提到的 ValueError
  • "已释放"的判定依据是 view.obj == NULLPyBuffer_Release 在释放视图时会把 view.obj 置 NULL,因此类型内部无需额外布尔位就能区分"持有视图"与"已释放"两种状态。PyPickleBuffer_ReleaseObjects/picklebufobject.c)先做同样的类型检查,然后直接调用 PyBuffer_Release(&self->view) 返回 0——即释放动作把对原始对象的引用计数减一,这正是与 Python 层 release() 方法(picklebuf_release 函数)完全等价的语义。

生命周期与 GC 行为

理解 GetBuffer 返回的"指针有效性",需要看对象的整个生命周期:

  • GC 遍历picklebuf_traverse 只访问 self->view.obj,即 GC 根集通过 PickleBuffer 能到达原始对象;
  • GC 清理picklebuf_clear 调用 PyBuffer_Release(&self->view),在 GC 清理阶段就把底层视图释放掉(对象仍可重建使用路径之外的引用);
  • 析构picklebuf_dealloc 依次 PyObject_GC_UnTrack、清除弱引用、PyBuffer_Release,最后 tp_free

因此文档中 "The returned pointer is valid as long as picklebuf is alive and has not been released" 的边界由两条保证支撑:tp_clear/tp_dealloc 都会释放视图;release 后 view.obj 为 NULL,GetBuffer 立刻报错而不会返回悬空视图。调用者只需遵守"不要把返回的 Py_buffer * 存到对象销毁之后"这一条。

PickleBuffer 自身的 buffer 协议:raw() 与代理式 getbuffer

除了文档列出的 C API,理解 PickleBuffer 还绕不开它作为 buffer 提供者的行为,因为 pickle 的带外序列化正是通过向它请求 buffer 来完成取数的。

Objects/picklebufobject.c 中实现了 picklebuf_getbuf

static int
picklebuf_getbuf(PyObject *op, Py_buffer *view, int flags)
{
    PyPickleBufferObject *self = (PyPickleBufferObject*)op;
    if (self->view.obj == NULL) {
        PyErr_SetString(PyExc_ValueError,
                        "operation forbidden on released PickleBuffer object");
        return -1;
    }
    return PyObject_GetBuffer(self->view.obj, view, flags);
}

注意它不复制、不转发自己的视图,而是把 buffer 请求原样转发给被包装的原始对象(self->view.obj),原始对象返回什么视图就是什么。配套的 picklebuf_releasebuf 是空函数,源码注释解释了原因:由于 bf_getbuffer 重定向到原始对象,释放会由原始对象完成,该桩函数"只存在于向 Python/getargs.c 的信号检查表明 PickleBuffer 导出的 buffer 具有非平凡释放行为"。

raw() 方法(Objects/picklebufobject.c)则回答"如何把 PickleBuffer 当纯字节块看待":它先用 PyMemoryView_FromObject 取 memoryview,再原地修改该 memoryview 的视图,把 format 改为 "B"ndim 置 1、itemsize 置 1、shape 指向总长度、strides 指向 (1,),得到"原始字节"视图;若底层视图有 suboffsets 或非 C 连续,则抛出 BufferError("cannot extract raw buffer from non-contiguous buffer")。文档虽未单列 raw/release 方法,但 release 的 C API 正是它的等价物,raw 则是 Lib/pickle.py 纯 Python 实现里 save_picklebuffer 用来取数并校验连续性的手段(with obj.raw() as m:)。

序列化链路:save_picklebuffer 如何触发带外写入

PickleBuffer 在 pickle 协议 5 下如何被写进流,是 Modules/_pickle.csave_picklebufferModules/_pickle.c)的职责。完整流程如下:

  1. 协议版本检查self->proto < 5 时抛出 PicklingError("PickleBuffer can only be pickled with protocol >= 5")。带外传输是协议 5 引入的特性,旧协议没有 NEXT_BUFFER 操作码;
  2. 取视图:以 PyBUF_FULL_RO 请求 buffer(与构造时一致);
  3. 连续性检查view.suboffsets != NULL || !PyBuffer_IsContiguous(&view, 'A') 时抛 PicklingError("PickleBuffer can not be pickled when pointing to a non-contiguous buffer")。带外数据必须是可整体拷贝的连续块;
  4. in-band / out-of-band 决策:若 Pickler 设置了 buffer_callback,则以 PickleBuffer 对象为参数调用它,回调返回真值表示"仍然写在流内(in-band)";
  5. 分支写入
    • in-band:按 view.readonly 选择 _save_bytes_data_save_bytearray_data,把数据直接写进 pickle 流(C 实现直接引用 view.buf,不做拷贝——Lib/pickle.py 纯 Python 版本在注释中明确 "The C implementation avoids a copy here");
    • out-of-band:只写入 NEXT_BUFFER(字节 '\x97',见 Modules/_pickle.c 中的 OPCODES 定义);若 view.readonly 为真,再追加一个 READONLY_BUFFER 操作码,告诉反序列化方重建时应得到只读对象(bytes)而非可写对象(bytearray);
  6. 收尾:无论走哪条分支,最后 PyBuffer_Release(&view)

反序列化端的对称实现在 load_next_bufferModules/_pickle.c),由操作码分发表 OP(NEXT_BUFFER, load_next_buffer) 注册(Modules/_pickle.c)。Unpickler 侧通常配合 buffers 参数(一个预填了带外数据对象的迭代器)逐个消费这些"缓冲区占位符"。

类型分发与模块注册

_pickle 如何识别 PickleBuffer?Modules/_pickle.c 中的类型分发直接比较 type == &PyPickleBuffer_Type,与 PyPickleBuffer_Check 同源;而 Modules/_pickle.c 的模块初始化中 PyModule_AddType(m, &PyPickleBuffer_Type) 把它同时注册进 _pickle 模块,Lib/pickle.pyfrom _pickle import PickleBuffer 导出到公共命名空间。Python 层的纯 Python Pickler 同样注册了分发:Lib/pickle.pydispatch[PickleBuffer] = save_picklebuffer,其 save_picklebufferLib/pickle.py)与 C 版语义一致:协议 < 5 报错、raw() 非连续报错、in-band 写 tobytes()、out-of-band 写 NEXT_BUFFER(及只读时的 READONLY_BUFFER)。

使用示例与验证

以下示例完整走了一遍文档列出的全部 C API,可直接作为 C 扩展中的用法模板:

#include "Python.h"
#include "cpython/picklebufobject.h"

PyObject *
demo_picklebuffer(PyObject *base)
{
    /* 1. 从任意支持 buffer 协议的对象创建 */
    PyObject *pb = PyPickleBuffer_FromObject(base);
    if (pb == NULL) {
        return NULL;              /* base 不支持 buffer 协议:异常已设置 */
    }

    if (!PyPickleBuffer_Check(pb)) {
        Py_DECREF(pb);
        return PyBool_FromLong(0); /* 恒成功,这里仅为演示 */
    }

    /* 2. 获取底层视图:只读使用,绝不修改或释放 */
    const Py_buffer *view = PyPickleBuffer_GetBuffer(pb);
    if (view == NULL) {
        Py_DECREF(pb);
        return NULL;              /* 已 release 时抛 ValueError */
    }
    fprintf(stderr, "size=%zd readonly=%d\n", view->len, view->readonly);

    /* 3. 释放底层 buffer(等价于 Python 层 release()) */
    if (PyPickleBuffer_Release(pb) < 0) {
        Py_DECREF(pb);
        return NULL;
    }

    Py_DECREF(pb);
    return Py_None;
}

Python 层对应的带外序列化/反序列化流程:

import pickle

data = bytearray(range(1000))

def send_ooo(channel, buf: pickle.PickleBuffer):
    # 真正的字节通过 channel 带外发送,pickle 流里只有占位符
    channel.send_nowait(bytes(buf))
    return False          # False => 强制 out-of-band

bufs = []
def recv_ooo(channel):
    return True           # 有 buffer_callback 时被回调,按需返回

p = pickle.Pickler(None, protocol=5, buffer_callback=send_ooo)
# 省略:p 的流输出与 channel 的实际收发
# 反序列化端:
# pickle.Unpickler(stream, buffers=[recv_ooo(channel) ...])

行为边界可从 Lib/test/test_picklebuffer.pyLib/test/picklecommon.pyLib/test/pickletester.py 的测试矩阵中验证:它们覆盖了 PickleBuffer 构造参数校验、raw()/release() 的异常路径,以及 pickle 协议 5 下 in-band/out-of-band 两种写法的往返一致性。

小结:API 契约速查

API 成功 失败 备注
PyPickleBuffer_Check(op) 返回 1(是实例)/ 0(不是) 不会失败 宏,Py_IS_TYPE 精确类型比较
PyPickleBuffer_FromObject(obj) 新 PickleBuffer 实例 NULL + 异常(TypeError/BufferError PyBUF_FULL_RO 请求视图,持有源对象强引用
PyPickleBuffer_GetBuffer(pb) const Py_buffer * 指向内部视图 NULL + 异常(类型错误 TypeError;已释放 ValueError 指针不得跨对象生命周期使用,不得修改或释放
PyPickleBuffer_Release(pb) 返回 0 返回 -1 + 异常(类型错误 TypeError 释放对源对象的引用;之后视图 view.obj 为 NULL

四条实践要点:

  1. 这些 API 位于 Include/cpython/picklebufobject.h,受 Py_LIMITED_API 保护,使用它们意味着扩展绑定 CPython 内部 API,跨小版本需重新编译;
  2. FromObject 之后 PickleBuffer 会持有源对象引用,GC 也能从它遍历到源对象——这是"带外数据在传输期间保持存活"的底层保证;
  3. GetBuffer 拿到的视图是只读契约:修改 Py_buffer 字段或对其调用 PyBuffer_Release 都是 UB,唯一的正确释放方式是 PyPickleBuffer_Release(或对象自身销毁,tp_dealloc/tp_clear 会兜底释放);
  4. 参与 pickle 带外传输要求底层 buffer 连续C-contiguous 且无 suboffsets),且协议版本 ≥ 5;非连续视图会在序列化期抛 PicklingError,这一点由 Modules/_pickle.c 的检查与 raw()BufferError 双重保障。
登录后查看全文
热门项目推荐
相关项目推荐