这组长文追踪一套具体实现:代码在何处生成,值如何跨过 Go、Wasm 和 CPython,哪些状态保留到下一轮,哪些资源必须在本轮结束时清理。
每篇先给一个可以跟下去的例子,再展开源码、对象关系和失败情况。读完后,应当能从一个函数调用追到真正执行它的地方,并解释这一步依赖哪些前提。
阅读路线
- CPython 与 NumPy 怎样进入 Wasm:交叉编译、静态扩展注册与导入。
- Prefix:从源码片段到原位领取:完整前缀、提前读取、队首匹配和最终执行。
- PLM:完整 AST 的改写过程:从变换前后的代码观察依赖、控制流和异常位置。
- wazero Host 状态与 Guest 内存:编译代码、实例、调用栈、表和线性内存。
- COW 实现与快照复用条件:slice、地址范围、文件映射、写入私有化,以及安全复用的时机。
- 跨语言所有权与失败收尾:缓冲区、Python 引用、Future、取消和实际 defer 顺序。
最关心 COW 时,可以先读第四、第五篇,再回看构建和执行过程。
先分清三个观察层
上图展示 Linux COW 模式中的对象与映射关系。Guest 框内列出数据类别,位置不代表真实地址顺序。底部只画出一个未写入基线页的共享关系,新增匿名内存和已经私有化的页没有全部展开。点击图可单独放大。
阅读后面的 []byte 时,先问它描述哪一段存储。Go 的 slice 值记录数据起点、长度和容量;底下的字节可能由 Go 分配,也可能来自 Linux 的映射。在 COW 路径中,复制一个 slice 描述不会建立新的页映射。
再看 Guest 里的对象。CPython 的对象、字节缓冲和 C 栈放在线性内存中。wazero 自己的实例对象、函数引用和执行状态则还有各自的保存位置。一次内存复制能够恢复多少行为,要沿着这些引用逐项确认。
这组文章使用的实现版本
- Spine:
190ea2e238bba454cdcc335e95736c3f27f6be9a,本组写作开始时的代码版本。 - 原始 Pysolate:
702f6b6cf48b65f878b057d0c6bce22e852068ee,用于追踪已有的 NumPy 构建实现,只读检查。 - wazero:Spine 的
go.mod固定为v1.11.0。
文中的当前运行结果会注明测试对象。源码片段用于解释具体路径,教学推演会说明输入和假设。没有执行过的构建步骤不会写成成功日志。
NumPy 的当前状态
本次在 Spine 现有 Wasm 产物中实际尝试导入 NumPy,结果为:
{"python":"3.14.0","error_type":"ModuleNotFoundError","error":"No module named 'numpy'"}
原始 Pysolate 则有独立的 numpy-core 构建 profile,包括 NumPy 源码选择、静态扩展 archive、初始化符号注册和包文件打包。第一篇会沿这条链解释 NumPy 怎样可被 CPython 导入,并列出 Spine 若要采用这条路径需要接上的部分。本轮保持 Spine 的运行时实现不变。
带着具体问题读
- C 扩展被链接进 Wasm 后,Python 的 import 如何找到它?
- 后面的工具先完成时,前面的赋值是否已经发生?
- 同一个请求出现两次,准备队列怎样分别交付?
- 对 A 的线性内存写一个字节,B 和基线分别会看到什么?
- 放在 Go 对象里的数据,会不会随 Guest 内存一起恢复?
- 取消发生后,哪段代码确保后台读取已经退出?
这些问题会在各篇中落实到具体数据和调用顺序。遇到 Go 组合写法不熟悉,可以回看 Go 组合速成;需要按文件逐段读时,可以回到 代码带读总路线。