Skip to main content

Examples

These are drawn directly from the working example programs in spine-go/example/ and spine-zig/src/main.zig. If you copy-paste from this page, it should actually run against a checked-out spine-go/spine-zig and (optionally) a running spined.

Publisher / Subscriber​

package main

import (
"context"
"log/slog"
"os"
"time"

"github.com/poisnoir/spine-go"
)

func main() {
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
ctx := context.Background()
node, _ := spine.CreateNode("common", "publisher_sample", ctx, logger)

pub, _ := spine.NewPublisher[uint32](node, "temperature")

var temp uint32 = 0
for {
pub.Publish(temp)
temp++
time.Sleep(15 * time.Millisecond)
}
}

Note the semantic difference between the two Subscribers, covered on the spine-zig page: Go's Get() returns the latest value (stale intermediates get silently dropped); Zig's next() delivers every message in order, like a stream.

Service / ServiceCaller​

package main

import (
"context"
"log/slog"
"os"

"github.com/poisnoir/spine-go"
)

func main() {
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
ctx := context.Background()
node, _ := spine.CreateNode("common", "service_sample", ctx, logger)

lenFunc := func(i uint32) (uint32, error) {
return i * 2, nil
}
_, _ = spine.NewService(node, "double", lenFunc)

select {} // block forever
}

Either service can be called by either language's caller — this exact pair (a time_two service and caller) has been run cross-language in both directions as separate live processes, not just assumed compatible.

A real cross-language pub/sub pipeline: kinematics-engine​

kinematic-engine (Zig, using red for the actual math) is a real three-node pipeline running today: a Go input node (keyboard-controller or iphone_imu) publishes a [4][4]float64 delta transform, kinematic-engine subscribes to it, solves IK, and publishes the resulting [6]f64 joint angles, which crack-head (also Zig) subscribes to and renders in MuJoCo. All three talk directly to each other over Spine, verified running together as separate live processes — a shell script that builds and launches all three in the right order lives alongside the repo checkouts for this project (run-demo.sh), though it isn't published anywhere on its own:

const spine = @import("spine_zig");
const red = @import("red");

const ArctosRobot = red.RobotType("arctos.urdf");

pub fn main(init: std.process.Init) !void {
const io = init.io;
const allocator = init.arena.allocator();

var node = try spine.Node.init("rime", "kinematic-engine", io, allocator);

const input = try node.subscribe([4][4]f64, "r1-change");
const output = try node.publish([6]f64, "joints");

var arm = ArctosRobot.init();
var current_joints: [6]f64 = .{ 0, 0, 0, 0, 0, 0 };

while (true) {
try output.publish(current_joints);
const delta = try input.next();
// ...forward kinematics, compose delta, inverse kinematics...
}
}

See Kinematic Engine for the full picture, including why this pipeline runs in its own "rime" namespace rather than "common" (and what that means for spined).

What's not real yet​

Earlier drafts of this page showed mDNS-based multi-machine discovery and AES-GCM encrypted namespaces (spine.WithNamespace(...), spine.WithKey(...)). Neither exists in the code — there is no cross-machine transport at all today (see Spine Overview), so those examples have been removed rather than left in as if they were runnable.

See also​