PPysolate SpineREAD THE CODE · UNDERSTAND THE SYSTEM
课程与章节
CODE READING / 00

先跟完一次 order.py

按函数职责和调用目的读正常执行路径。

本章目录

这章回答: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。