- 游戏开发
- 后端
【免费下载链接】kbengine
A MMOG engine of server.
导读
本文以 KBEngine 仓库内嵌的 CPython 3.7.3 源码文档 kbe/src/lib/python/Doc/c-api/objbuffer.rst 为主体,系统讲解 Python C API 中"旧缓冲区协议"(Old Buffer Protocol)的四个遗留函数:PyObject_AsCharBuffer、PyObject_AsReadBuffer、PyObject_CheckReadBuffer与PyObject_AsWriteBuffer。读者将掌握它们的签名、语义、底层实现(Objects/abstract.c),理解它们为何在 Python 3 被标记为弃用,以及如何用新缓冲区协议(PyObject_GetBuffer/PyBuffer_Release)和参数解析格式码s*/y*/w*完成等价替换。对于在 KBEngine 这类以 C++ 服务端嵌入 Python 的引擎中编写扩展模块的开发者,本文是一份可直接落地的内存缓冲区 API 选型参考。
一、什么是"旧缓冲区协议"
在 Python 2 时代,C 扩展需要直接读写 Python 对象背后的一段连续内存(例如bytes、bytearray或自定义二进制类型)时,使用一组以 "As/Check + Buffer" 命名的辅助函数。这套接口统称为旧缓冲区协议(Old Buffer Protocol),其核心特征是:一次性给出"指向内存的裸指针 + 长度",调用方拿走后即可直接操作,接口本身不提供资源生命周期管理。
- 文档原文(objbuffer.rst)明确指出:这些函数在 Python 2 中属于旧缓冲区协议 API,在 Python 3 中该协议已不存在,但函数仍然被导出,以便于移植 2.x 代码。
- 它们本质上是围绕新缓冲区协议(见 buffer.rst)的兼容包装,但不会把资源获取与释放的控制权交给调用方——导出的缓冲区在被使用时,其底层的导出对象是否仍存活、内存是否仍有效,完全不受这几个函数约束。
因此文档给出明确建议:新代码应调用PyObject_GetBuffer(或在PyArg_ParseTuple一族函数中使用y*/w*格式码)来获得对象上的缓冲区视图,并在不再需要时调用PyBuffer_Release释放。
二、四个遗留函数的签名与语义
以下四个函数均声明于 kbe/src/lib/python/Include/abstract.h,并被Py_DEPRECATED(3.0)宏标记为弃用。
1.PyObject_AsCharBuffer:只读字符缓冲区
int PyObject_AsCharBuffer(PyObject *obj, const char **buffer, Py_ssize_t *buffer_len)- 用途:返回一个指向只读内存位置的指针,该内存可作为"基于字符的输入"使用。
obj必须支持单段(single-segment)字符缓冲区接口。 - 成功:返回
0,此时*buffer被设置为内存位置,*buffer_len被设置为缓冲区长度;输出参数仅在无错误时才会被写入。 - 失败:返回
-1并设置TypeError异常。
2.PyObject_AsReadBuffer:只读任意数据缓冲区
int PyObject_AsReadBuffer(PyObject *obj, const void **buffer, Py_ssize_t *buffer_len)- 用途:与
PyObject_AsCharBuffer等价,区别在于它要求对象支持"可读、单段"缓冲区接口,返回的指针可包含任意二进制数据(不限于字符语义)。 - 成功:返回
0,写入*buffer与*buffer_len。 - 失败:返回
-1并设置TypeError。
3.PyObject_CheckReadBuffer:只读缓冲区支持检测
int PyObject_CheckReadBuffer(PyObject *o)- 用途:检测
o是否支持"单段可读缓冲区接口"。 - 返回值:支持返回
1,否则返回0。该函数永远成功,不会抛出异常。 - 重要实现细节:文档明确指出,此函数会尝试获取并释放一个缓冲区,过程中若对应函数抛出了异常,异常会被静默吞掉(suppressed)。如果需要错误报告,应改用
PyObject_GetBuffer。
4.PyObject_AsWriteBuffer:可写缓冲区
int PyObject_AsWriteBuffer(PyObject *obj, void **buffer, Py_ssize_t *buffer_len)- 用途:返回一个指向可写内存位置的指针。
obj必须支持"单段、可写"缓冲区接口(即底层是可变对象,如bytearray)。 - 成功:返回
0,写入*buffer与*buffer_len。 - 失败:返回
-1并设置TypeError;从源码看,错误消息为"expected a writable bytes-like object"。
引用计数说明
kbe/src/lib/python/Doc/data/refcounts.dat 中记录了这些函数的引用计数约定:obj/o参数均为借入引用(refcount 标记为0,即不新增引用),函数本身返回int。这意味着调用方不能指望这些函数持有或延长导出对象的生命周期。
三、源码级实现剖析
在 kbe/src/lib/python/Objects/abstract.c 中可以完整看到这四个函数的真实实现,理解它们是如何"包装"新缓冲区协议的。
3.1PyObject_CheckReadBuffer的实现
int PyObject_CheckReadBuffer(PyObject *obj) { PyBufferProcs *pb = obj->ob_type->tp_as_buffer; Py_buffer view; if (pb == NULL || pb->bf_getbuffer == NULL) return 0; if ((*pb->bf_getbuffer)(obj, &view, PyBUF_SIMPLE) == -1) { PyErr_Clear(); return 0; } PyBuffer_Release(&view); return 1; }关键点:
- 它通过对象的类型对象
tp_as_buffer查找bf_getbuffer槽位,不存在则直接判定不支持。 - 用
PyBUF_SIMPLE标志请求一个简单缓冲区;如果导出失败,调用PyErr_Clear()清除异常并返回 0——这正是文档所说"异常被抑制"的出处。 - 成功后立即
PyBuffer_Release(&view)释放,然后返回1。可见检测本身就是"试拿一下再放掉"的过程。 - 历史记录(Misc/HISTORY)提到 Issue #3946:该函数曾在
memoryview对象上崩溃,说明这类"裸检测"存在边界情况,这也是新代码应改用PyObject_GetBuffer的原因之一。
3.2 只读族函数共用同一个助手函数
PyObject_AsCharBuffer与PyObject_AsReadBuffer实际上都委托给静态函数as_read_buffer:
static int as_read_buffer(PyObject *obj, const void **buffer, Py_ssize_t *buffer_len) { Py_buffer view; if (obj == NULL || buffer == NULL || buffer_len == NULL) { null_error(); return -1; } if (PyObject_GetBuffer(obj, &view, PyBUF_SIMPLE) != 0) return -1; *buffer = view.buf; *buffer_len = view.len; PyBuffer_Release(&view); return 0; } int PyObject_AsCharBuffer(PyObject *obj, const char **buffer, Py_ssize_t *buffer_len) { return as_read_buffer(obj, (const void **)buffer, buffer_len); } int PyObject_AsReadBuffer(PyObject *obj, const void **buffer, Py_ssize_t *buffer_len) { return as_read_buffer(obj, buffer, buffer_len); }关键点:
- 空指针入参走
null_error()并返回-1。 - 通过新协议的
PyObject_GetBuffer(obj, &view, PyBUF_SIMPLE)获取视图,把view.buf和view.len拷贝给调用方,然后立即PyBuffer_Release(&view)。 - 由此可以推断:调用方拿到的裸指针在函数返回后,导出对象引用已被释放,缓冲区随时可能失效。源码上方注释也直白地写道:"We release the buffer right after use of this function which could cause issues later on. Don't use these functions in new code."(我们在使用后立即释放缓冲区,这可能在后续造成问题,不要在 new code 中使用这些函数。)
3.3PyObject_AsWriteBuffer的实现
int PyObject_AsWriteBuffer(PyObject *obj, void **buffer, Py_ssize_t *buffer_len) { PyBufferProcs *pb; Py_buffer view; if (obj == NULL || buffer == NULL || buffer_len == NULL) { null_error(); return -1; } pb = obj->ob_type->tp_as_buffer; if (pb == NULL || pb->bf_getbuffer == NULL || ((*pb->bf_getbuffer)(obj, &view, PyBUF_WRITABLE) != 0)) { PyErr_SetString(PyExc_TypeError, "expected a writable bytes-like object"); return -1; } *buffer = view.buf; *buffer_len = view.len; PyBuffer_Release(&view); return 0; }关键点:
- 与只读版本的关键差异是使用
PyBUF_WRITABLE标志请求可写视图;对不可变对象(如bytes)导出必然失败。 - 失败时统一设置
TypeError,错误消息为"expected a writable bytes-like object",与文档所述"返回 -1 并设置 TypeError"一致。 - 同样的模式:拿指针 → 复制长度 → 立即
PyBuffer_Release,不向调用方移交任何生命周期责任。
3.4 导出符号与二进制兼容
在 Windows 平台上,这些函数仍作为 Python 3.7 的 ABI 符号导出,见 kbe/src/lib/python/PC/python3.def(如PyObject_AsCharBuffer=python37.PyObject_AsCharBuffer等)。这证实了文档"为便于移植 2.x 代码而保留导出"的说法——老扩展链接期不会因符号缺失而失败,但运行时行为已经变为新协议包装。
四、为什么弃用:新缓冲区协议与资源生命周期
旧协议函数最大的问题不在取指针,而在生命周期:它们内部"拿 view → 拷贝指针 → 立刻释放",调用方后续使用的内存无法保证仍属于一个存活的对象。新缓冲区协议(buffer.rst)通过Py_buffer结构解决这一问题:
- 生产者侧:类型通过
tp_as_buffer的bf_getbuffer槽位导出缓冲区,允许选择性导出只读 / 读写视图。 - 消费者侧:调用
PyObject_GetBuffer获取Py_buffer视图,其中view.buf指向数据、view.len是长度、view.obj持有导出对象的新引用;使用完毕后必须调用PyBuffer_Release归还引用并结束导出。文档明确警告:忘记释放会导致资源泄漏等问题。
/* 新协议推荐用法 */ Py_buffer view; if (PyObject_GetBuffer(obj, &view, PyBUF_SIMPLE) != 0) { /* 处理异常 */ return -1; } /* 在此安全使用 view.buf / view.len,view.obj 保证对象存活 */ PyBuffer_Release(&view);实现上,PyObject_GetBuffer本身也在 abstract.c:它检查tp_as_buffer与bf_getbuffer,缺失时抛出"a bytes-like object is required, not '%.100s'",否则调用导出函数。
五、参数解析中的替代方案:s*、y*、w*
文档推荐的另一条路径是在PyArg_ParseTuple一族函数中直接使用格式码获取Py_buffer,完全绕开旧协议函数。相关格式码定义在 arg.rst:
| 格式码 | 接受对象 | 填充目标 | 说明 |
|---|---|---|---|
s* | str或 bytes-like 对象 | Py_buffer | 接受 Unicode(按 UTF-8 转换)与字节类对象;C 串可含内嵌 NUL |
y* | bytes-like 对象(不接受 Unicode) | Py_buffer | 文档标注:这是接收二进制数据的推荐方式 |
w* | 实现读写缓冲区接口的对象 | Py_buffer | 只接受可变对象;完成后调用方须调用PyBuffer_Release |
一个典型的迁移对照:
/* 旧写法(弃用):拿到即失效的裸指针 */ const char *buf; Py_ssize_t len; if (PyObject_AsCharBuffer(obj, &buf, &len) < 0) { /* TypeError 已设置 */ } /* 新写法:生命周期受控的视图 */ Py_buffer view; if (!PyArg_ParseTuple(args, "y*", &view)) { /* 参数错误已设置 */ } /* 使用 view.buf / view.len ... */ PyBuffer_Release(&view);注意y*不接收 Unicode 对象,这使其成为二进制协议数据的首选;而w*保证对象可变,等价于旧PyObject_AsWriteBuffer的语义但生命周期安全。头文件 abstract.h 中的 FIXME 注释也印证了官方立场:这些旧函数在 Python 自身内部都应被替换,仅出于向后兼容而保留。
六、迁移注意事项与在 KBEngine 中的上下文
6.1 迁移检查清单
- 检测对象是否支持只读缓冲区:用
PyObject_GetBuffer(obj, &view, PyBUF_SIMPLE)的返回值判断,而非PyObject_CheckReadBuffer(后者吞异常、无法报告错误原因)。 - 获取只读数据:改用
PyObject_GetBuffer+PyBuffer_Release,或用格式码y*(二进制)/s*(文本或二进制)。 - 获取可写数据:用
PyBUF_WRITABLE标志,或格式码w*;务必在写完后PyBuffer_Release。 - 不要持有旧 API 返回的裸指针:它们在函数返回后可能指向已失效内存;务必在受控的 view 生命周期内完成读写。
6.2 项目内的实际上下文
本仓库内嵌的 CPython 版本为3.7.3(见 kbe/src/lib/python/README.rst),因此上述弃用标注、符号导出和实现代码即为该版本的真实形态。在 KBEngine 的脚本桥接层中,实际代码已按现代 API 编写:例如 kbe/src/lib/pyscript/py_memorystream.cpp 通过PyBytes_AsStringAndSize直接取得 bytes 内容并追加到流,而不是使用本文的旧协议函数——这正符合官方"新代码不要使用这些函数"的建议。历史变更记录(Misc/HISTORY)中 Issue #22896 也明确要求"避免使用PyObject_AsCharBuffer()、PyObject_AsReadBuffer()和PyObject_AsWriteBuffer()",可作为迁移的权威依据。
七、总结
PyObject_AsCharBuffer/PyObject_AsReadBuffer/PyObject_CheckReadBuffer/PyObject_AsWriteBuffer是 Python 2 旧缓冲区协议留给 Python 3 的兼容残留:它们在新协议之上"取指针、即释放",不提供生命周期保障,自 3.0 起全部标记弃用。读懂它们的实现(abstract.c)能帮你彻底理解"为什么裸指针不可靠",而正确的现代写法始终是PyObject_GetBuffer+PyBuffer_Release,或在PyArg_ParseTuple中使用y*/w*格式码。在 KBEngine 这类长期维护、C++ 与 Python 深度耦合的引擎源码中,遵循这一迁移路径是编写健壮 C 扩展的基本前提。
- 游戏开发
- 后端
【免费下载链接】kbengine
A MMOG engine of server.
相关推荐
KBEngine 内嵌 CPython 3.7 的 contextvars C API 完全指南:PyContext 与 PyContextVar 接口详解
KBEngine 内嵌 CPython 3.7 的 contextvars C API 完全指南:PyContext 与 PyContextVar 接口详解 导
游戏开发后端KBEngine 内嵌 CPython 3.7 C API 详解:操作系统工具、sys 模块访问与进程控制
KBEngine 内嵌 CPython 3.7 C API 详解:操作系统工具、sys 模块访问与进程控制 KBEngine 服务器将完整的 CPython 源
游戏开发后端mmcv.visualization 可视化模块完全指南:图像、检测框与光流展示 API 详解
mmcv.visualization 可视化模块完全指南:图像、检测框与光流展示 API 详解 本文以 OpenMMLab 计算机视觉基础库 mmcv 的可视化
游戏开发后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考