Skip to main content
Version: v0.0.1

Zig example — examples/zig/typed_actor.zig

This example uses the typed actor wrapper from zig/jzx/lib.zig to avoid “void pointer everywhere” ergonomics.

Instead of writing a raw C-ABI behavior like:

  • fn behavior(ctx: [*c]c.jzx_context, msg: [*c]const c.jzx_message) ...

you write a typed behavior like:

  • fn behavior(state: *State, msg: *Message, ctx: jzx.ActorContext) jzx.BehaviorResult

The wrapper builds a trampoline that:

  • receives the C message,
  • casts message.data to your MsgPtr,
  • calls your typed function,
  • maps .ok/.stop/.fail back into the C enum.

Imports

Imports
const std = @import("std");
const jzx = @import("jzx");
const c = jzx.c;

Typed state and message

State + message types
const CounterState = struct {
total: u32 = 0,
};

const Message = struct {
value: u32,
};

Interpretation:

  • CounterState.total is the accumulated sum.
  • Message.value is the increment carried by each message.

Why these are structs:

  • They’re stable, named layouts that are easy to pass by pointer.
  • They map cleanly to “bytes on the wire” if you later want to serialize them.

The typed behavior function

counterBehavior(): typed state + typed message
fn counterBehavior(state: *CounterState, msg: *Message, ctx: jzx.ActorContext) jzx.BehaviorResult {
_ = ctx;
state.total += msg.value;
return .stop;
}

Line-by-line intent:

  • state: *CounterState is the actor’s state pointer.
    • Owned by the caller (this example keeps it on the stack in main).
  • msg: *Message is the decoded message payload pointer.
    • The typed wrapper casts c.jzx_message.data to this pointer type.
    • Important contract: the payload pointer must be non-null and properly aligned for Message.
  • ctx: jzx.ActorContext is a small, wrapper-friendly context:
    • includes loop and self id
    • avoids exposing the full c.jzx_context directly
  • state.total += msg.value is the “work”.
  • return .stop requests a clean stop after one message.

main(): spawn typed actor, send message, run

main(): spawn and run
pub fn main() !void {
var loop = try jzx.Loop.create(null);
defer loop.deinit();

var counter = CounterState{};
var actor = try jzx.Actor(CounterState, *Message).spawn(
loop.ptr,
std.heap.c_allocator,
&counter,
&counterBehavior,
.{},
);
defer actor.destroy();

var msg = Message{ .value = 42 };
_ = c.jzx_send(loop.ptr, actor.getId(), &msg, @sizeOf(Message), 0);
try loop.run();

std.debug.print("Counter total = {d}\n", .{counter.total});
}

Deep explanation:

  • jzx.Actor(CounterState, *Message) is a compile-time specialization:
    • State = CounterState
    • MsgPtr = *Message (must be a pointer type)
  • .spawn(loop.ptr, allocator, state_ptr, behavior_ptr, opts):
    • allocates a small “shim” object that stores { behavior, state }
    • spawns a runtime actor whose state is that shim
    • the shim trampoline decodes messages and calls your typed behavior
  • defer actor.destroy():
    • frees the wrapper’s shim allocation
    • important: this does not stop the runtime actor; it only frees wrapper-owned memory
    • why it’s safe here: the behavior returns .stop, so the actor stops during loop.run()
  • var msg = Message{ .value = 42 }; is stack allocated.
    • This is safe in this example because:
      • the message is sent before loop.run(), and
      • loop.run() does not return until the message has been processed and the actor stops.
    • If you were scheduling messages with timers or sending from another thread, you would typically heap-allocate the payload (or ensure lifetime another way).

Full listing (for reference)

examples/zig/typed_actor.zig
const std = @import("std");
const jzx = @import("jzx");
const c = jzx.c;

const CounterState = struct {
total: u32 = 0,
};

const Message = struct {
value: u32,
};

fn counterBehavior(state: *CounterState, msg: *Message, ctx: jzx.ActorContext) jzx.BehaviorResult {
_ = ctx;
state.total += msg.value;
return .stop;
}

pub fn main() !void {
var loop = try jzx.Loop.create(null);
defer loop.deinit();

var counter = CounterState{};
var actor = try jzx.Actor(CounterState, *Message).spawn(
loop.ptr,
std.heap.c_allocator,
&counter,
&counterBehavior,
.{},
);
defer actor.destroy();

var msg = Message{ .value = 42 };
_ = c.jzx_send(loop.ptr, actor.getId(), &msg, @sizeOf(Message), 0);
try loop.run();

std.debug.print("Counter total = {d}\n", .{counter.total});
}