Files
goa/stdlib_gpython.go
T
Hanzo Dev 54a40e19c1 goa: pure-Go polyglot embedding (Go, Python, TS/JS, Rust/WASM)
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.
2026-06-28 19:14:19 -07:00

147 lines
3.6 KiB
Go

package goa
import (
"encoding/json"
"fmt"
"github.com/go-python/gpython/py"
)
// registerGoBackedStdlib installs Go-native implementations of stdlib modules
// gpython does not ship ("batteries not included"). The win is twofold: our
// Python backends keep importing the real module names, and the hot,
// C-accelerated-in-CPython modules run at Go speed instead of interpreted.
//
// This file ships `json` (encoding/json). The same pattern extends to re
// (regexp), hashlib (crypto), base64 (encoding/base64), datetime (time), uuid —
// each a thin module whose methods marshal py.Object <-> Go and call Go stdlib.
func registerGoBackedStdlib() {
py.RegisterModule(&py.ModuleImpl{
Info: py.ModuleInfo{
Name: "json",
Doc: "Go-backed json (encoding/json) for gpython.",
},
Methods: []*py.Method{
py.MustNewMethod("dumps", jsonDumps, 0, "dumps(obj) -> str"),
py.MustNewMethod("loads", jsonLoads, 0, "loads(s) -> obj"),
},
})
}
func jsonDumps(_ py.Object, args py.Tuple) (py.Object, error) {
if len(args) < 1 {
return nil, py.ExceptionNewf(py.TypeError, "dumps() missing 1 argument")
}
gv, err := pyToGo(args[0])
if err != nil {
return nil, err
}
b, err := json.Marshal(gv)
if err != nil {
return nil, py.ExceptionNewf(py.ValueError, "json.dumps: %v", err)
}
return py.String(b), nil
}
func jsonLoads(_ py.Object, args py.Tuple) (py.Object, error) {
if len(args) < 1 {
return nil, py.ExceptionNewf(py.TypeError, "loads() missing 1 argument")
}
var s string
switch v := args[0].(type) {
case py.String:
s = string(v)
case py.Bytes:
s = string(v)
default:
return nil, py.ExceptionNewf(py.TypeError, "loads() expects str/bytes, got %s", args[0].Type().Name)
}
var gv interface{}
if err := json.Unmarshal([]byte(s), &gv); err != nil {
return nil, py.ExceptionNewf(py.ValueError, "json.loads: %v", err)
}
return goToPy(gv), nil
}
// pyToGo converts a gpython object into a json-marshalable Go value.
func pyToGo(o py.Object) (interface{}, error) {
switch v := o.(type) {
case nil, py.NoneType:
return nil, nil
case py.String:
return string(v), nil
case py.Bytes:
return string(v), nil
case py.Bool:
return bool(v), nil
case py.Int:
return int64(v), nil
case py.Float:
return float64(v), nil
case py.Tuple:
return seqToGo(v)
case *py.List:
return seqToGo(v.Items)
case py.StringDict:
// gpython represents every dict as a string-keyed StringDict.
m := make(map[string]interface{}, len(v))
for k, val := range v {
gv, err := pyToGo(val)
if err != nil {
return nil, err
}
m[k] = gv
}
return m, nil
default:
return nil, py.ExceptionNewf(py.TypeError, "json: cannot serialize %s", o.Type().Name)
}
}
func seqToGo(items []py.Object) (interface{}, error) {
out := make([]interface{}, len(items))
for i, it := range items {
gv, err := pyToGo(it)
if err != nil {
return nil, err
}
out[i] = gv
}
return out, nil
}
// goToPy converts a decoded JSON Go value into gpython objects.
func goToPy(v interface{}) py.Object {
switch t := v.(type) {
case nil:
return py.None
case bool:
if t {
return py.True
}
return py.False
case string:
return py.String(t)
case float64:
// json numbers decode to float64; keep integers integral where exact.
if t == float64(int64(t)) {
return py.Int(int64(t))
}
return py.Float(t)
case []interface{}:
items := make([]py.Object, len(t))
for i, e := range t {
items[i] = goToPy(e)
}
return py.NewListFromItems(items)
case map[string]interface{}:
d := py.NewStringDict()
for k, e := range t {
d[k] = goToPy(e)
}
return d
default:
return py.String(fmt.Sprintf("%v", t))
}
}