Appearance
Zig
std.DynLib loads the runtime without any packages. One flag is required.
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.soBuild with -lc
Without -lc, Zig uses its own ELF loader, which cannot load a C++ shared library; the process crashes inside the runtime. With -lc, std.DynLib uses the system dlopen. Compile every guido host with -lc.
Example
A button that responds to clicks (zig run main.zig -lc):
zig
const std = @import("std");
const GuidoStart = *const fn (c_int, [*]const [*:0]const u8) callconv(.c) c_int;
const GuidoIteration = *const fn () callconv(.c) c_int;
const GuidoSend = *const fn ([*:0]const u8) callconv(.c) c_int;
const GuidoRecv = *const fn () callconv(.c) ?[*:0]const u8;
const GuidoStop = *const fn () callconv(.c) void;
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const alloc = gpa.allocator();
const home = std.posix.getenv("HOME") orelse return error.NoHome;
const lib_path = try std.fmt.allocPrint(alloc, "{s}/.local/lib/libguido.so", .{home});
var lib = try std.DynLib.open(lib_path);
const start = lib.lookup(GuidoStart, "guido_start") orelse return error.MissingSymbol;
const iteration = lib.lookup(GuidoIteration, "guido_iteration") orelse return error.MissingSymbol;
const send = lib.lookup(GuidoSend, "guido_send") orelse return error.MissingSymbol;
const recv = lib.lookup(GuidoRecv, "guido_recv") orelse return error.MissingSymbol;
const stop = lib.lookup(GuidoStop, "guido_stop") orelse return error.MissingSymbol;
const args = [_][*:0]const u8{"--no-project"};
if (start(args.len, &args) != 0) return error.StartFailed;
_ = send("{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"handshake\",\"params\":{\"clientInfo\":{\"name\":\"zig\",\"version\":\"0\"}}}");
_ = send("{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"instantiate\",\"params\":{\"className\":\"Button\"}}");
var root: []const u8 = "";
while (iteration() == 0) { // ends when the window is closed
while (recv()) |p| {
const line = std.mem.span(p); // valid until the next recv
const parsed = try std.json.parseFromSlice(std.json.Value, alloc, line, .{});
defer parsed.deinit();
const obj = parsed.value.object;
const id: i64 = if (obj.get("id")) |v| switch (v) {
.integer => |i| i,
.float => |f| @intFromFloat(f),
else => 0,
} else 0;
if (id == 1) {
root = try alloc.dupe(u8, obj.get("result").?.object.get("rootId").?.string);
} else if (id == 2) {
const oid = obj.get("result").?.object.get("oid").?.string;
var buf: [512]u8 = undefined;
_ = send(try std.fmt.bufPrintZ(&buf, "{{\"jsonrpc\":\"2.0\",\"method\":\"batch\",\"params\":{{\"ops\":[{{\"method\":\"set\",\"params\":{{\"oid\":\"{s}\",\"property\":\"text\",\"value\":\"Click me\"}}}},{{\"method\":\"addChild\",\"params\":{{\"parent\":\"{s}\",\"child\":\"{s}\"}}}}]}}}}", .{ oid, root, oid }));
_ = send(try std.fmt.bufPrintZ(&buf, "{{\"jsonrpc\":\"2.0\",\"id\":3,\"method\":\"connect\",\"params\":{{\"oid\":\"{s}\",\"signal\":\"pressed\"}}}}", .{oid}));
} else if (obj.get("method")) |m| {
if (std.mem.eql(u8, m.string, "signal")) std.debug.print("clicked\n", .{});
}
}
}
stop();
}A version with request routing is in the repo at examples/embedding/zig/.
Scenes from the Godot editor
The message shapes are documented on the C page and are identical here:
loadPack {"path":"/abs/app.pck"}(absolute path), or launch with--bridge-pack /abs/app.pck.instantiateScene {"path":"res://main.tscn"}returns the scene oid;addChildit to the handshake'srootId.call {"oid":scene,"method":"get_node","args":["%SaveButton"]}returns the node as{"result":{"$oid":"..."}}; useconnect,setandcallon that oid.
Static linking
bash
zig build-exe main.zig -lc -lstdc++ /path/to/libguido.aDeclare the five functions as extern (signatures in guido.h) instead of using DynLib. The result is one self-contained ~22 MB binary. The archive must be built with lto=none; see Building from source.
Notes
- Data from
recvis valid until the nextrecv; copy what you keep (alloc.dupe, as withrootabove). - One thread owns
start/iteration/stop;sendis thread-safe. - Batch fire-and-forget operations into one
batchmessage per frame; asyncrequest is the barrier before reads. - Route
signalnotifications bysubIdfrom theconnectreply. - Shutdown: run two more
iterationcalls after the last sends, thenstop(), then drainrecv()once.