news 2026/10/16 2:11:59

KBEngine 内嵌 CPython 3.7 的旧缓冲区协议(Old Buffer Protocol)C API 详解与迁移指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
KBEngine 内嵌 CPython 3.7 的旧缓冲区协议(Old Buffer Protocol)C API 详解与迁移指南
  • 游戏开发
  • 后端

【免费下载链接】kbengine

A MMOG engine of server.

项目地址:https://gitcode.com/gh_mirrors/kb/kbengine
点击查看免费下载

导读

本文以 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 迁移检查清单

  1. 检测对象是否支持只读缓冲区:用PyObject_GetBuffer(obj, &view, PyBUF_SIMPLE)的返回值判断,而非PyObject_CheckReadBuffer(后者吞异常、无法报告错误原因)。
  2. 获取只读数据:改用PyObject_GetBuffer+PyBuffer_Release,或用格式码y*(二进制)/s*(文本或二进制)。
  3. 获取可写数据:用PyBUF_WRITABLE标志,或格式码w*;务必在写完后PyBuffer_Release。
  4. 不要持有旧 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.

项目地址:https://gitcode.com/gh_mirrors/kb/kbengine
点击查看免费下载
上一篇:Ansible Semaphore 自动化部署工具全面指南
下一篇:5个强力功能让Buzz成为你的终极离线语音转录助手

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/16 2:10:30

Mac安装傲游云浏览器v4.1.2.1000:从Gatekeeper到云同步的避坑指南

简介&#xff1a;傲游云浏览器&#xff08;Maxthon&#xff09;Mac版是一款网页浏览工具&#xff0c;版本为v4.1.2.1000&#xff0c;面向需要在Mac OS与Windows、Android、iOS等多设备间同步浏览数据且希望获得一致浏览体验的Mac用户。该版本针对Mac OS操作习惯进行优化&#x…

作者头像 李华
网站建设 2026/10/16 2:10:18

LeetCode 11 盛最多水的容器:双指针 O(n) 题解与多语言实现

示例工程 【免费下载链接】leetcode &#x1f525;LeetCode solutions in any programming language | 多种编程语言实现 LeetCode、《剑指 Offer&#xff08;第 2 版&#xff09;》、《程序员面试金典&#xff08;第 6 版&#xff09;》题解 项目地址&#xff1a; https://git…

作者头像 李华
网站建设 2026/10/16 2:09:18

Agentic RAG实战:从传统检索到可编排智能问答

1. 项目概述&#xff1a;从"检索问答"到"可编排的智能检索"先说结论&#xff1a;Agentic RAG Pipeline不是把一个大模型包一层检索工具那么简单&#xff0c;它是一个让模型像人一样去"查资料、筛资料、质疑资料"的完整流程系统。这两年只要做知识…

作者头像 李华