☰
KBEngine 内置 Python 的 marshal 模块:对象二进制序列化与 .pyc 字节码加载全解析
2026/10/17 2:30:50 网站建设 项目流程
  • 游戏开发
  • 后端

【免费下载链接】kbengine

A MMOG engine of server.

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

导读

marshal是 Python 标准库中专门用于把 Python 对象序列化为二进制字节流、并能反向还原的内部模块。它的使命非常单一且特殊:支撑 Python 解释器读写.pyc伪编译字节码文件,因此它不是通用的持久化或跨语言交换方案。本篇文章以 KBEngine 仓库内嵌的 CPython 3.x 文档(kbe/src/lib/python/Doc/library/marshal.rst)为骨架,结合仓库内该版本解释器的真实 C 实现(kbe/src/lib/python/Python/marshal.c)、C API 文档(kbe/src/lib/python/Doc/c-api/marshal.rst)与单元测试(kbe/src/lib/python/Lib/test/test_marshal.py),完整讲解marshal的定位、支持的数据类型、版本演进、四个核心函数、底层格式设计、.pyc加载链路以及安全边界。读完本文,你将能正确区分marshal与pickle/shelve的适用场景,掌握四个 API 的精确行为与异常语义,并能读懂.pyc文件被解释器反序列化的内部调用链。


一、marshal 是什么:内部专用、面向 .pyc 的序列化器

marshal模块包含一组函数,可读写 Python 值的二进制表示。其格式是Python 专属(specific to Python),但与机器架构无关(independent of machine architecture):例如你可以在 PC 上把一个 Python 值写入文件,把文件传输到 Sun 工作站再读回来,结果一致。

这里有一个关键设计约束(文档原文明确指出):格式细节是刻意不公开的(undocumented on purpose),它可能在 Python 版本之间发生变化(虽然很少变)。原因正如marshal.c文件头注释所写:

/* Write Python objects to files and read them back. This is primarily intended for writing and reading compiled Python code, even though dicts, lists, sets and frozensets, not commonly seen in code objects, are supported. Version 3 of this protocol properly supports circular links and sharing. */

即:marshal的首要用途是读写 Python 模块的"伪编译"代码(.pyc文件中的 code object),虽然它也支持 dict、list、set、frozenset 等容器。因此 Python 维护者保留在不兼容的方式下修改 marshal 格式的权利。

模块命名溯源

脚注给出了命名来历:该名字源自 Modula-3 设计者(及其他语言社区)的术语——"marshalling" 指以自包含形式搬运数据。严格来说,"to marshal" 是把数据从内部形式转换为外部形式(例如放进 RPC 缓冲区),"unmarshalling" 是反向过程。这与marshal承担"解释器内部数据往返"的职责完全对应。

与 pickle / shelve 的分工

维度marshalpickle/shelve
定位内部模块,支撑.pyc读写通用持久化 / RPC 对象传输
版本兼容可能被不兼容地修改,无保证版本独立性有保证
性能与 pickle 相当与 marshal 相当
支持类型范围窄(见下一节)宽得多
安全性不安全,禁止反序列化不可信数据同样需警惕,但文档此处强调 marshal 更明确

文档给出的结论非常直接:如果你要做对象的序列化/反序列化,请用pickle——性能相当、版本独立有保证、支持的对象范围大得多。shelve则是基于 pickle 的持久化字典。


二、支持的数据类型与安全警告

2.1 可以 marshal 的类型

marshal只支持"值独立于某次特定 Python 运行"的对象,文档列出的完整清单如下:

  • 布尔值(booleans)
  • 整数(integers)
  • 浮点数(floating point numbers)
  • 复数(complex numbers)
  • 字符串(strings)
  • 字节串(bytes)
  • 字节数组(bytearrays)
  • 元组(tuples)
  • 列表(lists)
  • 集合(sets)
  • 冻结集合(frozensets)
  • 字典(dictionaries)
  • 代码对象(code objects)——这是最核心的类型,正是.pyc存储的东西

容器类型(tuple/list/set/frozenset/dict)只有当其内部包含的值本身也是受支持类型时才受支持。此外,三个单例也可以被 marshal/unmarshal:None、Ellipsis、StopIteration。

注意限制:对于格式版本低于 3的情况,递归的列表、集合和字典无法写入(会引发问题,详见后文"版本演进")。

2.2 不支持的类型的后果

如果被 dump 的值(或其内部某个对象)属于不支持的动态类型(典型如自定义类的实例、lambda、生成器等),dump会抛出ValueError——但垃圾数据也会被写入文件,之后load无法正确读回该对象。反过来,如果某个含不支持类型的对象曾被dump写出,load读到该处时会把None替换为那个无法反序列化的类型(这一点在marshal_load的 C 实现 docstring 中亦有明确说明,见kbe/src/lib/python/Python/marshal.c)。

2.3 安全警告(务必牢记)

marshal模块不保证能抵御错误或恶意构造的数据。永远不要反序列化来自不可信或未认证来源的数据。

这条警告并非泛泛而谈——从实现看,marshal的读取路径会直接根据输入字节分配内存、构造对象并重建引用关系,恶意数据可能引发解释器层面的问题。KBEngine 服务端通常只在本机可信环境中由解释器自己加载.pyc,但任何把外部输入喂给marshal.loads的代码都是危险的。


三、四个核心函数:dump / load / dumps / loads

marshal模块定义了读写文件的函数,也定义了操作 bytes-like 对象的函数。仓库的 C 实现(marshal_methods[]方法表,见kbe/src/lib/python/Python/marshal.c)恰好注册了这四者:MARSHAL_DUMP_METHODDEF、MARSHAL_LOAD_METHODDEF、MARSHAL_DUMPS_METHODDEF、MARSHAL_LOADS_METHODDEF。

3.1 marshal.dump(value, file[, version])

把value写入一个已打开的文件对象。约束:

  • value必须是受支持类型;
  • file必须是可写的二进制文件(以"wb"模式打开,文本模式会失败);
  • 若值包含不支持的类型,抛ValueError,但垃圾数据也会写入文件,之后load无法正确读回;
  • 可选参数version指定要使用的数据格式。

从 C 实现看(marshal_dump_impl),dump内部其实是先调用PyMarshal_WriteObjectToString把对象整体序列化为一个 bytes 对象,再调用file.write(s)一次写入——因此它对 file 对象的要求本质上是"有一个能接受 bytes 的write方法"。

3.2 marshal.load(file)

从已打开的文件读取一个值并返回。约束:

  • file必须是可读的二进制文件;
  • 如果读不到有效值(例如数据来自另一个不兼容 marshal 格式的 Python 版本),抛出EOFError、ValueError或TypeError;
  • 补充说明(文档 note):如果文件里是曾被dump写出的含不支持类型的对象,load会用None替代该不可反序列化的类型。

C 实现的marshal_load会先调用file.read(0)探测read方法存在且返回 bytes(返回类型不对时抛TypeError: file.read() returned not bytes but ...),然后通过readinto风格读取真实数据,由read_object反序列化。

3.3 marshal.dumps(value[, version])

返回一个bytes对象,其内容与dump(value, file)会写入文件的字节完全一致。约束:

  • value必须是受支持类型;
  • 值含不支持类型时抛ValueError;
  • 可选参数version指定数据格式。

它直接映射到 C 函数PyMarshal_WriteObjectToString(marshal_dumps_impl只是一行调用),内部先把数据写入一个预分配的 bytes 缓冲区(初始容量 50 字节,超出后按 12.5% 比例增长,见w_reserve),最后精确裁剪到实际长度。

3.4 marshal.loads(bytes)

把 bytes-like 对象转换回一个值。约束:

  • 找不到有效值时抛EOFError、ValueError或TypeError;
  • 输入中多余的字节会被忽略(即只要开头能解析出一个完整对象,尾部垃圾不影响结果)。

它直接映射到PyMarshal_ReadObjectFromString,读取时走"快速路径":直接在输入缓冲区上按指针滑动解析(见r_string的 fast path),这也是marshal.loads比load更快的原因之一(Misc/HISTORY中亦有 "Speed up marshal.loads()" 的改进记录)。

3.5 一个完整的可运行示例

import marshal, io, types # dumps / loads:bytes 往返 data = marshal.dumps({"name": "KBEngine", "level": 42, "flags": [True, None]}) obj = marshal.loads(data) assert obj == {"name": "KBEngine", "level": 42, "flags": [True, None]} # dump / load:二进制文件往返 with open("/tmp/obj.bin", "wb") as f: marshal.dump(obj, f) with open("/tmp/obj.bin", "rb") as f: obj2 = marshal.load(f) assert obj == obj2 # 指定版本写入(低版本无法表达递归结构) marshal.dumps([1, 2, 3], 2) # 使用 version 2 格式 marshal.dumps({"a": 1}, 4) # 使用当前版本 4 # code object 是 marshal 的"主职" co = compile("x = 1 + 2", "<demo>", "exec") co2 = marshal.loads(marshal.dumps(co)) assert type(co2) is types.CodeType

四、version 常量:格式版本演进与递归支持

模块定义了常量marshal.version,指示模块当前使用的格式版本。文档给出的演进史如下:

版本引入的特性
0历史格式(historical format)
1共享 interned 字符串(shares interned strings)
2浮点数改用二进制格式(binary format for floating point numbers)
3新增对象实例化与递归支持(object instancing and recursion)
4当前版本(the current version is 4)

关键理解点:

  • 版本 3 是递归的分水岭:文档明确"for formatversionlower than 3, recursive lists, sets and dictionaries cannot be written"。也就是说,只有 version ≥ 3 才能序列化自引用/循环引用的容器。marshal.c头部注释也印证了这一点:"Version 3 of this protocol properly supports circular links and sharing"(正确地支持循环引用与共享)。
  • 在仓库内嵌的这份解释器中,Include/marshal.h第 10 行定义#define Py_MARSHAL_VERSION 4,与文档"当前版本是 4"完全一致;模块初始化时通过PyModule_AddIntConstant(mod, "version", Py_MARSHAL_VERSION)暴露给 Python 层。
  • C API 文档Doc/c-api/marshal.rst中仍写着"currently 2",那是文档与实现之间的历史差异——以实际仓库代码为准,当前Py_MARSHAL_VERSION是 4。

另外,dump/dumps的version参数在 C 层有默认值:Clinic 定义中version: int(c_default="Py_MARSHAL_VERSION"),即不传时自动使用当前版本 4。


五、源码级实现剖析:格式、类型标签与栈深度保护

5.1 内部格式的"类型标签"设计

虽然格式细节"刻意不公开",但从kbe/src/lib/python/Python/marshal.c第 41-73 行的#define可以窥见其设计:每个序列化对象以一个单字节类型标签开头,随后是数据。部分标签(ASCII 字符常量)如下:

TYPE_NULL '0' TYPE_NONE 'N' TYPE_FALSE 'F' TYPE_TRUE 'T' TYPE_STOPITER 'S' TYPE_ELLIPSIS '.' TYPE_INT 'i' TYPE_INT64 'I'(仅向后兼容,不再生成) TYPE_FLOAT 'f' TYPE_BINARY_FLOAT 'g' TYPE_COMPLEX 'x' TYPE_BINARY_COMPLEX 'y' TYPE_LONG 'l' TYPE_STRING 's' TYPE_INTERNED 't' TYPE_REF 'r' TYPE_TUPLE '(' TYPE_LIST '[' TYPE_DICT '{' TYPE_CODE 'c' TYPE_UNICODE 'u' TYPE_UNKNOWN '?' TYPE_SET '<' TYPE_FROZENSET '>' TYPE_ASCII 'a' TYPE_ASCII_INTERNED 'A' TYPE_SMALL_TUPLE ')' TYPE_SHORT_ASCII 'z' TYPE_SHORT_ASCII_INTERNED 'Z' FLAG_REF '\x80' /* 与类型组合,表示"把此对象加入引用索引" */

几点值得注意的实现事实:

  • 数值按最低有效字节在前(little-endian)存储,C API 文档明确"Numeric values are stored with the least significant byte first";
  • 版本 4 在写字符串时做了两个优化(marshal.c中w_object相关分支):ASCII 字符串走TYPE_SHORT_ASCII_INTERNED等紧凑路径;长度小于 256 的短字符串也有专门编码;
  • FLAG_REF '\x80'与类型字节按位组合,是版本 3+ 实现"对象共享/递归引用"的机制——写入时把对象登记进哈希表索引,读取时通过TYPE_REF重建共享引用;
  • 大整数(long)与浮点/复数的二进制格式(TYPE_BINARY_FLOAT 'g'、TYPE_BINARY_COMPLEX 'y')就是版本 2 引入的"浮点数二进制格式"。

5.2 栈深度保护:MAX_MARSHAL_STACK_DEPTH

marshal的读写是递归实现,过深的嵌套会撑爆解释器栈。因此实现中定义了高水位保护(见marshal.c第 24-39 行):

#if defined(MS_WINDOWS) #define MAX_MARSHAL_STACK_DEPTH 1000 #else #define MAX_MARSHAL_STACK_DEPTH 2000 #endif

当对象嵌套深度超过该阈值时,写入方以WFERR_NESTEDTOODEEP报错,最终向 Python 层抛ValueError: object too deeply nested to marshal;读取方在r_object中同样检查深度。Windows 上阈值减半(1000)是因为 PGO 构建下r_object会过度分配栈导致栈溢出(源码注释引用了 bpo-33720)。这解释了"不能序列化任意深度嵌套对象"这一现实限制。

5.3 错误码体系

写入端内部使用WFERR_*错误码(marshal.c第 75-78 行):

WFERR_OK 0 WFERR_UNMARSHALLABLE 1 -> ValueError: unmarshallable object WFERR_NESTEDTOODEEP 2 -> ValueError: object too deeply nested to marshal WFERR_NOMEMORY 3 -> MemoryError

PyMarshal_WriteObjectToString收尾时按错误码翻译成对应 Python 异常(见该函数末尾),这就是dump/dumps抛ValueError的内部来源。


六、marshal 的实战主职:.pyc 字节码加载链路

文档点明 marshal 存在的核心原因:读写.pyc伪编译文件。仓库中这一链路有完整的源码证据,可以层层追踪:

  1. 导入时从 .pyc 读回 code object:kbe/src/lib/python/Lib/importlib/_bootstrap_external.py的_compile_bytecode()中:

    code = marshal.loads(data) if isinstance(code, _code_type): ... return code else: raise ImportError('Non-code object in {!r}'.format(bytecode_path), ...)

    可见:.pyc文件在 16 字节头(MAGIC + flags + 源文件哈希/时间戳等)之后,正文就是一个marshal.loads可解析的 code object;若反序列化结果不是 code object,则抛ImportError拒绝加载。

  2. 写入 .pyc 时生成 marshal 数据:_code_to_timestamp_pyc等函数先写入MAGIC_NUMBER与头部字段,随后把编译产物序列化进去(对应写方向使用marshal.dumps/PyMarshal_WriteObjectToString)。

  3. 冻结模块也走 marshal:kbe/src/lib/python/Python/import.c中,冻结模块(frozen module,编译进解释器的模块)的加载正是通过PyMarshal_ReadObjectFromString((const char *)p->code, size)完成的(见get_frozen_object,第 1271 行),而kbe/src/lib/python/Programs/_freeze_importlib.c用PyMarshal_WriteObjectToString(code, Py_MARSHAL_VERSION)把导入机制模块预序列化进二进制。这一对 C API 是 marshal 在解释器底层的直接使用者。

  4. IDLE 远程执行也依赖 marshal:kbe/src/lib/python/Lib/idlelib/rpc.py第 47 行co = marshal.loads(ms),把远程传来的 code object 字节流还原为本地可执行代码对象——这是"值独立于特定运行"特性的典型应用(跨进程传输 code object)。

6.1 C API 全貌(供扩展层使用)

除 Python 层四个函数外,C 层还暴露了更多工具(kbe/src/lib/python/Doc/c-api/marshal.rst):

  • PyMarshal_WriteLongToFile(long value, FILE *file, int version)——只写 value 的低 32 位;
  • PyMarshal_WriteObjectToFile(PyObject *value, FILE *file, int version);
  • PyMarshal_WriteObjectToString(PyObject *value, int version)——返回 bytes;
  • PyMarshal_ReadLongFromFile(FILE *)——只读 32 位,错误时设EOFError并返回-1;
  • PyMarshal_ReadShortFromFile(FILE *)——只读 16 位,错误时设EOFError并返回-1;
  • PyMarshal_ReadObjectFromFile(FILE *)——错误时设EOFError/ValueError/TypeError并返回NULL;
  • PyMarshal_ReadLastObjectFromFile(FILE *)——变体,假定文件不再读其他对象,会激进地把整个文件读入内存再反序列化(更快,但只适合"确定不再读取"的场景);
  • PyMarshal_ReadObjectFromString(const char *data, Py_ssize_t len)——对应loads。

这些接口说明:任何需要和解释器字节码格式打交道的原生扩展,都可以复用 marshal 的同一套格式。


七、测试佐证:test_marshal.py 覆盖的行为边界

仓库自带完整的kbe/src/lib/python/Lib/test/test_marshal.py(571 行),可直接作为行为契约的权威证据,主要覆盖:

  • 类型往返:HelperMixin.helper对任意样本执行marshal.loads(marshal.dumps(sample, *extra))并断言相等,同时验证文件版dump/load往返;
  • 大整数:test_ints用sys.maxsize ** 2级别的大整数验证;test_int64手工构造TYPE_INT64(b'I' + int.to_bytes(base, 8, 'little', signed=True))验证 64 位整数兼容读取;
  • 布尔/单例:test_bool.py中assertIs(marshal.loads(marshal.dumps(True)), True)验证布尔值往返保持同一对象;
  • code object:test_code系列对compile(...)产物、含递归引用的 code object 元组做往返验证;
  • 错误输入:marshal.loads(b'0')、marshal.loads(b'f')、截断数据(marshal.dumps(2**65)[:-1])会抛异常;marshal.loads(b'')抛EOFError(见test_exceptions.py);
  • 版本参数:marshal.loads(marshal.dumps(5, 0))与marshal.loads(marshal.dumps(5, 1))都等于 5;
  • 格式合法性:run_tests(100, marshal.loads)用随机数据压力验证生成的 marshal 数据总能被正确解析。

这些测试一方面印证了本文前述的 API 语义(异常类型、截断处理、版本参数),另一方面也展示了:marshal 的输入必须严格符合格式,任何畸形字节都会导致异常——这是它"内部专用、不做防御性设计"定位的直接体现。


八、总结与使用决策清单

  1. 什么时候用 marshal:只在处理.pyc字节码、冻结模块、或需要在解释器内部(含 C 扩展层)交换 code object 时使用;KBEngine 这类内嵌 Python 的服务端中,通常你无需直接调用它——导入机制会自动完成。
  2. 什么时候绝对不要用:任何涉及跨版本持久化、RPC 传输、或来源不可信数据的场景。一律改用pickle(持久化/传输)或shelve(持久化字典),并继续遵守"不反序列化不可信数据"的安全底线。
  3. 格式限制:只支持文档列出的内置类型与None/Ellipsis/StopIteration;version < 3 无法表达递归容器;嵌套深度受MAX_MARSHAL_STACK_DEPTH(非 Windows 为 2000,Windows 为 1000)限制。
  4. 版本事实:本仓库内嵌解释器的Py_MARSHAL_VERSION为 4(kbe/src/lib/python/Include/marshal.h),marshal.version在 Python 层暴露同一值;C API 旧文档中"currently 2"的说法与实现不符,应以代码为准。

延伸阅读(仓库内路径)

  • 模块文档原文:kbe/src/lib/python/Doc/library/marshal.rst
  • C 实现:kbe/src/lib/python/Python/marshal.c
  • C API 文档:kbe/src/lib/python/Doc/c-api/marshal.rst
  • 版本宏定义:kbe/src/lib/python/Include/marshal.h
  • 单元测试:kbe/src/lib/python/Lib/test/test_marshal.py
  • .pyc 加载调用点:kbe/src/lib/python/Lib/importlib/_bootstrap_external.py(_compile_bytecode使用marshal.loads)
  • 冻结模块加载调用点:kbe/src/lib/python/Python/import.c(PyMarshal_ReadObjectFromString)
  • 游戏开发
  • 后端

【免费下载链接】kbengine

A MMOG engine of server.

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

相关推荐

上一篇:从 "got undefined" 到路径兼容重试:oh-my-openagent prompt-async-gate 生产事故的根因分析与修复复盘
下一篇:>

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询