Appearance
Rust
Load the runtime with libloading and model messages with serde_json, or link statically for a single-file binary.
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
cargo add libloading serde_jsonExample
A button that responds to clicks:
rust
use libloading::{Library, Symbol};
use serde_json::{json, Value};
use std::ffi::{CStr, CString};
use std::os::raw::{c_char, c_int};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let lib_path = std::env::var("GUIDO_LIB")
.unwrap_or_else(|_| format!("{}/.local/lib/libguido.so", std::env::var("HOME").unwrap()));
let lib = unsafe { Library::new(&lib_path)? };
let start: Symbol<unsafe extern "C" fn(c_int, *const *const c_char) -> c_int> =
unsafe { lib.get(b"guido_start")? };
let iteration: Symbol<unsafe extern "C" fn() -> c_int> = unsafe { lib.get(b"guido_iteration")? };
let send_raw: Symbol<unsafe extern "C" fn(*const c_char) -> c_int> = unsafe { lib.get(b"guido_send")? };
let recv: Symbol<unsafe extern "C" fn() -> *const c_char> = unsafe { lib.get(b"guido_recv")? };
let stop: Symbol<unsafe extern "C" fn()> = unsafe { lib.get(b"guido_stop")? };
let args = [CString::new("--no-project")?];
let argv: Vec<*const c_char> = args.iter().map(|a| a.as_ptr()).collect();
unsafe { start(argv.len() as c_int, argv.as_ptr()) };
let send = |v: Value| {
let line = CString::new(v.to_string()).unwrap();
unsafe { send_raw(line.as_ptr()) };
};
send(json!({"jsonrpc":"2.0","id":1,"method":"handshake",
"params":{"clientInfo":{"name":"rust","version":"0"}}}));
send(json!({"jsonrpc":"2.0","id":2,"method":"instantiate","params":{"className":"Button"}}));
let (mut root, mut button) = (Value::Null, Value::Null);
loop {
if unsafe { iteration() } != 0 { break; } // ends when the window closes
loop {
let p = unsafe { recv() };
if p.is_null() { break; }
// The buffer is reused on the next recv; take ownership now.
let line = unsafe { CStr::from_ptr(p) }.to_string_lossy().into_owned();
let msg: Value = serde_json::from_str(&line)?;
match msg["id"].as_i64() {
Some(1) => root = msg["result"]["rootId"].clone(),
Some(2) => {
button = msg["result"]["oid"].clone();
send(json!({"jsonrpc":"2.0","method":"batch","params":{"ops":[
{"method":"set","params":{"oid":button,"property":"text","value":"Click me"}},
{"method":"addChild","params":{"parent":root,"child":button}}]}}));
send(json!({"jsonrpc":"2.0","id":3,"method":"connect",
"params":{"oid":button,"signal":"pressed"}}));
}
None if msg["method"] == "signal" => println!("clicked"),
_ => {}
}
}
}
unsafe { stop() };
Ok(())
}A version with request routing and error handling is in the repo at examples/embedding/rust/.
Scenes from the Godot editor
The message shapes are documented on the C page and are identical here:
rust
send(json!({"jsonrpc":"2.0","id":10,"method":"loadPack",
"params":{"path":"/abs/app.pck"}})); // absolute path
send(json!({"jsonrpc":"2.0","id":11,"method":"instantiateScene",
"params":{"path":"res://main.tscn"}}));
send(json!({"jsonrpc":"2.0","id":12,"method":"call",
"params":{"oid":scene,"method":"get_node","args":["%SaveButton"]}}));
// reply: {"result":{"result":{"$oid":"..."}}}; connect "pressed" on that oidDeserializing object references:
rust
#[derive(serde::Deserialize)]
struct ObjRef { #[serde(rename = "$oid")] oid: String }Static linking
Linking libguido.a removes the dlopen step and produces one ~22 MB self-contained binary. In build.rs:
rust
println!("cargo:rustc-link-search=native=/path/to/guido/lib");
println!("cargo:rustc-link-lib=static=guido");
println!("cargo:rustc-link-lib=dylib=stdc++");
// plus: m, pthread, dlDeclare the five functions as extern "C" (signatures in guido.h). The archive must be built with lto=none; see Building from source.
Structuring a larger app
- Run the engine on a dedicated thread: the thread that calls
guido_startmust also runguido_iterationandguido_stop. Bridge to the rest of the program with channels, for exampleHashMap<u64, oneshot::Sender<Value>>for replies and anmpscchannel for signals.guido_sendis thread-safe, so any thread can send directly. - Keep the
Libraryalive for the whole process (for example withBox::leak); the engine cannot be unloaded. - Batch fire-and-forget operations into one
batchmessage per frame; send asyncrequest when a read must observe earlier writes. - Route
signalnotifications bysubIdfrom theconnectreply. - Shutdown: run two more
iterationcalls after the last sends, thenstop(), then drainrecv()once. - To avoid FFI entirely, spawn
guido(on PATH) with--bridge-stdioviastd::process::Commandand exchange the same JSON over its pipes.