Appearance
Go
Two options: spawn the runtime and exchange JSON over pipes (no cgo), or load it in-process through a small cgo shim. The pipe approach is simpler and is usually the right choice.
Install
bash
tar xzf guido-0.1.0-linux-x86_64.tar.gz
./guido-0.1.0-linux-x86_64/install.sh # installs `guido` on PATHExample (no cgo)
A button that responds to clicks:
go
package main
import (
"bufio"
"encoding/json"
"fmt"
"os/exec"
)
type msg struct {
ID *int `json:"id,omitempty"`
Method string `json:"method,omitempty"`
Result json.RawMessage `json:"result,omitempty"`
Params map[string]any `json:"params,omitempty"`
}
func main() {
cmd := exec.Command("guido", "--no-project", "--bridge-stdio")
stdin, _ := cmd.StdinPipe()
stdout, _ := cmd.StdoutPipe()
if err := cmd.Start(); err != nil {
panic(err)
}
send := func(v map[string]any) {
b, _ := json.Marshal(v)
stdin.Write(append(b, '\n'))
}
send(map[string]any{"jsonrpc": "2.0", "id": 1, "method": "handshake",
"params": map[string]any{"clientInfo": map[string]any{"name": "go", "version": "0"}}})
send(map[string]any{"jsonrpc": "2.0", "id": 2, "method": "instantiate",
"params": map[string]any{"className": "Button"}})
var rootID, button any
scanner := bufio.NewScanner(stdout)
scanner.Buffer(make([]byte, 1024*1024), 1024*1024)
for scanner.Scan() { // ends when the window is closed
var m msg
if json.Unmarshal(scanner.Bytes(), &m) != nil {
continue
}
switch {
case m.ID != nil && *m.ID == 1:
var r struct{ RootID any `json:"rootId"` }
json.Unmarshal(m.Result, &r)
rootID = r.RootID
case m.ID != nil && *m.ID == 2:
var r struct{ Oid any `json:"oid"` }
json.Unmarshal(m.Result, &r)
button = r.Oid
send(map[string]any{"jsonrpc": "2.0", "method": "batch", "params": map[string]any{"ops": []any{
map[string]any{"method": "set", "params": map[string]any{"oid": button, "property": "text", "value": "Click me"}},
map[string]any{"method": "addChild", "params": map[string]any{"parent": rootID, "child": button}},
}}})
send(map[string]any{"jsonrpc": "2.0", "id": 3, "method": "connect",
"params": map[string]any{"oid": button, "signal": "pressed"}})
case m.Method == "signal":
fmt.Println("clicked")
}
}
cmd.Wait()
}Run with go run .. The runtime is a separate process, so an engine crash cannot crash the Go program.
Scenes from the Godot editor
The message shapes are documented on the C page and are identical here:
go
abs, _ := filepath.Abs("app.pck") // runtime paths must be absolute
send(map[string]any{"jsonrpc": "2.0", "id": 10, "method": "loadPack",
"params": map[string]any{"path": abs}})
send(map[string]any{"jsonrpc": "2.0", "id": 11, "method": "instantiateScene",
"params": map[string]any{"path": "res://main.tscn"}})
send(map[string]any{"jsonrpc": "2.0", "id": 12, "method": "call",
"params": map[string]any{"oid": scene, "method": "get_node", "args": []any{"%SaveButton"}}})
// reply: {"result":{"result":{"$oid":"..."}}}; connect "pressed" on that oidIn-process (cgo)
For a single process, dlopen the library through a cgo shim. The complete version is in the repo at examples/embedding/go/; the structure:
go
func main() {
// Required: Go moves goroutines between OS threads, but the engine's
// start/iteration/stop must stay on one thread.
runtime.LockOSThread()
C.guido_load(C.CString(libPath)) // dlopen + dlsym shim in the cgo preamble
C.guido_start1(C.CString("--no-project"))
for C.guido_iteration() == 0 {
for p := C.guido_recv(); p != nil; p = C.guido_recv() {
handle(C.GoString(p)) // GoString copies; safe to keep
}
}
C.guido_stop()
}Notes
- Let one goroutine own the connection (the scanner loop, or the locked pump goroutine under cgo) and route replies through channels:
map[int64]chan json.RawMessagefor requests, a channel for signals. - Batch
set/addChildoperations into onebatchmessage; send asyncrequest when a read must observe earlier writes. - Route
signalnotifications bysubIdfrom theconnectreply. - Object handles are opaque; decode as
anyorstringand send back unchanged. - Shutdown: closing stdin makes a spawned runtime finish its queue and exit. Under cgo, run two more
guido_iterationcalls after the last sends, thenguido_stop().