CPython 数据序列化 C API 全解:用 PyMarshal_* 函数族读写 marshal 二进制格式
本文围绕 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 | 支持 slice(TYPE_SLICE ':') |
| 6 | Python 3.15 | 支持 frozendict(TYPE_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.c 的w_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.c 中magic = PyMarshal_ReadLongFromFile(fp);与之配对(写入侧同理)。
PyMarshal_WriteObjectToFile:将 Python 对象写入文件
void PyMarshal_WriteObjectToFile(PyObject *value, FILE *file, int version);
- 把任意受支持的 Python 对象
valuemarshal 到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.c 中 return PyMarshal_ReadObjectFromString(...)。
读取方向的五个函数
PyMarshal_ReadLongFromFile:从文件读 32 位 long
long PyMarshal_ReadLongFromFile(FILE *file);
- 从以读方式打开的
FILE*数据流中读回一个 Clong; - 无论原生
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*数据流读回一个 Cshort; - 只能读回 16 位值,与原生
short宽度无关; - 出错时设置
EOFError并返回-1。
实现位于 Python/marshal.c(ReadLong 在 Python/marshal.c),两者都通过内部的 r_short/r_long 辅助按小端序从流中取字节。
PyMarshal_ReadObjectFromFile:从文件流式反序列化一个对象
PyObject* PyMarshal_ReadObjectFromFile(FILE *file);
- 从
FILE*数据流中读回一个 Python 对象; - 出错时设置
EOFError、ValueError或TypeError,并返回NULL。
该函数是"逐字节流式读取"的实现:每次需要数据时从文件读一块缓冲区,适合文件后面还要继续读取其他内容的场景。
PyMarshal_ReadLastObjectFromFile:一次读完,性能更激进
PyObject* PyMarshal_ReadLastObjectFromFile(FILE *file);
- 功能与
PyMarshal_ReadObjectFromFile相同,但假定你不会再从这个文件读取任何东西; - 因此它会把剩余文件数据一次性全部加载进内存,让反序列化直接在内存缓冲上进行,而不必逐字节从文件读取;
- 官方文档明确警告:只有确定后续不会再读该文件时才使用这个变体;
- 出错时设置
EOFError、ValueError或TypeError,返回NULL。
这正是它被选用于 .pyc 导入的原因:import 机制读完全文件头之后,文件中剩下的只有唯一的 code 对象,读后不再访问,于是 Python/pythonrun.c 使用这个"读到最后"的高性能变体。
PyMarshal_ReadObjectFromString:从内存字节缓冲反序列化
PyObject* PyMarshal_ReadObjectFromString(const char *data, Py_ssize_t len);
- 从
data指向的、长度为len字节的缓冲中读回一个 Python 对象; - 出错时设置
EOFError、ValueError或TypeError,返回NULL; - 与
PyMarshal_WriteObjectToString一样属于稳定 ABI 函数,是 C 扩展中处理内存序列化数据的标准入口; - CPython 内部使用极广:冻结模块导入(Python/import.c)、启动路径数据(Modules/getpath.c)、跨子解释器共享对象时把对象经 marshal 字节串传递(Python/crossinterp.c 的
_PyMarshal_ReadObjectFromXIData内部即调用它),以及multiprocessing等模块的基础设施。
底层实现机制:写路径、错误模型与递归深度保护
理解上述函数在什么情况下会失败,需要看一下 Python/marshal.c 的实现细节:
统一的 WFILE 双模式写路径
写入类函数(PyMarshal_WriteObjectToFile 与 PyMarshal_WriteObjectToString,分别定义在 Python/marshal.c 与 Python/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.c 的W_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.c 用PyMarshal_ReadLongFromFile读文件头四个 32 位字段、PyMarshal_ReadLastObjectFromFile读 code 对象;- 冻结模块:构建期 Programs/_freeze_module.c 用
PyMarshal_WriteObjectToString序列化 code,运行期 Python/import.c 用PyMarshal_ReadObjectFromString反序列化; - 启动路径解析:Modules/getpath.c 从内存缓冲读取嵌入的启动数据结构;
- 子解释器对象共享:Python/crossinterp.c 把对象 marshal 成字节串,跨解释器边界传递后再 unmarshal;
- C API 测试:
_testcapi模块为每个函数提供了可被单元测试调用的包装——Modules/_testcapimodule.c 中PyMarshal_WriteLongToFile、PyMarshal_WriteObjectToFile、PyMarshal_ReadShortFromFile、PyMarshal_ReadLongFromFile、PyMarshal_ReadLastObjectFromFile、PyMarshal_ReadObjectFromFile全部出现,对应 Python 层测试 Lib/test/test_marshal.py 验证了往返一致性; - 稳定 ABI 保障:Misc/stable_abi.toml 将
PyMarshal_ReadObjectFromString与PyMarshal_WriteObjectToString列为受限 API 函数,PC/python3dll.c 负责将其从共享库导出。
使用小结与注意事项
- 二进制模式:所有
FILE*参数必须以二进制模式打开,数值固定小端序; - 32/16 位截断:
PyMarshal_WriteLongToFile与PyMarshal_ReadLongFromFile只处理最低 32 位,PyMarshal_ReadShortFromFile只处理 16 位——与原生类型宽度无关,跨平台传输时这是有意为之; - 错误检查:写入函数无返回值,必须
PyErr_Occurred();ReadLong/ReadShort出错返回-1+EOFError;对象读取函数出错返回NULL+EOFError/ValueError/TypeError; - 流式 vs 读完:文件后段还要继续读,用
PyMarshal_ReadObjectFromFile;确定是文件最后一个对象,用PyMarshal_ReadLastObjectFromFile换取一次性的内存加载性能(CPython 的.pyc导入正是后者); - 稳定 ABI:
PyMarshal_WriteObjectToString/PyMarshal_ReadObjectFromString可安全用于Py_LIMITED_API扩展; - 安全边界: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(实现)。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00