PPysolate SpineREAD THE CODE · UNDERSTAND THE SYSTEM
课程与章节
源码索引 / CURRENT IMPLEMENTATION

runner.go

199 行 · 点击行号可定位;函数链接随声明位置更新,不手填行号。

跳到函数 / 声明
// Package spine is a small, fresh-instance Wasm CPython runner.
package spine

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"regexp"
	"sort"
	"strings"

	"github.com/tetratelabs/wazero"
	"github.com/tetratelabs/wazero/api"
	"github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1"
)

// Runner owns compiled code, tools and an optional clean image, never a live user Guest.
// Close must happen after all Run calls return. Tool implementations must honor context.
type Runner struct {
	runtime       wazero.Runtime
	code          wazero.CompiledModule
	manifest      Manifest
	guestManifest []guestToolSpec
	image         []byte // Immutable full-copy baseline; COW owns its image separately.
	cow           cowRuntime
}

type guestToolSpec struct {
	Name           string `json:"name"`
	AllowEarlyRead bool   `json:"allow_early_read"`
}

type Output struct {
	Value       json.RawMessage
	Stdout      string
	Transformed string // Optional actual Guest AST rendering for learning, not execution input.
}

var pythonIdentifier = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`)

var pythonKeywords = map[string]bool{
	"False": true, "None": true, "True": true, "and": true, "as": true, "assert": true,
	"async": true, "await": true, "break": true, "class": true, "continue": true, "def": true,
	"del": true, "elif": true, "else": true, "except": true, "finally": true, "for": true,
	"from": true, "global": true, "if": true, "import": true, "in": true, "is": true,
	"lambda": true, "nonlocal": true, "not": true, "or": true, "pass": true, "raise": true,
	"return": true, "try": true, "while": true, "with": true, "yield": true,
}

// New compiles the actual artifact once; every Run instantiates private memory.
func New(ctx context.Context, wasm []byte, manifest Manifest) (*Runner, error) {
	r := &Runner{
		runtime:       wazero.NewRuntimeWithConfig(ctx, wazero.NewRuntimeConfig().WithCloseOnContextDone(true)),
		manifest:      make(Manifest, len(manifest)),
		guestManifest: make([]guestToolSpec, 0, len(manifest)),
	}
	for name, spec := range manifest {
		if !pythonIdentifier.MatchString(name) || pythonKeywords[name] || name == "inputs" || name == "__name__" || name == "__builtins__" || strings.HasPrefix(name, "_spine") {
			r.Close(ctx)
			return nil, fmt.Errorf("tool name cannot be injected into Python: %s", name)
		}
		if spec.Call == nil {
			r.Close(ctx)
			return nil, fmt.Errorf("tool has no Host implementation: %s", name)
		}
		r.manifest[name] = spec
		r.guestManifest = append(r.guestManifest, guestToolSpec{Name: name, AllowEarlyRead: spec.AllowEarlyRead})
	}
	sort.Slice(r.guestManifest, func(i, j int) bool { return r.guestManifest[i].Name < r.guestManifest[j].Name })
	if _, err := wasi_snapshot_preview1.Instantiate(ctx, r.runtime); err != nil {
		r.Close(ctx)
		return nil, err
	}
	if _, err := r.runtime.NewHostModuleBuilder("spine").NewFunctionBuilder().WithFunc(r.hostCall).Export("call").
		NewFunctionBuilder().WithFunc(r.hostPrepare).Export("prepare").
		NewFunctionBuilder().WithFunc(r.hostResolve).Export("resolve").Instantiate(ctx); err != nil {
		r.Close(ctx)
		return nil, err
	}
	var err error
	r.code, err = r.runtime.CompileModule(ctx, wasm)
	if err != nil {
		r.Close(ctx)
		return nil, err
	}
	for _, name := range []string{"_initialize", "init", "alloc", "release", "execute"} {
		if r.code.ExportedFunctions()[name] == nil {
			r.Close(ctx)
			return nil, fmt.Errorf("missing Guest export: %s", name)
		}
	}
	return r, nil
}

func (r *Runner) Close(ctx context.Context) error {
	r.image = nil
	err := r.runtime.Close(ctx)
	if r.cow != nil {
		err = errors.Join(err, r.cow.close())
		r.cow = nil
	}
	return err
}

// Run owns the instance, I/O and all Guest allocations until it returns.
func (r *Runner) Run(ctx context.Context, source string, inputs any) (Output, error) {
	return r.run(ctx, source, inputs, false, nil)
}

// RunPLM enables the built-in Guest AST pass, only for explicitly allowed snapshot reads.
func (r *Runner) RunPLM(ctx context.Context, source string, inputs any) (Output, error) {
	return r.run(ctx, source, inputs, true, nil)
}

func (r *Runner) run(ctx context.Context, source string, inputs any, plm bool, chunks <-chan string) (Output, error) {
	state := newRun(ctx, r, plm)
	defer state.close()
	ctx = context.WithValue(state.ctx, runKey{}, state)
	request, err := json.Marshal(struct {
		Source   string          `json:"source"`
		Inputs   any             `json:"inputs"`
		PLM      bool            `json:"plm"`
		Manifest []guestToolSpec `json:"manifest"`
	}{source, inputs, plm, r.guestManifest})
	if err != nil {
		return Output{}, err
	}
	if len(request) > maxMessage {
		return Output{}, errors.New("request exceeds 1 MiB")
	}
	stdout, stderr := &boundedText{}, &boundedText{}
	// No host directories, environment, stdin, network or process capabilities.
	m, err := r.newGuest(ctx, stdout, stderr)
	if err != nil {
		return Output{}, err
	}
	defer m.Close(context.Background())
	fail := func(err error) (Output, error) {
		return Output{Stdout: stdout.String()}, fmt.Errorf("%w%s", err, stderr.String())
	}
	if chunks != nil {
		if err := receiveSource(ctx, m, request, chunks); err != nil {
			return fail(err)
		}
		// Input and source already belong to this Guest. Do not send a second copy.
		request = []byte(`{"prefix":true}`)
	}
	packed, err := callWithBytes(ctx, m, "execute", request)
	if err != nil {
		return fail(err)
	}
	// execute returns (length << 32) | pointer. Go copies before release/Close.
	p, n := uint32(packed[0]), uint32(packed[0]>>32)
	if p == 0 {
		return fail(errors.New("Guest execution bridge failed: "))
	}
	defer m.ExportedFunction("release").Call(ctx, uint64(p))
	if n > maxMessage {
		return fail(errors.New("result exceeds 1 MiB"))
	}
	data, ok := m.Memory().Read(p, n)
	if !ok {
		return fail(errors.New("Guest result outside linear memory"))
	}
	var response struct {
		Value       json.RawMessage `json:"value"`
		Error       string          `json:"error"`
		Transformed string          `json:"transformed"`
	}
	if err := json.Unmarshal(data, &response); err != nil {
		return fail(err)
	}
	out := Output{Value: response.Value, Stdout: stdout.String(), Transformed: response.Transformed}
	if response.Error != "" {
		return out, errors.New(response.Error)
	}
	return out, nil
}

// The 1 MiB message/output limit is a demo policy, not a Wasm limit.
const maxMessage = 1 << 20

type boundedText struct{ strings.Builder }

func (b *boundedText) Write(p []byte) (int, error) {
	if b.Len()+len(p) > maxMessage {
		return 0, errors.New("stdout/stderr exceeds 1 MiB")
	}
	return b.Builder.Write(p)
}

// writeResponse is the only Host → Guest memory write for tool results.
func writeResponse(m api.Module, ptr, capacity uint32, data []byte) uint32 {
	if len(data) > maxMessage || uint64(len(data)) > uint64(capacity) || !m.Memory().Write(ptr, data) {
		return ^uint32(0)
	}
	return uint32(len(data))
}