Skip to content

Nim

std/dynlib and std/json cover everything; no packages are needed.

Install

bash
tar xzf guido-0.1.0-linux-x86_64.tar.gz
./guido-0.1.0-linux-x86_64/install.sh     # installs ~/.local/lib/libguido.so

Example

A button that responds to clicks (nim r -d:release main.nim):

nim
import std/[dynlib, json, os]

type
  GuidoStart = proc (argc: cint, argv: cstringArray): cint {.cdecl.}
  GuidoIteration = proc (): cint {.cdecl.}
  GuidoSend = proc (line: cstring): cint {.cdecl.}
  GuidoRecv = proc (): cstring {.cdecl.}
  GuidoStop = proc () {.cdecl.}

let lib = loadLib(getEnv("HOME") / ".local/lib/libguido.so")
let guidoStart = cast[GuidoStart](lib.symAddr("guido_start"))
let guidoIteration = cast[GuidoIteration](lib.symAddr("guido_iteration"))
let guidoSend = cast[GuidoSend](lib.symAddr("guido_send"))
let guidoRecv = cast[GuidoRecv](lib.symAddr("guido_recv"))
let guidoStop = cast[GuidoStop](lib.symAddr("guido_stop"))

proc send(v: JsonNode) = discard guidoSend(cstring($v))

let argv = allocCStringArray(["--no-project"])
doAssert guidoStart(1, argv) == 0
deallocCStringArray(argv)   # guido copies the args during start

send(%*{"jsonrpc": "2.0", "id": 1, "method": "handshake",
        "params": {"clientInfo": {"name": "nim", "version": "0"}}})
send(%*{"jsonrpc": "2.0", "id": 2, "method": "instantiate",
        "params": {"className": "Button"}})

var rootId, button: string
while guidoIteration() == 0:   # ends when the window is closed
  while true:
    let p = guidoRecv()
    if p.isNil: break
    let msg = parseJson($p)    # `$` copies; safe past the next recv
    let id = if msg.hasKey("id"): msg["id"].getInt() else: 0
    if id == 1:
      rootId = msg["result"]["rootId"].getStr()
    elif id == 2:
      button = msg["result"]["oid"].getStr()
      send(%*{"jsonrpc": "2.0", "method": "batch", "params": {"ops": [
        {"method": "set", "params": {"oid": button, "property": "text", "value": "Click me"}},
        {"method": "addChild", "params": {"parent": rootId, "child": button}}]}})
      send(%*{"jsonrpc": "2.0", "id": 3, "method": "connect",
              "params": {"oid": button, "signal": "pressed"}})
    elif msg.hasKey("method") and msg["method"].getStr() == "signal":
      echo "clicked"

guidoStop()

A version with request routing is in the repo at examples/embedding/nim/.

Scenes from the Godot editor

The message shapes are documented on the C page and are identical here:

nim
send(%*{"jsonrpc": "2.0", "id": 10, "method": "loadPack",
        "params": {"path": absolutePath("app.pck")}})   # absolute path
send(%*{"jsonrpc": "2.0", "id": 11, "method": "instantiateScene",
        "params": {"path": "res://main.tscn"}})
send(%*{"jsonrpc": "2.0", "id": 12, "method": "call",
        "params": {"oid": scene, "method": "get_node", "args": ["%SaveButton"]}})
# reply: {"result":{"result":{"$oid":"..."}}}; connect "pressed" on that oid

Static linking

nim
{.passL: "/path/to/libguido.a -lstdc++ -lm -lpthread -ldl".}
proc guido_start(argc: cint, argv: cstringArray): cint {.importc, cdecl.}
# the other four likewise (signatures in guido.h)

The result is one self-contained ~22 MB binary. The archive must be built with lto=none; see Building from source.

Notes

  • $cstring copies; do it before the next guidoRecv.
  • One thread owns start/iteration/stop; send is thread-safe. For async programs, run the loop in a thread and forward lines over a Channel.
  • Batch fire-and-forget operations into one batch message per frame; a sync request is the barrier before reads.
  • Route signal notifications by subId from the connect reply.
  • Shutdown: run two more guidoIteration calls after the last sends, then guidoStop(), then drain once for the final quit notification.