Skip to content

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 PATH

Example (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 oid

In-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.RawMessage for requests, a channel for signals.
  • Batch set/addChild operations into one batch message; send a sync request when a read must observe earlier writes.
  • Route signal notifications by subId from the connect reply.
  • Object handles are opaque; decode as any or string and send back unchanged.
  • Shutdown: closing stdin makes a spawned runtime finish its queue and exit. Under cgo, run two more guido_iteration calls after the last sends, then guido_stop().