当前版本:普通执行 + whole-program PLM + prefix,附可选 prepared full-copy。 本文描述当前代码,不是旧 Pysolate 的缩写。第 1–9 节先讲普通路径,第 10 节接入实际 PLM。Prefix 已接入同一套 Future;本版用 full-copy 交代基线/隔离,不实现 Linux COW,不宣称共享页收益。后续每个实现阶段同时更新本手册;不要求你先读旧仓库,也不做性能测量。
0. 学习顺序
你已经了解 Python 和研究目标,这里先补实际实现所需的背景,不从 C 指针开始。
- 跑一个结果,解释两行 Python:读 §1。能区分输入、单价、总价,以及它们分别在哪里计算。
- 建立执行模型:读 §2。能区分 CPython、Wasm、wazero、WASI,以及 Host 和 Guest。
- 跟完一次工具调用:读 §3–4,对照
guest/bootstrap.py、bridge.go和cmd/demo/main.go。能解释错误怎样回到 Python。 - 理解每次 Run 的状态:读 §5,对照
runner.go。能解释为什么不串状态、谁负责释放。 - 再读 C 和构建:读 §6–7。能解释指针、长度、引用计数,以及改哪种文件需要重建 Wasm。
- 用测试检查理解:读 §8–9。先自己判断改动位置,再和 AI 实现。
- 学习 PLM:读 §10 链接的实际实现章节,比较 prepare 启动和 resolve 交付。
- 学习 prefix:再读 流式前缀章节,理解源码接收、最终执行与缓存领取的边界。
- 可选背景:prepared-copy 只说明干净基线与私有复制,可先跳过。
后续按 pysolate-explained 的方式用 Astro 发布;当前 Markdown 是内容源,尚未搭站或发布。你可以稍后集中学习,不需要每阶段停下来验课。
不用一口气读完。每一层的验收标准是能用自己的话解释,不是记住 API 拼写。源码链接定位到文件,正文用函数名定位,避免行号变化后讲义失效。
1. 先跑:输入到输出
在这个独立仓库根目录运行:
go run ./cmd/demo
# {"item": "book", "total": 42}
go run ./cmd/demo -inputs '{"item":"pen","quantity":5}'
# {"item": "pen", "total": 15}
这两条命令已实际运行过。Go CLI 读取 examples/lookup.py:
price = tool("lookup", key=inputs["item"])
result = {"item": inputs["item"], "total": price * inputs["quantity"]}
默认输入是 {"item":"book","quantity":2}。inputs 是 bootstrap 放进执行环境的 Python 对象,不是 Python 自动提供的变量。
第一行读取 inputs["item"] 得到 book,然后调用 tool。真正的价格表在 Go Host 中:book: 21、pen: 3。Host 返回 21,Python 将它赋给 price。第二行的 21 * 2 在 Guest 的 CPython 中计算。
result 是这个项目规定的输出变量。bootstrap 执行完模块后读取它并编码为 JSON;没设置时返回 None,即 JSON null。print() 是 stdout,不是 result;Go 的 Output 分别保存这两者。
改自己的 Python 程序时用 -source path/to/file.py。这是更换输入源码,不是修改解释器,不需要重建 Wasm。
**先回答:**如果把第二行改为 result = price,总价逻辑消失在哪一侧?哪些 Go/C 文件完全不用改?
2. 背景:到底是谁在执行 Python?
2.1 CPython、Wasm、wazero 是三件事
- CPython 是 Python 的参考解释器,主要用 C 实现。本项目使用 CPython 3.14。
- WebAssembly(Wasm) 是一种可验证的二进制指令格式。这里把 CPython 和 C bridge 编译、链接成 Wasm。
- wazero 是 Go 写的 Wasm runtime。Go Host 用它加载模块、编译 Wasm、实例化并调用导出函数。
因此,这里不是把用户 Python 源码翻译成 Wasm。用户源码由已经在 Wasm 内运行的 CPython compile/exec 执行。也没有调用本机 Python 子进程来伪装 Guest。
不要混淆两次编译:wazero 的 CompileModule 处理 Wasm 指令;Python 的 compile 处理这次收到的 Python 源码,生成 CPython 可执行的 code object。
2.2 Host / Guest
Host 是外面的 Go 程序,持有工具注册表、价格数据和 runtime。未来真实工具使用的凭据也应留在 Host。
Guest 是 Wasm 实例中的 CPython、标准库和用户程序。每次 Run 创建新实例;可共享编译代码,但不会因此共享 Python 的堆、模块或 global。
Host 可以主动读写 Guest 的线性内存;Guest 不能把一个数字直接当成 Go 地址来访问 Host 内存。Guest 要请求外部能力,必须调用 runtime 接入的函数。
2.3 WASI 和嵌入式文件系统
WASI 给 Wasm 提供一组与系统相关的接口,例如文件读写和退出。安装 WASI 不代表把整个宿主操作系统都授权给 Guest。
本项目没有 preopen(预先授权挂载)宿主目录,没有传入宿主环境变量或配置外部 socket;不能据此读取 Host 的 /etc/passwd 或随意启动宿主进程。部分基础 WASI 接口仍存在,例如 stdout 和 Guest 退出,不应说成“Guest 完全没有系统接口”。
CPython 又需要 json 等标准库文件。构建时用 wasi-vfs 把这些文件打包进 Wasm,Guest 内路径是 /usr/lib/python3.14。这不等于挂载 Host 的同名目录。
Python 的名字隐藏不是安全边界。用户即使自行 import _spine 调底层入口,仍只能经过 Host 注册表获得工具权限。
2.4 Reactor 和初始化
这个 Wasm 是 reactor:由 Host 显式调用导出函数,而不是启动一个等待 stdin 的 Python CLI。
_initialize:工具链提供的初始化入口,完成 C/WASI/VFS 所需初始化。init:我们在 C 中写的入口,启动 CPython、导入 bootstrap。execute:执行这一轮输入。
这三个名字不能互换。wazero 实例化完成也不意味着 CPython 已经初始化。
3. 一次调用怎样往返?
Go Runner.Run
→ C execute
→ Python bootstrap.execute
→ 用户 Python
→ Python tool
→ C python_call
→ Go hostCall
→ Go lookup
← JSON 结果或错误
← Python bytes
← Python 值,或在这里 raise RuntimeError
→ 继续 Python 控制流
→ 把 result 编码成 bytes
→ 返回结果 buffer 的位置和长度
← Go Output.Value / error
3.1 Python bootstrap:执行约定
读 guest/bootstrap.py,先看 execute,再看 tool。
execute(request) 接收 JSON 文本,解析得到 source 和 inputs。它建立 scope:
__name__ = "__main__",允许常见的模块入口判断;inputs是这次输入;tool是项目提供的调用函数。
exec(compile(...), scope) 执行普通 Python 模块。代码包含 if、循环、函数、try/except 都由 CPython 处理;普通模式不运行 AST 优化器。scope 也不是权限沙箱;只负责把执行所需的变量放到一起。
执行完成后,scope.get("result") 决定输出。外层 except BaseException 把未捕获异常的类型和消息编码成错误;它也覆盖 SystemExit。当前没有完整 traceback。finally 刷新 stdout,避免缓冲的 print 随实例关闭消失。如果 bootstrap 自己失败,C bridge 会向 stderr 报错,Go 将其作为 bridge 失败返回。
tool(name, **args) 把调用变成 {"tool":"lookup","args":{"key":"book"}},调用 C 扩展 _spine.call,再通过 decode 解码响应。存在 error 字段就 raise RuntimeError,否则返回 value。
这里使用“字段是否存在”,而不是“错误消息是否非空”:空字符串也可以是合法错误消息;value: null 则是成功返回 None。
3.2 Host bridge:权限与参数
读 bridge.go。Tool 是函数类型:
type Tool func(ctx context.Context, args json.RawMessage) (any, error)
含义是:工具接收取消上下文和 JSON 参数,返回可编码的值或错误。不是大接口,也没有 Broker 子类。
hostCall 通过 readRequest、invoke、encodeResponse 完成以下步骤。这几个普通函数也被 PLM 路径复用,不是通用 Broker:
- 根据指针、长度读取 Guest 线性内存,检查范围和消息上限。
json.Unmarshal解码请求,得到工具名和参数。- 查
r.tools[request.Tool]。没有注册就报unknown tool,不做自动猜测或回退。 - 同步执行 Go 函数,得到值或错误。
- 编码响应,交给
writeResponse写回 Guest。
json.RawMessage 是保留原始 JSON 内容的字节切片。它的解码方法会复制参数,所以工具不持有 Guest 的可变内存视图。工具应把收到的 RawMessage 当作只读数据,因为 Run 还可能保留它用于 Future 匹配;需要修改时先解码成自己的结构,不原地改传入字节。外层请求只解析一次;具体 lookup 再解析自己的参数类型,这是两层不同的协议结构,不是反复证明同一对象。
响应的 Error 用 *string:nil 表示无错误,指向 "" 表示有一个空消息错误。Value 不用 omitempty,否则成功的 nil 会被省略。这个区别已经由真实测试覆盖。
工具返回的 Go 值不能编码为 JSON 时,bridge 返回明确工具错误,不假造一个结果。
3.3 CLI:业务不放进解释器
main 读取 -wasm、-source、-inputs,以及可选的 -plm、-prefix、-show-transformed、-prepared-copy,调用 run。失败打印到 stderr 并非零退出。
run 读取 Wasm 和 Python 文件,解析输入 JSON;demoTools 建立固定价格表及工具注册表。lookup 是一个捕获 catalog 的 Go 闭包;它负责解析 key、查价格、决定 missing key 的错误,不是 bootstrap 或 C bridge 的职责。
然后它建立演示用的 30 秒 context,调用 New、Run(开启优化时为 RunPLM),打印 stdout 和结果,最后关闭 Runner。30 秒是 CLI 的选择,不是 CPython 的限制;测试中的 2 秒只是取消测试条件,不是性能指标。
**练习:**新增 discount 工具时,你会先改 cmd/demo/main.go 还是 guest/runtime.c?先写下理由,不急着实现。
4. 读 Go 所需的最少背景
不需要先学完整 Go。当前代码遇到这些语法时,按下列含义读:
struct:一组具名字段,例如 Runner 持有什么、Output 返回什么。func (r *Runner) Run(...):属于 Runner 的方法,r是当前对象;*Runner是指针,不是复制整个 Runner。[]byte:字节切片,包含底层存储的位置和长度等信息;不能假设拿到切片就获得独立副本。map[string]Tool:字符串到函数的注册表,类似 Python 的 dict of callables。any:允许不同 Go 值的接口类型。能放入any不代表能转成 JSON,例如 channel 不能直接作为结果。(value, error):Go 用返回值表达正常错误;err != nil时由调用者决定处理方式。这和 Python exception 不同,所以 bridge 必须显式转换。defer:当前函数退出时执行,后注册的先执行。不只是成功时执行。context.Context:传递 deadline/取消信号,不是 Python globals,也不自动强杀任意 Go 函数。- 闭包:函数保留外层变量。例如
lookup访问catalog,fail访问本轮 stdout/stderr。
uint32/uint64 在这里主要用于 Wasm ABI。^uint32(0) 是所有位为 1 的 32 位值,跨到 C 的 int32_t 时被解释为 -1,表示底层传输失败。
5. Runner:谁拥有状态,怎样结束?
读 runner.go。推荐顺序:Runner / Output → New → Run → 最后几个 helper。
5.1 Runner 和 Output
Runner 拥有:
runtime:wazero runtime;code:已编译 Wasm,可供多次实例化;tools:固定工具注册表;earlyReads:Host 明确允许提前执行的工具名集合,语义见第 10 节。image:可选的干净初始化内存基线;nil 为 fresh 初始化,有值时每个实例全量复制一份。
Output 返回 Value、Stdout,以及 PLM 模式下供讲解的 Transformed。Value 用 json.RawMessage 保留 JSON,而不是强迫库层猜测业务结果的 Go 类型。
Runner 不持有可复用的 Python 实例。 每次 Run 的实例、I/O buffer 和输入输出分配都在方法局部。不要把“重用代码”和“重用 Python 状态”混为一谈。
5.2 New:准备不变部分
New 建立 wazero runtime,并启用 WithCloseOnContextDone(true),让执行能响应 context 取消。复制工具注册表意味着调用者以后替换原 map 的条目不会改变 Runner;但复制的是函数值,不是深拷贝闭包捕获的数据。
它依次安装 WASI、注册名为 spine 的 Host 模块及其 call/prepare/resolve 函数、编译传入的 Wasm,并确认 _initialize/init/alloc/release/execute 这些入口存在。失败时关闭已创建的 runtime。
这里检查导出名是为了给错误 artifact 一个直接错误;它不是给任意未知 Wasm 建完整兼容性证明。项目运行自己构建的 Guest。
5.3 Run:按代码顺序跟一次
Run、RunPLM、RunPrefix 共用一个 run 生命周期;前两者传完整源码,prefix 在同一实例里先接收片段。它先建立本轮 runState 和子 context;普通模式不启动 Future,PLM/prefix 的任务也由这里收尾。
- 编码输入。 把
source、inputs变成 JSON。不能编码或超过请求上限时,尚未创建 Guest,直接返回。 - 创建私有实例。 为本轮创建 stdout/stderr buffer,交给
newGuest(prepared.go)使用编译 code 实例化。空模块名避免名字争用,WithStartFunctions()禁用默认启动调用。 - 初始化。
newGuest调_initialize;fresh 模式再调 Cinit,prepared 模式则全量复制干净 image。初始化失败由 helper 关闭实例并报错。 - 登记关闭。
newGuest成功返回后,run登记defer m.Close(context.Background()),覆盖后面的成功和失败出口。 - 写请求。 共用
callWithBytes(在 prefix.go)调用 Guestalloc,再Memory().Write写入 JSON;调用返回时释放请求。 - 执行。 调 Guest
execute(ptr, len),它最终进入 Python bootstrap。 - 读响应。 将返回的 64 位数拆成结果位置、长度,检查可读范围和结果上限,解码 JSON,登记结果 buffer 的
release。 - 交付。 返回
Output;存在错误则返回 Go error。json.RawMessage已复制结果,不会在 Guest 关闭后悬空。
请求已由 callWithBytes 在调用返回时释放;执行结果是 C 单独分配的副本。之后退出时的顺序是结果释放 → 实例关闭 → runState 取消/等待任务。普通模式没有提前任务,PLM worker 也不持有 Guest 内存。如果 trap/取消使 Guest 已不能再调用 release,仍会执行实例关闭,回收这份 Guest 的资源;不是承诺取消后还能执行任意 Guest 清理代码。
fail 只是局部 helper,把已有 stdout 和 stderr 带回调用者,没有状态机或自动重试。
5.4 Close、boundedText、writeResponse
Runner.Close 关闭 runtime 及编译代码等资源;调用者必须等自己的 Run 调用结束后再 Close。这是当前所有权约定,不是额外实现了并发关闭协调器。
boundedText.Write 限制单个 stdout/stderr buffer 的累计大小。嵌入 strings.Builder 复用字符串拼接,覆盖 Write 加入上限检查。写入超限返回错误,不静默截断成看似完整的输出。
writeResponse 是 Host 写回工具响应的地方。检查消息大小、Guest 提供的 buffer 容量和地址范围;失败返回全 1 的状态码,C 将它转为 Python 错误。
这些是跨 Wasm 内存边界的检查,不同于对可信内部对象反复编码、hash、批准。
5.5 隔离与取消的准确范围
CPython 堆、模块、C static 数据和线性内存属于实例。测试先修改 builtins,下一轮看不到,是因为换了整个 Guest,不是靠删几个 global reset。
Host 工具不是每次 Run 自动隔离的。如果闭包捕获可变 map,多个调用能看到同一份 Host 状态;工具实现者负责语义和并发。当前演示价格表只读。
Wasm 无限循环能被 deadline 中止。Go 工具若调用网络,应把 ctx 传进去;若工具卡在不响应取消的 Go 函数,当前设计不能强杀它。现在普通 Run 不启动异步任务。RunPLM 已增加 Run-owned Future,但仍没有工具失败后的自动重试。
**练习:**如果把 Python 实例存进 Runner,每次只换 inputs,builtins.secret 测试为什么可能失败?清空 scope 是否足够?
6. C 与 ABI:为什么需要指针和长度?
读 guest/runtime.c。先理解 ABI,再读引用计数。
6.1 线性内存与 ABI
ABI(Application Binary Interface)约定两边怎样用底层参数传值。这里不直接把 Python dict 或 Go map 当 Wasm 参数,而是传 JSON bytes 的位置和长度。
wasm32 的指针是 Guest 线性内存中的 32 位偏移,不是 Host 的机器地址。ptr=1000, len=20 表示 Guest 内存从偏移 1000 开始的 20 个字节;wazero Memory.Read/Write 将这个约定落实为有界访问。
alloc(size)返回 Guest 内的空间。release(ptr)释放 Guest 内的分配。execute(ptr, size)返回(length << 32) | pointer。
最后一项把两个 32 位数装入一个 64 位整数:低 32 位是地址,高 32 位是长度。它不是 hash,也不是 certificate;只是这个短 ABI 的返回约定。
6.2 宏、methods、module、init_spine
EXPORT(name) 给函数标记 Wasm 导出名,让 Go 找到它。import_module("spine") / import_name("call") 声明外部导入;实际实现就是 Go 注册的 spine.call。
MAX_MESSAGE 是 C 侧工具消息容量,与 Go maxMessage 同为 1 MiB。以后改变消息约定要一起修改,不为这两个常量另造生成器。
methods 列表把 Python 方法名 call 对应到 C 函数 python_call;阶段 2 又增加 prepare/resolve,详见 PLM 章节。module 描述名为 _spine 的内置扩展模块;init_spine 创建它。这不是一个另行加载的宿主 .so。
execute_fn 缓存 bootstrap 的 execute Python 对象。它是 Guest C static 状态,每个新实例各有一份,不是 Go 的全局变量。
6.3 init:启动 CPython
PyImport_AppendInittab 在启动前注册 _spine。PyConfig_InitIsolatedConfig 建立隔离的解释器配置;当前关闭 site 导入、字节码文件写入,并明确指定嵌入标准库路径。
Py_InitializeFromConfig 启动解释器,随后清理配置对象。再 import spine_bootstrap,取得 execute 函数保存到 execute_fn。失败打印 CPython 错误并返回非零状态。
fresh Guest 调用一次 init;prepared 模式只有参考 Guest 调 init,后续实例复制其基线。两者都不实现同实例反复初始化协议。
6.4 python_call:CPython 到 Host
PyArg_ParseTuple(..., "s#", ...) 取得请求字符串数据及长度。PY_SSIZE_T_CLEAN 和 Py_ssize_t 配合 CPython 的带长度 API 使用。
接着分配临时响应 buffer,调用导入的 Host 函数。Host 在这个 buffer 写入 JSON,并返回写入长度。C 把响应复制成 Python bytes 后释放临时 buffer。
当 ABI 状态不合法时,PyErr_SetString 设置 Python 异常,返回 NULL 告知 CPython 调用失败。这不同于把字符串 "error" 当成功返回值。
6.5 execute:CPython 到 Go
C execute 调用缓存的 Python execute_fn。bootstrap 返回 bytes;C 获得其内容后,复制进独立 malloc buffer,再减少 Python 对象引用。Go 随后读取并释放这个 buffer。
为什么不直接返回 Python bytes 内部地址?因为 Python 对象被释放后,借出的指针可能失效。明确复制和所有权比跨语言保持一条隐式活引用简单。
Py_DECREF 是 CPython 引用计数管理,free 是 C 分配管理,两者不能混用。bootstrap 模块对象取得函数后减少引用;execute_fn 的长期引用留到实例结束。当前不调用 Py_Finalize 后复用实例,而是关闭整个 Guest;不承诺关闭时运行所有 Python atexit 逻辑。
函数返回 0 表示桥接失败;正常返回的 buffer 归 Go 调用者负责。工具临时 buffer 则只属于一次 python_call。
**练习:**如果删掉 memcpy,直接返回 PyBytes 的内容地址,同时保留 Py_DECREF(result),生命周期错在哪里?先说清楚,不要实际引入悬空指针。
7. 构建:源码怎样变成真正运行的 artifact?
7.1 哪些改动需要重建?
- 改
examples/lookup.py或换-source:这是运行输入,不重建 Wasm。 - 改 Go Host:重新
go run或go test即可。 - 改
guest/bootstrap.py、guest/runtime.c或 Guest 链接配置:必须重建、打包并替换dist/spine.wasm,再跑真实测试。
你编辑的 guest/bootstrap.py 不会被运行中的 Guest 从宿主目录自动读取。它位于 artifact 内部。只改源码却运行旧 Wasm,是这里最容易造成假验证的错误。
阶段 2 已实际重新链接 C bridge,并把新 bootstrap 和 plm.py 打包进入新的 Wasm;不是运行阶段 1 的旧 artifact。CPython 静态库仍复用已有构建输入,不从头编译解释器。
7.2 build-guest.sh 逐段读
读 build-guest.sh。它只做一个有界构建,不管理云机器,不调用旧 runtime。
set -euo pipefail:失败、未定义变量和管道失败不能静默忽略。读取脚本根目录,以及SPINE_BUILD_INPUTS指定的已有工具链/CPython 构建输入;检查 Linux x86_64。- 在本仓库
build/创建临时目录,退出时清理。只操作该临时目录,不清空共享 CPython 缓存或旧论文 dist。 - 用 wasi-sdk 的 clang 编译本项目 C bridge,并链接 CPython 静态库、其所需支持库、wasi-vfs。静态库是已编译对象的集合,这一步不重新编译 CPython 源码。
-mexec-model=reactor选择 reactor;导出内存、设置初始/最大内存与 C stack 大小,生成 raw Wasm。- Python 小段复制 CPython 标准库,排除测试/cache 文件,把本项目 bootstrap 复制为
spine_bootstrap.py,并复制plm.py。 - wasi-vfs 将标准库目录嵌入 raw Wasm。
wasm-tools validate检查 Wasm 格式;成功后才替换dist/spine.wasm。
validate 不代表工具语义已正确,仍需执行 Go 的真实 Guest 测试。
7.3 构建依赖的范围
这个仓库是独立 Go module,源码和运行不依赖旧仓库;构建 Guest 仍依赖 CPython/WASI 工具链和预编译库,不把这些大型输入塞进 Git。
在具有这些输入的 Linux x86_64 环境,运行:
SPINE_BUILD_INPUTS=/path/to/extracted-build-inputs bash build-guest.sh
这里的 /path/to/... 是需要替换的路径,不是已经执行成功的具体命令。构建输入布局、当前机器上的可用路径见 构建与来源说明。当前脚本不负责从零下载/编译 CPython;空环境不能只靠 go mod tidy 生成 Guest。
本机已有 artifact,所以日常学习只需要 Go 与 dist/spine.wasm。换到全新 clone 时,Git 不包含这个生成文件,需要取回已构建 artifact 或在合适的 Linux 环境构建。
7.4 限制常量来自哪里?
- 请求、执行结果、单次工具消息,以及 stdout/stderr 各有 1 MiB 上限,定义于 Go/C。
- Guest 初始线性内存 128 MiB,最大 512 MiB;C stack 16 MiB,来自链接参数。线性内存中的 stack 不是额外再加一份 Host 内存。
这些是当前演示的工程选择,不是 Wasm 标准强制值,也不是实测内存占用。上限检查不等于从输入到输出全链路的 Host 内存配额:例如 JSON 序列化先产生数据,再检查大小。可信 Go 工具本身也不被 Guest 内存上限限制。
没有承诺完整 CPython 包兼容、任意 native 扩展、任意精度 Python 数值的无损 Go JSON 往返,或完整终端交互。这些若成为新需求,先讨论需要保留的语义。
8. 测试:每个检查在回答什么?
本节先读 runner_test.go 的普通路径测试;PLM 的 13 个真实路径子测试与 6 个 AST 单测见第 10 节。TestRealGuest 先读取真实 artifact,缺失直接失败,不 skip、不换成本机 Python。它建一个 Runner,顺序运行多个新 Guest。
表驱动部分列出输入源码、预期 JSON 或预期错误;比较 JSON 时忽略空白格式,不把打印风格当业务语义。case 名称与含义如下:
python:在 Guest 内计算,并报告 CPython 3.14。host:真实工具结果与输入组合得到 42。null tool result:Host 成功的 nil 交付为 Python None。caught tool error:Python 在工具调用位置捕获 RuntimeError。uncaught tool error:未捕获工具错误返回 Go。empty tool error:空错误消息不能被省略为成功。malformed tool request:自行调用底层_spine.call的坏 JSON 不绕过解析。unknown tool:注册表不包含的工具拒绝。python error:除零不会变成成功结果。syntax error:源码编译失败能报告。non JSON result:Python set 不能静默当作 JSON 成功。private state write:一轮修改 builtins。private state read:下一轮看不到上轮改动。no host filesystem:尝试读/etc/passwd失败。这个具体测试不是完整安全审计。stdout:print 与 result 分开交付。deadline:中止 Guest 无限循环,随后再次运行仍成功;没有声称可以强杀任意 Go 工具。
工具调用计数按每个用例检查预期次数;单独用 -run 筛选子测试也能正确验证,不再依赖整组全部运行。隔离检查现在按顺序执行,不要随手加 t.Parallel:测试共享 Host 计数器,而写/读用例本身也有先后关系。
执行:
go test ./... -count=1 -v
go vet ./...
go test 的用时是测试框架输出,不是性能实验。本项目只检查结果、失败和隔离语义。
常见失败怎么定位
open dist/spine.wasm: no such file:缺少构建输出,不是 Python 语法错。missing Guest export:artifact 与这套 ABI 不匹配,先查实际文件。CPython initialization failed:查 Guest stderr、标准库打包与初始化。unknown tool:检查注册表及调用名称,不先改 C。RuntimeError: missing key:业务工具自己的错误,是否捕获由 Python 决定。Guest execution bridge failed:bootstrap/C 调用未正常产生响应,查 stderr。result exceeds 1 MiB:输出协议上限,不直接等同于 Wasm 内存耗尽。
9. 练习方式:你决定行为,AI 协助实现
每次练习只选一个小变化:
- 你描述新需求,并给一个输入输出例子。
- 你先猜要改哪个函数、哪些地方不该改。
- AI 检查调用链,指出语义问题并建议小补丁。
- 确认成功和失败行为后再实现;你解释 diff 和测试结果。
可选练习:
- 业务层:新增另一个只读工具。观察为什么无需改变解释器。
- 错误呈现:返回更有用的 Python traceback。先决定暴露哪些内容、是否改变 Output,再改 bootstrap。
- 执行约定:忘记赋值
result时应返回 None,还是明确报错?这不是“增强安全”的默认选择,而是你要决定的 API 行为。
这些题没有提前实现。涉及核心语义时先讨论,不由 AI 自动选择“更通用”的框架。
10. PLM 与后续阶段
Whole-program PLM(已实现)
完整讲解见 阶段 2:Whole-program PLM,包括实际转换、AST 支持范围、逐函数导航、channel/WaitGroup 背景、错误/取消和 13 个真实路径子测试。它直接接在本手册的普通执行路径上,没有另一套解释器或 Broker。
现在 Host 可以提前开始明确 opt-in 的稳定快照读取;结果/错误仍在原调用处 resolve。不支持的小语言之外的程序原样运行。
Prefix(已实现)
完整讲解见 阶段 3:Prefix。RunPrefix 在同一个 Guest 中接收 append-only 片段,只提前准备连续的顶层简单读取;关闭 stream 后编译执行拼接的最终源码。最终优化或不优化时都复用已准备 handle,没有第二套 Future。
Prepared full-copy(已实现,替代本版 COW 实现任务)
按当前准备重点,只用 短的 full-copy 实现 说明初始化基线和私有写入。NewPrepared 捕获干净内存,所有 Run 模式仍共用原有路径。没有 memfd/MAP_PRIVATE,没有真实页共享或性能结论;Linux COW 不再是本版待完成项。
实现重点仍是 PLM 与协作 coding。
11. 仓库文件索引与补充背景
- README:运行入口和学习手册入口,不重复整篇讲解。
- go.mod / go.sum:独立模块及依赖版本/下载校验;直接依赖 wazero,没有旧 runtime 包依赖。依赖图仍可能包含 wazero 自己需要的库。
- 构建与来源说明:artifact 和构建输入的来源、平台边界。
- .gitignore:排除 dist/build/cache 和本机临时状态,避免将大型工具链及 artifact 混入源码历史。
- LICENSE:沿用抽取源码的 MIT 许可;CPython 等第三方依赖仍遵守各自许可,不能把 MIT 当成整个生成 Wasm 的统一许可。
按需查原始背景文档,不要求通读:
- Go Tour:方法、切片、map、defer。
- Go context:取消的协作式约定。
- Go encoding/json:RawMessage 与 JSON 类型映射。
- wazero:Wasm runtime 的模块/实例概念。
- CPython 初始化配置:为什么要配置路径和运行环境。
- CPython 引用计数:Py_DECREF 的生命周期含义。
这些是补充阅读链接,当前实现行为以本仓库源码和真实测试为准。