Skip to content

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.so

Build 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:

  1. loadPack {"path":"/abs/app.pck"} (absolute path), or launch with --bridge-pack /abs/app.pck.
  2. instantiateScene {"path":"res://main.tscn"} returns the scene oid; addChild it to the handshake's rootId.
  3. call {"oid":scene,"method":"get_node","args":["%SaveButton"]} returns the node as {"result":{"$oid":"..."}}; use connect, set and call on that oid.

Static linking

bash
zig build-exe main.zig -lc -lstdc++ /path/to/libguido.a

Declare 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 recv is valid until the next recv; copy what you keep (alloc.dupe, as with root above).
  • One thread owns start/iteration/stop; send is thread-safe.
  • 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 iteration calls after the last sends, then stop(), then drain recv() once.