这章回答:examples/order.py 中的两个具名函数调用,怎样从 CLI 入口走到 Go Host,再把总价交回 Python?
先准备既有 Guest artifact,然后运行:
./demos/run.sh examples/order.py
默认输入是 item=book、quantity=2。CLI 的本地 catalog 返回 book 单价 21 和 shipping 5,Python 计算 21 × 2 + 5,结果为 {"item":"book","total":47}。这里的“order”只是示例文件名和计算流程,不是下单动作。
先纠正入口名称
当前工具注册表是 Manifest。demoTools() 返回一个名为 lookup 的 ToolSpec,它的 Call 字段指向 Go 函数,AllowEarlyRead 标记它可以参与允许的早读。Python 因而直接写 lookup(key=...);不再写旧式的 tool("lookup", ...),也不再把工具名作为单独的运行时入口传入。
cmd/demo/main.go · L31–L46 · 打开完整源码
func demoTools() spine.Manifest {
// Host owns this immutable snapshot. No tool is available unless registered.
catalog := map[string]any{"book": 21, "pen": 3, "shipping": 5, "choose": "pen"}
return spine.Manifest{"lookup": {Call: func(ctx context.Context, args json.RawMessage) (any, error) {
var a struct {
Key string `json:"key"`
}
if err := json.Unmarshal(args, &a); err != nil {
return nil, err
}
price, ok := catalog[a.Key]
if !ok {
return nil, fmt.Errorf("missing key: %s", a.Key)
}
return price, nil
}, AllowEarlyRead: true}}
这段注册代码还说明了状态归属:catalog 是 Host 持有的只读快照,Go callback 先解码 key,查表,找不到时返回错误。Python 只能通过 Manifest 中存在的具名函数触达这段代码。
CLI 先准备一次运行
main 用 flag 得到 artifact、源文件、inputs 和模式选项,再把它们交给 runWithMode。普通的 run 只是把 prepared-cow 设为 false 的便捷入口;实际准备和执行都在 runWithMode。
cmd/demo/main.go · L15–L30 · 打开完整源码
func main() {
artifact := flag.String("wasm", "dist/spine.wasm", "real CPython Wasm artifact")
source := flag.String("source", "examples/lookup.py", "Python source")
input := flag.String("inputs", `{"item":"book","quantity":2}`, "JSON inputs")
plm := flag.Bool("plm", false, "prepare allowed snapshot reads early")
show := flag.Bool("show-transformed", false, "print the actual Guest AST rendering to stderr")
prefix := flag.Bool("prefix", false, "demo append-only source delivery in line fragments")
prepared := flag.Bool("prepared-copy", false, "clone clean initialized memory by full copy; not COW")
preparedCOW := flag.Bool("prepared-cow", false, "share clean initialized Linux memory with MAP_PRIVATE COW")
flag.Parse()
if err := runWithMode(*artifact, *source, *input, *plm, *show, *prefix, *prepared, *preparedCOW); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
runWithMode 先检查 full-copy 与 COW 不能同时打开,然后依次读取 Wasm 和 Python 文件,解析 JSON inputs,创建 context。默认选择 spine.New;其他内存 flag 选择对应的 prepared 构造函数。Runner 创建成功后由 defer runner.Close 负责关闭。
cmd/demo/main.go · L49–L82 · 打开完整源码
func run(artifact, source, input string, plm, show, prefix, prepared bool) error {
return runWithMode(artifact, source, input, plm, show, prefix, prepared, false)
}
func runWithMode(artifact, source, input string, plm, show, prefix, prepared, preparedCOW bool) error {
if prepared && preparedCOW {
return fmt.Errorf("-prepared-copy and -prepared-cow are mutually exclusive")
}
wasm, err := os.ReadFile(artifact)
if err != nil {
return err
}
code, err := os.ReadFile(source)
if err != nil {
return err
}
var inputs any
if err := json.Unmarshal([]byte(input), &inputs); err != nil {
return err
}
tools := demoTools()
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
newRunner := spine.New
if prepared {
newRunner = spine.NewPrepared
} else if preparedCOW {
newRunner = spine.NewPreparedCOW
}
runner, err := newRunner(ctx, wasm, tools)
if err != nil {
return err
}
defer runner.Close(context.Background())
普通路径没有 Prefix channel:它把 runner.Run 作为 execute 函数;若启用 -plm,则换成 runner.RunPLM。因此本例默认进入 Run(ctx, string(code), inputs),PLM 只是同一源文件的另一种执行入口,不改变 Python 的最终计算式。
cmd/demo/main.go · L83–L104 · 打开完整源码
var out spine.Output
if prefix {
// File replay is a small demo producer, not an LLM provider integration.
chunks := make(chan string)
go func() {
defer close(chunks)
for _, part := range strings.SplitAfter(string(code), "\n") {
select {
case chunks <- part:
case <-ctx.Done():
return
}
}
}()
out, err = runner.RunPrefix(ctx, chunks, inputs)
} else {
execute := runner.Run
if plm {
execute = runner.RunPLM
}
out, err = execute(ctx, string(code), inputs)
}
Run 怎样接住这一轮
Runner 的公开入口把本轮交给内部 run。run 为这次执行建立 run state,把 source、inputs、PLM 标志和当前具名 manifest 编成 request;随后创建一个新的 Guest,并调用 Guest 的 execute 导出函数。
从 order.py 看,Guest 执行的第一行是 lookup(key=inputs["item"])。Python bootstrap 把具名函数放进执行环境,函数调用形成 Host 请求。请求经过 C import 和已注册的 Host callback,Go 解析参数并在 manifest 中找到 lookup 的 Call。21 返回 Guest 后,第一行把它保存为 price。
第二行用同一路径查询固定的 shipping,得到 5。第三行没有跨边界,只读取 Python 局部变量和 inputs 做乘法、加法,并把字典赋给 result。
Runner 读回 execute 返回的指针和长度,在 Guest release 之前复制结果字节,再解码 response。成功 response 中的 value 成为 Output.Value;stdout 和可选的 transformed source 也一起进入 Output。CLI 最后打印 stdout,再打印 JSON value。
把调用关系压缩成一条线
main
→ runWithMode
→ demoTools:Manifest{"lookup": ToolSpec{Call: ...}}
→ spine.New
→ runner.Run
→ run / new Guest / execute
→ Python: lookup(key=...)
→ C import → Host callback → Manifest["lookup"].Call
→ 21 或 5 回到 Python
→ result = {item, price * quantity + shipping}
→ Output.Value
→ fmt.Println
关键交接点只有两个:工具调用把参数从 Python 带到 Host,再把返回值带回 Python;最终结果把 result 从 Guest 带回 Go。lookup 是当前真实的函数流程入口,catalog 和 Runner 状态都不应被描述成 Python 全局变量。
出错和下一步
lookup 找不到 key 时,Go callback 返回错误,Host bridge 把它作为本次具名调用的错误交回 Guest;Python 未捕获时,Run 返回错误,CLI 输出到 stderr 并以失败状态退出。Runner 仍在 runWithMode 的 defer 中收尾。
想看每个用户程序怎样改变这条主线,读 六个用户程序;想看 JSON/字节怎样穿过 Guest 与 Host,读 Host 工具边界。
读完本章应能回答:lookup 由谁注册、21 和 5 分别在哪里产生、以及为什么第三行的总价计算不会再次调用 Host。