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