首页
/ CPython 数据序列化 C API 全解:用 PyMarshal_* 函数族读写 marshal 二进制格式

CPython 数据序列化 C API 全解:用 PyMarshal_* 函数族读写 marshal 二进制格式

2026-09-04 12:53:21作者:史锋燃Gardner

本文围绕 CPython 的 marshal 数据序列化 C API 展开:系统介绍 Py_MARSHAL_VERSION 宏与 8 个 PyMarshal_* 函数(写文件、写内存、读文件、读内存)的签名、参数语义与错误处理约定,并结合 Python/marshal.c 的底层实现、[pythonrun.c](https://gitcode.com/GitHub_Trending/cp/cpython/blob/486b000c6c19c555f03b481f735f4dec498f0f67/Python/pythonrun.c?utm_source=gitcode_repo_files).pyc 字节码导入的真实调用链,以及 Misc/stable_abi.toml 中的稳定 ABI 状态,说明这些 API 在 CPython 源码中的实际用途与使用边界。读完本文后,你可以在 C 扩展或嵌入解释器的程序中直接序列化/反序列化 Python 对象,并理解 CPython 内部是如何用这套 API 加载 .pyc 文件与冻结模块的。

总体定位:与 Python 层 marshal 模块共用一种二进制格式

CPython 的 C API 提供了一组名为 "Data marshalling support" 的例程,它们允许 C 代码以与 Python 层 marshal 模块完全相同的数据格式处理序列化对象:既有把数据写入序列化格式(marshal)的函数,也有把数据读回(de-marshal)的函数。需要特别注意两点使用约束(出自官方文档 Doc/c-api/marshal.rst):

  • 文件必须使用二进制模式打开FILE* 参数要求流以 "rb"/"wb" 方式打开,任何文本模式下的换行转换都会破坏格式;
  • 数值统一采用小端序(least significant byte first,最低位字节在前),这保证了序列化结果与本机字节序无关,可在不同架构机器之间传输。

该模块支持多个数据格式版本。版本演进历史记录在 Doc/library/marshal.rst 中,从源码中的类型标签定义也可以看到各版本引入的新特性:

版本 引入版本 新特性
1 Python 2.4 共享 intern 字符串(TYPE_INTERNED 't'
2 Python 2.5 浮点数二进制表示(TYPE_BINARY_FLOAT 'g'
3 Python 3.4 对象引用(TYPE_REF 'r'),支持递归/共享容器
4 Python 3.4 短字符串高效表示(TYPE_SHORT_ASCII 'z'TYPE_SMALL_TUPLE ')'
5 Python 3.14 支持 sliceTYPE_SLICE ':'
6 Python 3.15 支持 frozendictTYPE_FROZENDICT '}'

上述类型标签与版本演进注释均定义在 Python/marshal.c。当前仓库中的格式版本号为 6,由头文件中的宏直接给出:

// Include/cpython/marshal.h
#define Py_MARSHAL_VERSION 6

Py_MARSHAL_VERSION 宏

  • 声明位置:Include/cpython/marshal.h,对外通过 Include/marshal.h Py_LIMITED_API 模式下引入;
  • 含义:当前 marshal 数据格式版本号,与 Python 层的 marshal.version 常量一致;
  • 用法约定:所有带 version 参数的写入/读取函数都应以该宏(或你显式需要的旧版本号)作为格式参数。CPython 内部调用它时一律传 Py_MARSHAL_VERSION,例如冻结模块工具 Programs/_freeze_module.c
PyObject *marshalled = PyMarshal_WriteObjectToString(code, Py_MARSHAL_VERSION);

写入方向的三个函数

PyMarshal_WriteLongToFile:向文件写入 32 位 long

void PyMarshal_WriteLongToFile(long value, FILE *file, int version);
  • 把 C long 整数 value 以 marshal 格式写入 file
  • 关键限制:无论原生 long 是多少位,实际只写入最低 32 位。这一点在实现中一目了然——Python/marshal.cw_long 辅助函数固定按 4 个字节输出:
static void
w_long(long x, WFILE *p)
{
    w_byte((char)( x      & 0xff), p);
    w_byte((char)((x>> 8) & 0xff), p);
    w_byte((char)((x>>16) & 0xff), p);
    w_byte((char)((x>>24) & 0xff), p);
}
  • version 指定文件格式版本;
  • 错误处理:函数无返回值,失败时设置异常指示器,调用方必须用 PyErr_Occurred() 检查;
  • 典型用途:.pyc 文件头就是若干个 32 位 long(magic number、flags、时间戳、源文件大小)。CPython 加载 .pyc 时正是用它读回头部的 magic:Python/pythonrun.cmagic = PyMarshal_ReadLongFromFile(fp); 与之配对(写入侧同理)。

PyMarshal_WriteObjectToFile:将 Python 对象写入文件

void PyMarshal_WriteObjectToFile(PyObject *value, FILE *file, int version);
  • 把任意受支持的 Python 对象 value marshal 到 file
  • version 指定文件格式;
  • 同样无返回值,失败时设置错误指示器,需用 PyErr_Occurred() 检查。

底层实现与 dumps/dump 共用同一条写路径:Python/marshal.c 中的 WFILE 结构体通过 fp 是否为 NULL 来区分"写文件"与"写内存字节串"两种模式(fp != NULL 时经 w_flush 落盘,否则在 bytes 对象缓冲区中动态扩容):

typedef struct {
    FILE *fp;
    int error;  /* see WFERR_* values */
    int depth;
    PyObject *str;
    char *ptr;
    const char *end;
    char *buf;
    _Py_hashtable_t *hashtable;
    int version;
    int allow_code;
} WFILE;

其中 version 字段决定字符串短编码、引用(version 3+ 的递归支持)等新特性是否启用,allow_code 则控制是否允许序列化 code 对象。

PyMarshal_WriteObjectToString:返回 bytes 对象

PyObject* PyMarshal_WriteObjectToString(PyObject *value, int version);
  • 返回一个 bytes 对象,内容即 value 的 marshal 表示;调用方负责释放返回值的引用;
  • 成功时返回新对象,失败时返回 NULL 并设置异常;
  • 这是 C 扩展里最常用的序列化入口,也是稳定 ABI(Stable ABI)成员之一——见 Misc/stable_abi.toml 与 ctypes 稳定 ABI 测试 Lib/test/test_stable_abi_ctypes.py,即使在 Py_LIMITED_API 模式下也可调用。

CPython 中一个真实的生产级用例是冻结模块(frozen modules)机制:构建期 Programs/_freeze_module.c 用该函数把编译好的 code 对象 marshal 成字节串嵌入可执行文件;运行期 Python/import.c 用配对函数 PyMarshal_ReadObjectFromString 读回,从而无需磁盘上的 .pyc 即可导入模块:

// Python/import.c
PyObject *co = PyMarshal_ReadObjectFromString(info->data, info->size);

路径配置模块的启动数据同样走这条路:Modules/getpath.creturn PyMarshal_ReadObjectFromString(...)

读取方向的五个函数

PyMarshal_ReadLongFromFile:从文件读 32 位 long

long PyMarshal_ReadLongFromFile(FILE *file);
  • 从以读方式打开的 FILE* 数据流中读回一个 C long
  • 无论原生 long 多大,最多只能读回 32 位值(与写侧的 32 位限制对称);
  • 出错时设置 EOFError 并返回 -1

这是 .pyc 头部解析的配套函数。完整调用链见 Python/pythonrun.c:先读 magic 校验版本,再连续三次读 long 消费 flags、时间戳、源大小字段,最后用 PyMarshal_ReadLastObjectFromFile 读代码对象:

magic = PyMarshal_ReadLongFromFile(fp);
...
(void) PyMarshal_ReadLongFromFile(fp);
(void) PyMarshal_ReadLongFromFile(fp);
(void) PyMarshal_ReadLongFromFile(fp);
...
v = PyMarshal_ReadLastObjectFromFile(fp);

PyMarshal_ReadShortFromFile:从文件读 16 位 short

int PyMarshal_ReadShortFromFile(FILE *file);
  • FILE* 数据流读回一个 C short
  • 只能读回 16 位值,与原生 short 宽度无关;
  • 出错时设置 EOFError 并返回 -1

实现位于 Python/marshal.cReadLongPython/marshal.c),两者都通过内部的 r_short/r_long 辅助按小端序从流中取字节。

PyMarshal_ReadObjectFromFile:从文件流式反序列化一个对象

PyObject* PyMarshal_ReadObjectFromFile(FILE *file);
  • FILE* 数据流中读回一个 Python 对象;
  • 出错时设置 EOFErrorValueErrorTypeError,并返回 NULL

该函数是"逐字节流式读取"的实现:每次需要数据时从文件读一块缓冲区,适合文件后面还要继续读取其他内容的场景。

PyMarshal_ReadLastObjectFromFile:一次读完,性能更激进

PyObject* PyMarshal_ReadLastObjectFromFile(FILE *file);
  • 功能与 PyMarshal_ReadObjectFromFile 相同,但假定你不会再从这个文件读取任何东西
  • 因此它会把剩余文件数据一次性全部加载进内存,让反序列化直接在内存缓冲上进行,而不必逐字节从文件读取;
  • 官方文档明确警告:只有确定后续不会再读该文件时才使用这个变体;
  • 出错时设置 EOFErrorValueErrorTypeError,返回 NULL

这正是它被选用于 .pyc 导入的原因:import 机制读完全文件头之后,文件中剩下的只有唯一的 code 对象,读后不再访问,于是 Python/pythonrun.c 使用这个"读到最后"的高性能变体。

PyMarshal_ReadObjectFromString:从内存字节缓冲反序列化

PyObject* PyMarshal_ReadObjectFromString(const char *data, Py_ssize_t len);
  • data 指向的、长度为 len 字节的缓冲中读回一个 Python 对象;
  • 出错时设置 EOFErrorValueErrorTypeError,返回 NULL
  • PyMarshal_WriteObjectToString 一样属于稳定 ABI 函数,是 C 扩展中处理内存序列化数据的标准入口;
  • CPython 内部使用极广:冻结模块导入(Python/import.c)、启动路径数据(Modules/getpath.c)、跨子解释器共享对象时把对象经 marshal 字节串传递(Python/crossinterp.c_PyMarshal_ReadObjectFromXIData 内部即调用它),以及 multiprocessing 等模块的基础设施。

底层实现机制:写路径、错误模型与递归深度保护

理解上述函数在什么情况下会失败,需要看一下 Python/marshal.c 的实现细节:

统一的 WFILE 双模式写路径

写入类函数(PyMarshal_WriteObjectToFilePyMarshal_WriteObjectToString,分别定义在 Python/marshal.cPython/marshal.c)共享同一个 w_object 递归序列化器。WFILE 结构的 error 字段记录 5 种内部错误码(Python/marshal.c):

#define WFERR_OK 0
#define WFERR_UNMARSHALLABLE 1        // 遇到不支持的类型
#define WFERR_NESTEDTOODEEP 2         // 嵌套过深
#define WFERR_NOMEMORY 3              // 内存分配失败
#define WFERR_CODE_NOT_ALLOWED 4     // 未允许写 code 对象
  • WFERR_UNMARSHALLABLE 对应 Python 层抛出的 ValueError(不支持的类型);
  • 容器大小超过 32 位(SIZE32_MAX)会被判定为不可序列化,见 Python/marshal.cW_SIZE 宏;
  • 由于写操作采用"先标记错误、延迟抛异常"的模式,PyMarshal_WriteObjectToFile 在失败后才会把内部错误码翻译成 PyErr,因此文档要求用 PyErr_Occurred() 检查返回值之外的状态。

递归深度上限:防止恶意或病态数据打爆栈

反序列化是深度递归的。CPython 用一个平台相关的高水位 MAX_MARSHAL_STACK_DEPTH 限制嵌套深度,达到上限即抛异常而不是继续递归(Python/marshal.c):

#if defined(MS_WINDOWS)
#  define MAX_MARSHAL_STACK_DEPTH 1000
#elif defined(__wasi__)
#  define MAX_MARSHAL_STACK_DEPTH 1500
#elif defined(__APPLE__) && defined(TARGET_OS_IPHONE) && TARGET_OS_IPHONE
#  define MAX_MARSHAL_STACK_DEPTH 1500
#else
#  define MAX_MARSHAL_STACK_DEPTH 2000
#endif

其中 Windows 上特意调低到 1000,是出于历史上 r_object 在 Windows PGO 构建中栈过量分配导致栈溢出的缺陷防护(源码注释中引向了 bpo-33720)。实践含义:用 PyMarshal_ReadObjectFromFile / PyMarshal_ReadObjectFromString 处理外部来源的数据时,极深嵌套的数据会被拒绝而不是导致崩溃——但正如 Python 层文档反复强调的,marshal 不是为了防御恶意数据设计的格式,不要用它反序列化不可信来源的内容。

版本字段如何影响读写行为

WFILE.version 字段直接控制输出编码:例如 version 3+ 才允许输出 TYPE_REF(循环引用),version 4+ 才允许 TYPE_SHORT_ASCII/TYPE_SMALL_TUPLE 紧凑编码。读侧对称地按字节流中的类型标签分派到对应读取分支。因此 version 参数不是"可选的元数据",而是真正改变二进制内容的开关——跨版本兼容读旧数据时,应传旧版本号;写新数据传 Py_MARSHAL_VERSION

CPython 内部与测试中对这组 API 的使用证据

这套 API 不是孤立的公开接口,CPython 源码本身就是它最大的用户:

  • .pyc 导入Python/pythonrun.cPyMarshal_ReadLongFromFile 读文件头四个 32 位字段、PyMarshal_ReadLastObjectFromFile 读 code 对象;
  • 冻结模块:构建期 Programs/_freeze_module.cPyMarshal_WriteObjectToString 序列化 code,运行期 Python/import.cPyMarshal_ReadObjectFromString 反序列化;
  • 启动路径解析Modules/getpath.c 从内存缓冲读取嵌入的启动数据结构;
  • 子解释器对象共享Python/crossinterp.c 把对象 marshal 成字节串,跨解释器边界传递后再 unmarshal;
  • C API 测试_testcapi 模块为每个函数提供了可被单元测试调用的包装——Modules/_testcapimodule.cPyMarshal_WriteLongToFilePyMarshal_WriteObjectToFilePyMarshal_ReadShortFromFilePyMarshal_ReadLongFromFilePyMarshal_ReadLastObjectFromFilePyMarshal_ReadObjectFromFile 全部出现,对应 Python 层测试 Lib/test/test_marshal.py 验证了往返一致性;
  • 稳定 ABI 保障Misc/stable_abi.tomlPyMarshal_ReadObjectFromStringPyMarshal_WriteObjectToString 列为受限 API 函数,PC/python3dll.c 负责将其从共享库导出。

使用小结与注意事项

  1. 二进制模式:所有 FILE* 参数必须以二进制模式打开,数值固定小端序;
  2. 32/16 位截断PyMarshal_WriteLongToFilePyMarshal_ReadLongFromFile 只处理最低 32 位,PyMarshal_ReadShortFromFile 只处理 16 位——与原生类型宽度无关,跨平台传输时这是有意为之;
  3. 错误检查:写入函数无返回值,必须 PyErr_Occurred()ReadLong/ReadShort 出错返回 -1 + EOFError;对象读取函数出错返回 NULL + EOFError/ValueError/TypeError
  4. 流式 vs 读完:文件后段还要继续读,用 PyMarshal_ReadObjectFromFile;确定是文件最后一个对象,用 PyMarshal_ReadLastObjectFromFile 换取一次性的内存加载性能(CPython 的 .pyc 导入正是后者);
  5. 稳定 ABIPyMarshal_WriteObjectToString / PyMarshal_ReadObjectFromString 可安全用于 Py_LIMITED_API 扩展;
  6. 安全边界:marshal 格式不是安全边界,且 code 对象序列化结果不具备跨 Python 版本的兼容承诺(code 对象格式随解释器演进,用错误版本反序列化是未定义行为)——通用持久化应选 pickle,marshal 的定位是 CPython 内部的字节码与结构化数据载体。

参考文档与源码:Doc/c-api/marshal.rst(本文核心依据)、Doc/library/marshal.rst(Python 层格式与版本说明)、Include/cpython/marshal.h(声明与 Py_MARSHAL_VERSION)、Python/marshal.c(实现)。

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