CPython bytes 对象 C API 深度解析:PyBytes_* 与 PyBytesWriter 新 API(3.15)全解
【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython
本文以 CPython 官方 C API 文档 Doc/c-api/bytes.rst 为主体,结合仓库中的头文件与源码实现,系统讲解 bytes 对象在 C 扩展层的完整 API 面:类型判定、创建、读取、拼接、格式化,以及 3.15 新增的PyBytesWriter增量构建 API。读完本文,你可以为 C 扩展编写安全、高效、符合当前 CPython 规范的 bytes 处理代码,并理解这些 API 在 Objects/bytesobject.c 中的底层行为。
需要说明的适用前提:本仓库为 CPython main 分支源码,其中PyBytesWriter标注为 3.15 新增,PyBytes_Join标注为 3.14 新增,而PyBytes_FromStringAndSize(NULL, len)与_PyBytes_Resize已在 3.15 被软弃用(soft-deprecated)。如果你的扩展面向更旧版本运行,请留意这些版本边界。
bytes 类型在 C 层的表示
C API 文档开篇即给出一个统一约定:这些函数在期望 bytes 参数时,如果收到非 bytes 参数会抛出TypeError(部分函数则不检查类型,后文会逐一标注)。
PyBytesObject 布局
公开文档将PyBytesObject描述为“PyObject的一个子类型,代表 Python 的 bytes 对象”。在源码层面,其完整定义位于 Include/cpython/bytesobject.h:
typedef struct { PyObject_VAR_HEAD Py_DEPRECATED(3.11) Py_hash_t ob_shash; char ob_sval[1]; /* Invariants: * ob_sval contains space for 'ob_size+1' elements. * ob_sval[ob_size] == 0. * ob_shash is the hash of the byte string or -1 if not computed yet. */ } PyBytesObject;三个关键事实:
- 尾部保证 null 终止:缓冲区实际分配
ob_size + 1字节,最后一个字节恒为'\0'。这正是PyBytes_AsString能返回char*且可当 C 字符串使用的依据——即使内容中间含\0也不会截断语义,长度以ob_size为准; - 不可变:bytes 是 immutable 类型,公开 API 从不提供原地修改入口(唯一的“原地”能力
_PyBytes_Resize是私有 API 且已被软弃用,见后文); - 内部细节受
Py_LIMITED_API保护:Include/bytesobject.h 仅在未启用稳定 ABI 时才包含cpython/bytesobject.h。也就是说,编写 stable ABI 扩展时你只能拿到PyBytes_AsStringAndSize这类函数接口,拿不到结构体字段——这是 CPython 有意为之的设计。
PyBytes_Type是 bytes 类型的PyTypeObject实例,与 Python 层的bytes类是同一个对象,声明见 Include/bytesobject.h。
类型判定:PyBytes_Check 与 PyBytes_CheckExact
int PyBytes_Check(PyObject *o) int PyBytes_CheckExact(PyObject *o)PyBytes_Check:o是 bytes 或 bytes子类型的实例时返回真;PyBytes_CheckExact:仅当o恰好是 bytes 类型(不含子类)时返回真。
两者都“总是成功”,不会失败。实现上它们是宏,见 Include/bytesobject.h:
#define PyBytes_Check(op) \ PyType_FastSubclass(Py_TYPE(op), Py_TPFLAGS_BYTES_SUBCLASS) #define PyBytes_CheckExact(op) Py_IS_TYPE((op), &PyBytes_Type)从源码结构看,PyBytes_Check依赖Py_TPFLAGS_BYTES_SUBCLASS标志位做快速子类判定,这与 list、dict 等可变容器的判定方式一致。经验法则:如果你打算调用依赖具体布局的内部接口,用CheckExact;如果只是需要“按 buffer 协议消费”,用Check即可。
创建 bytes 对象
PyBytes_FromString 与 PyBytes_FromStringAndSize
PyObject* PyBytes_FromString(const char *v) PyObject* PyBytes_FromStringAndSize(const char *v, Py_ssize_t len)- 成功返回持有新数据拷贝的 bytes 对象,失败返回
NULL; PyBytes_FromString要求v不得为 NULL(不会检查,传 NULL 是未定义行为),按 null 终止串拷贝;PyBytes_FromStringAndSize显式指定长度,因此数据可以包含内嵌\0,且不必 null 终止——这使得“从大缓冲区截取子串”这类操作天然安全。
文档给出的重要警告是:当v为NULL时,对象的内存是未初始化的。Objects/bytesobject.c 中对该行为的注释写得更直白:此时分配size+1字节(末字节置\0),留给你自己填充数据。这正是历史代码中“先占坑后写数据”模式的来源,也正是 3.15 弃用它的动机:
3.15 软弃用:
PyBytes_FromStringAndSize(NULL, len)的用法建议改用PyBytesWriterAPI。
实现细节值得注意:长度为 0 时返回的是不可回收的空 bytes 单例,单字节结果同样命中单例池——Objects/bytesobject.c 的_PyBytes_FromSize在size == 0时直接返回bytes_get_empty()单例。
PyBytes_FromFormat:printf 风格的二进制拼装
PyObject* PyBytes_FromFormat(const char *format, ...) PyObject* PyBytes_FromFormatV(const char *format, va_list vargs)PyBytes_FromFormat接受 printf 风格格式串,一次计算并填充结果,免去“先算长度、再分配、再格式化”三步走;FromFormatV是va_list变体,适合封装 variadic 函数时转发参数。文档强调:变参必须是 C 类型且与格式字符精确对应。官方支持的格式字符表如下(完整继承自 Doc/c-api/bytes.rst):
| 格式字符 | 参数类型 | 说明 |
|---|---|---|
%% | n/a | 字面量% |
%c | int | 单个字节,用 Cint表示 |
%d | int | 等价于printf("%d")(见注 1) |
%u | unsigned int | 等价于printf("%u")(见注 1) |
%ld | long | 等价于printf("%ld")(见注 1) |
%lu | unsigned long | 等价于printf("%lu")(见注 1) |
%zd | Py_ssize_t | 等价于printf("%zd")(见注 1) |
%zu | size_t | 等价于printf("%zu")(见注 1) |
%i | int | 等价于printf("%i")(见注 1) |
%x | int | 等价于printf("%x")(见注 1) |
%s | const char* | null 终止的 C 字符数组 |
%p | const void* | 指针的十六进制表示;与printf("%p")基本一致,但保证以字面量0x开头 |
注 1:对所有整型说明符(d/u/ld/lu/zd/zu/i/x),即使指定了精度,0转换标志仍然生效。
另一个行为细节:遇到无法识别的格式字符时,格式串剩余部分会被原样拷贝进结果,多余参数被丢弃——它是“宽容”的而非“报错”的。头文件中还标注了Py_GCC_ATTRIBUTE((format(printf, ...)))(见 Include/bytesobject.h),即编译器会像检查printf一样帮你检查格式串与参数的匹配性,写扩展时应保留这个属性。
典型用途是协议封包类代码:
// 构造一个 3 字节的帧头:opcode、类型、2 字节长度 PyObject * make_frame_header(int opcode, int type, Py_ssize_t len) { return PyBytes_FromFormat("%c%c%zd", opcode, type, len); }PyBytes_FromObject:经由 buffer 协议转换
PyObject* PyBytes_FromObject(PyObject *o)返回实现 buffer 协议的对象的 bytes 表示。文档特别提醒:创建期间源 buffer 不得被修改——这是 buffer 协议对象的通用约束。buffer 协议的完整定义可参阅 Doc/c-api/buffer.rst。
读取长度与内容
长度:PyBytes_Size 与 PyBytes_GET_SIZE
Py_ssize_t PyBytes_Size(PyObject *o) Py_ssize_t PyBytes_GET_SIZE(PyObject *o)PyBytes_Size做类型检查(失败时置错);PyBytes_GET_SIZE是“无错误检查”的快速宏,实现见 Include/cpython/bytesobject.h:它直接读取Py_SIZE(self)。两者仅在非稳定 ABI 下可拿到底层宏,但函数形式始终可用。
内容指针:PyBytes_AsString、PyBytes_AS_STRING
char* PyBytes_AsString(PyObject *o) char* PyBytes_AS_STRING(PyObject *o)PyBytes_AsString返回内部缓冲区指针,该缓冲区共len(o) + 1字节、末尾恒为 null。使用约束(文档原文语义):
- 数据不得修改——除非该对象是刚用
PyBytes_FromStringAndSize(NULL, size)创建的占位对象; - 指针不得释放,它属于对象自身;
o不是 bytes 时返回NULL并抛出TypeError。
PyBytes_AS_STRING跳过类型检查,直接强制转换读取(Include/cpython/bytesobject.h),只应在已确认为 bytes 的路径上使用。
带长度出口:PyBytes_AsStringAndSize
int PyBytes_AsStringAndSize(PyObject *obj, char **buffer, Py_ssize_t *length)通过出参返回 null 终止的内容指针与长度,成功返回0。关键行为:
- 若
length传NULL,表示调用方不允许内嵌\0;一旦检测到内嵌空字节,函数返回-1并抛出ValueError; - 返回的 buffer 指向内部缓冲区(末尾多出的 null 字节不计入
length),修改与释放约束同上; - 对象非 bytes 时返回
-1并抛TypeError。
3.5 版本变更:更早版本在内嵌 null 字节时抛的是TypeError,3.5 起改为ValueError——如果你的扩展对返回值和异常做联动处理,注意这个语义。这是 C 层读取二进制数据的推荐入口:它同时给出指针和长度,避免对含\0数据误用strlen。
拼接、合并与调整大小
PyBytes_Concat 与 PyBytes_ConcatAndDel
void PyBytes_Concat(PyObject **bytes, PyObject *newpart) void PyBytes_ConcatAndDel(PyObject **bytes, PyObject *newpart)两者都在*bytes处构造“旧内容 +newpart”的新 bytes 对象。引用计数语义需要仔细对待:
- 调用者持有新对象的引用;
*bytes旧值的引用被“偷走”(stolen),即不再释放;- 若新对象创建失败,旧引用依然被偷走,
*bytes被置为NULL,异常被置位; PyBytes_ConcatAndDel额外会递减newpart的引用计数(强引用被释放)。
若newpart实现 buffer 协议,创建期间其 buffer 不得被修改。
PyBytes_Join(3.14 新增)
PyObject* PyBytes_Join(PyObject *sep, PyObject *iterable)与 Python 层sep.join(iterable)语义相同:
sep必须是 bytes 对象——注意它与PyUnicode_Join的差异:后者接受NULL分隔符并视为空格,而PyBytes_Join不接受NULL分隔符;iterable必须是可迭代对象,且产出的元素必须实现 buffer 协议;- 成功返回新 bytes,失败置异常并返回
NULL。
该 API 在 Include/cpython/bytesobject.h 中声明,同时保留了Py_DEPRECATED(3.14)的_PyBytes_Join别名转发到新名称。C 解释器自身也在用它拼装字节串,例如 Modules/_sre/sre.c 和 Modules/_io/bufferedio.c 中均可看到PyBytes_Join的调用,Lib/test/test_capi/test_bytes.py 则提供了它的 C API 测试覆盖。
_PyBytes_Resize:私有 API 与弃用方向
int _PyBytes_Resize(PyObject **bytes, Py_ssize_t newsize)这是以_Py前缀暴露的私有接口:把*bytes指向的对象调整到新长度,可理解成“创建新对象并销毁旧对象,只是更高效”。成功时*bytes保存调整后的对象(地址可能已变);重分配失败时原对象被释放、*bytes置NULL、设置MemoryError并返回-1。
3.15 软弃用:官方建议改用PyBytesWriter。带下划线的私有前缀本身就表明它不属于稳定 ABI,扩展代码不应继续依赖它。
表示与转义:PyBytes_Repr 与 PyBytes_DecodeEscape
PyBytes_Repr
PyObject *PyBytes_Repr(PyObject *bytes, int smartquotes)获取 bytes 的字符串表示,当前用于实现 Python 层bytes.__repr__。注意它不做类型检查:传入非 bytes 或NULL是未定义行为。smartquotes为真时,若内容含单引号则改用双引号包裹:字节串b"'Python'"在smartquotes为真时显示为b"'Python'",为假时显示为b"\'Python\'"。成功返回表示串的str对象(强引用),失败返回NULL并置异常。
PyBytes_DecodeEscape
PyObject *PyBytes_DecodeEscape(const char *s, Py_ssize_t len, const char *errors, Py_ssize_t unicode, const char *recode_encoding)对反斜杠转义串s做反转义,s不得为NULL,len必须为其大小。errors取"strict"、"replace"、"ignore"之一,传NULL时默认"strict"。成功返回反转义后的 bytes 对象(强引用),失败返回NULL并置异常。3.9 变更:unicode与recode_encoding两个参数已不再使用,仅为保持 ABI 兼容而保留。
PyBytesWriter:3.15 新增的增量构建 API
PyBytesWriter是本文档最值得关注的部分——它是 3.15 引入的公开 API,用于构建 Python bytes 对象,同时接过了PyBytes_FromStringAndSize(NULL, len)(预分配未知内容)和_PyBytes_Resize(增量调整)两个被软弃用场景的职责。
生命周期:Create / Finish / Discard
PyBytesWriter* PyBytesWriter_Create(Py_ssize_t size) PyObject* PyBytesWriter_Finish(PyBytesWriter *writer) PyObject* PyBytesWriter_FinishWithSize(PyBytesWriter *writer, Py_ssize_t size) PyObject* PyBytesWriter_FinishWithPointer(PyBytesWriter *writer, void *buf) void PyBytesWriter_Discard(PyBytesWriter *writer)生命周期规则非常严格:
PyBytesWriter_Create(size):创建一个 writer。size必须 ≥ 0;若size > 0则精确分配size字节并置 writer 大小为size(注意:此函数不做过量分配),此后调用者负责通过PyBytesWriter_GetData把这size字节写完;出错返回NULL并置异常;PyBytesWriter_Finish:把 writer 收尾为 Python bytes 对象。无论成败,调用之后 writer 实例即失效,不能再调用任何 API;PyBytesWriter_FinishWithSize(writer, size):与Finish类似,但先按size调整大小再产出对象。典型场景是用GetData拿到指针做二进制写操作(如fwrite/read)后,实际写入量可能小于申请量,用结束指针差值确定真实长度;PyBytesWriter_FinishWithPointer(writer, buf):用指针buf定位结束位置,伪代码为size = buf - PyBytesWriter_GetData(writer),然后等价于FinishWithSize。若buf不在内部缓冲区范围内,置异常并返回NULL(源码实现抛出ValueError: "invalid end pointer",见 Objects/bytesobject.c);PyBytesWriter_Discard(writer):错误路径上丢弃 writer;writer为NULL时什么都不做;调用后实例同样失效。
一个完整的工作流示例(注意错误路径必须Discard):
PyObject * encode_record(const char *tag, Py_ssize_t n) { PyBytesWriter *writer = PyBytesWriter_Create(32); // 预估容量 if (writer == NULL) { return NULL; // 异常已置 } if (PyBytesWriter_WriteBytes(writer, tag, -1) < 0) { // -1 表示按 strlen 取长 PyBytesWriter_Discard(writer); return NULL; } if (PyBytesWriter_Format(writer, "|%zd", n) < 0) { PyBytesWriter_Discard(writer); return NULL; } PyObject *result = PyBytesWriter_Finish(writer); // 成败后 writer 均失效 return result; }PyBytesWriter还有一条明确的红线:不线程安全——同一时刻只能由一个线程使用。这与 Doc/data/threadsafety.dat 中对 C API 线程安全属性的逐条登记机制相一致。
高层写入:WriteBytes 与 Format
int PyBytesWriter_WriteBytes(PyBytesWriter *writer, const void *bytes, Py_ssize_t size) int PyBytesWriter_Format(PyBytesWriter *writer, const char *format, ...)WriteBytes:把 writer 内部缓冲区增长size字节,将bytes的size字节拷到 writer 末端,并累加 writer 大小。size == -1时以strlen(bytes)计算长度。成功返回0,失败置异常返回-1;Format:与PyBytes_FromFormat相同支持集,但输出直接写到 writer 末端,按需增长缓冲区。
实现见 Objects/bytesobject.c:WriteBytes先Grow再memcpy;Format则先按格式串长度Grow预留空间,写完后用PyBytesWriter_Resize收缩回真实输出长度——这解释了为什么格式串可以包含占位符而不会撑大最终对象。
访问器:GetData 与 GetSize
void* PyBytesWriter_GetData(PyBytesWriter *writer) Py_ssize_t PyBytesWriter_GetSize(PyBytesWriter *writer)GetData返回内部缓冲区起点指针;指针保持有效,直到对该 writer 调用了GetData/GetSize之外的任何 API。两者都不能失败。典型用法是拿到指针后通过FinishWithPointer报告写入终点,或配合GrowAndUpdatePointer维护“当前位置”游标。
低层控制:Resize、Grow、GrowAndUpdatePointer
int PyBytesWriter_Resize(PyBytesWriter *writer, Py_ssize_t size) int PyBytesWriter_Grow(PyBytesWriter *writer, Py_ssize_t grow) void* PyBytesWriter_GrowAndUpdatePointer(PyBytesWriter *writer, Py_ssize_t size, void *buf)Resize:把 writer 调整到size字节(可放大也可缩小),新分配的字节保持未初始化。该函数会按策略过量分配,以获得多次 resize 下的摊还性能;Grow:在当前大小基础上增加grow字节;grow可为负以收缩(但收缩后大小不能为负,否则抛ValueError);同样过量分配;GrowAndUpdatePointer:等价于Grow但会同步更新游标指针buf——若扩容导致内部缓冲区整体搬迁,buf也随之移动,且其相对内部缓冲区的偏移保持不变;出错置异常返回NULL,buf不得为NULL。文档给出的伪代码即“记录偏移 → Grow → 重新加偏移”。
源码透视:小对象缓冲、过量分配与单例回收
文档描述了“通常过量分配”这类行为,而 Objects/bytesobject.c 和 Include/internal/pycore_bytesobject.h 给出了完整机制:
256 字节栈上小缓冲(SBO):
PyBytesWriter内部结构含char small_buffer[256]。只要数据不超过 256 字节,writer 就完全不触发堆分配——这是绝大多数“拼个短头”场景的快路径;按平台调参的过量分配系数:
#ifdef MS_WINDOWS /* On Windows, overallocate by 50% is the best factor */ # define OVERALLOCATE_FACTOR 2 #else /* On Linux, overallocate by 25% is the best factor */ # define OVERALLOCATE_FACTOR 4 #endif即 Linux 上每次 resize 追加 25% 容量、Windows 上追加 50%,注释表明这是实测调参结果(见 Objects/bytesobject.c);
writer 结构体走 freelist:
byteswriter_create通过_Py_FREELIST_POP_MEM(bytes_writers)复用结构体内存,Discard时归还——高频短生命周期 writer 的分配开销被摊平;单例回填:
PyBytesWriter_FinishWithSize在结果为空串或单字节时,会替换为全局空串单例或字节单例池中的对象(Objects/bytesobject.c),与PyBytes_FromStringAndSize的单例行为保持一致,从而保证小 bytes 的内存与is语义稳定。
从源码结构看,Create阶段“不做过量分配”(resize标志为 0)、而Grow/Resize阶段“做过量分配”的设计,让调用者可以用预估容量精确起步,再由写入过程按需弹性扩张——这正是它替代_PyBytes_Resize手动循环的原因。
实践建议与验证途径
结合文档中的弃用标记,面向当前 CPython 的代码组织建议如下:
| 场景 | 推荐 API | 避免 |
|---|---|---|
| 从已知 C 字符串构造 | PyBytes_FromString/FromFormat | — |
| 长度已知、内容未知,逐段填充 | PyBytesWriter_Create+GetData/WriteBytes+Finish | PyBytes_FromStringAndSize(NULL, n)(3.15 起软弃用) |
| 反复追加、无法预估总长 | PyBytesWriter+Grow/WriteBytes | _PyBytes_Resize(私有且 3.15 起软弃用)、重复PyBytes_Concat |
| 一次性拼接可迭代序列 | PyBytes_Join | 手工循环Concat |
| 读取内容 | PyBytes_AsStringAndSize(指针+长度) | 对返回值直接strlen |
| 需要精确类型判断 | PyBytes_CheckExact(内部布局相关路径)/PyBytes_Check(消费路径) | — |
所有成功/失败路径遵循统一纪律:返回NULL/-1即异常已置位,错误分支上必须PyBytesWriter_Discard收尾,Finish/FinishWithSize/FinishWithPointer之后不得再触碰 writer。
验证与深入阅读路径(均为本仓库内文件):
- 实现主体:Objects/bytesobject.c(含
PyBytesWriter全量实现,自 Objects/bytesobject.c#L3590 起); - 公共头文件:Include/bytesobject.h、Include/cpython/bytesobject.h(稳定 ABI 下不可见);
- 内部结构体与内联辅助:Include/internal/pycore_bytesobject.h;
- C API 测试:Lib/test/test_capi/test_bytes.py 与配套 C 扩展 Modules/_testcapi/bytes.c,覆盖了
PyBytesWriter_Create、单例行为、内部_PyBytesWriter_CreateByteArray等细节; - 版本变更说明:Doc/whatsnew/3.14.rst 记录了
PyBytes_Join的引入;PyBytes_FromStringAndSize(NULL, n)与_PyBytes_Resize的后续移除计划见 Doc/deprecations/c-api-pending-removal-in-3.18.rst。
小结
CPython 的 bytes C API 以“不可变 + 尾 null + 长度显式”为核心设计:创建类函数负责把 C 内存安全地提升为 Python 对象,读取类函数在“可当 C 字符串用”与“二进制安全”之间提供了显式开关(As*系列是否带长度参数),而 3.15 的PyBytesWriter用一套带严格生命周期语义的增量构建 API,统一收编了原先靠“NULL 占位 + 私有 Resize”拼凑的二进制拼装模式。理解PyBytesWriter的 SBO 快路径、平台相关的过量分配系数和单例回填(Objects/bytesobject.c#L3590-L3794),能让你在新扩展中既写出符合弃用方向的代码,也清楚其底层性能特征从何而来。
【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考