mirror of
https://github.com/hanzoai/goa.git
synced 2026-09-30 21:55:10 +00:00
One Module/Engine interface with a JSON-in/JSON-out calling convention runs four pure-Go engines behind a Pool that makes single-threaded interpreters serve requests as parallel goroutines: native plain Go handlers, zero overhead goja + esbuild JavaScript and TypeScript (TS transpiled to ES2020) gpython Python, no cgo and no GIL -> true goroutine parallelism wazero Rust/WASM via a trivial string ABI Services mount from a manifest (lang + source + routes), so any Go, Python, Rust or JS backend collapses into a single static CGO_ENABLED=0 binary served on the zip/gofiber app. A Go-backed json stdlib shim lets Python backends keep importing the real module names while running at Go speed. Examples and tests cover every engine; the Rust guest is a real wasm32 fixture exercised end to end.
93 lines
2.8 KiB
Go
93 lines
2.8 KiB
Go
package goa
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sort"
|
|
"sync"
|
|
|
|
"github.com/go-python/gpython/py"
|
|
_ "github.com/go-python/gpython/compile" // wires py.Compile
|
|
_ "github.com/go-python/gpython/stdlib" // wires py.NewContext + built-in stdlib
|
|
)
|
|
|
|
// gpythonEngine runs Python on gpython — a pure-Go interpreter. Because it is
|
|
// plain Go with no shared CPython GIL, each loaded module owns an independent
|
|
// py.Context and N modules in a Pool run N Python calls in true parallel, as
|
|
// ordinary goroutines, inside the CGO_ENABLED=0 static binary. A handler is
|
|
// `def name(payload: str) -> str` returning a JSON string.
|
|
//
|
|
// gpython is "batteries not included"; goa augments it with Go-backed stdlib
|
|
// shims (see stdlib_gpython.go) so real backends keep working in Python while
|
|
// the hot modules (json, re, hashlib, base64) run at Go speed.
|
|
type gpythonEngine struct{}
|
|
|
|
func init() {
|
|
registerGoBackedStdlib() // must run before any context is created
|
|
Register(gpythonEngine{})
|
|
}
|
|
|
|
func (gpythonEngine) Lang() string { return "python" }
|
|
func (gpythonEngine) Aliases() []string { return []string{"py", "python3"} }
|
|
|
|
func (gpythonEngine) Load(_ context.Context, src []byte, opt LoadOptions) (Module, error) {
|
|
pyctx := py.NewContext(py.DefaultContextOpts())
|
|
// ModuleInit compiles src in ExecMode (whole module) and runs it, so every
|
|
// top-level statement executes and all defs land in Globals. (py.RunSrc uses
|
|
// SingleMode — interactive, first-statement-only — which silently drops
|
|
// handlers defined after the first statement.)
|
|
mod, err := pyctx.ModuleInit(&py.ModuleImpl{
|
|
Info: py.ModuleInfo{Name: opt.Name, FileDesc: opt.Name + ".py"},
|
|
CodeSrc: string(src),
|
|
})
|
|
if err != nil {
|
|
_ = pyctx.Close()
|
|
return nil, fmt.Errorf("goa/py: load %q: %w", opt.Name, err)
|
|
}
|
|
return &gpyModule{ctx: pyctx, globals: mod.Globals}, nil
|
|
}
|
|
|
|
type gpyModule struct {
|
|
mu sync.Mutex // a py.Context is single-threaded; Pool gives concurrency
|
|
ctx py.Context
|
|
globals py.StringDict
|
|
}
|
|
|
|
func (m *gpyModule) Invoke(_ context.Context, fn string, payload []byte) ([]byte, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
obj, ok := m.globals[fn]
|
|
if !ok {
|
|
return nil, fmt.Errorf("goa/py: no function %q", fn)
|
|
}
|
|
res, err := py.Call(obj, py.Tuple{py.String(payload)}, nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("goa/py: %s: %w", fn, err)
|
|
}
|
|
switch v := res.(type) {
|
|
case py.String:
|
|
return []byte(v), nil
|
|
case py.Bytes:
|
|
return []byte(v), nil
|
|
case py.NoneType:
|
|
return []byte("null"), nil
|
|
default:
|
|
return nil, fmt.Errorf("goa/py: %s must return a JSON str/bytes, got %s", fn, res.Type().Name)
|
|
}
|
|
}
|
|
|
|
func (m *gpyModule) Funcs() []string {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
var out []string
|
|
for name, obj := range m.globals {
|
|
if _, ok := obj.(*py.Function); ok {
|
|
out = append(out, name)
|
|
}
|
|
}
|
|
sort.Strings(out)
|
|
return out
|
|
}
|
|
|
|
func (m *gpyModule) Close() error { return m.ctx.Close() }
|