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.datato yourMsgPtr, - calls your typed function,
- maps
.ok/.stop/.failback into the C enum.
Cross-links
- Run it: Quickstart
- Type-safety layer: Zig wrapper (
zig/jzx/lib.zig) - Under the hood: C ABI (
include/jzx/jzx.h), Runtime core (src/jzx_runtime.c)
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.totalis the accumulated sum.Message.valueis 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: *CounterStateis the actor’s state pointer.- Owned by the caller (this example keeps it on the stack in
main).
- Owned by the caller (this example keeps it on the stack in
msg: *Messageis the decoded message payload pointer.- The typed wrapper casts
c.jzx_message.datato this pointer type. - Important contract: the payload pointer must be non-null and properly aligned for
Message.
- The typed wrapper casts
ctx: jzx.ActorContextis a small, wrapper-friendly context:- includes
loopandselfid - avoids exposing the full
c.jzx_contextdirectly
- includes
state.total += msg.valueis the “work”.return .stoprequests 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 = CounterStateMsgPtr = *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
stateis that shim - the shim trampoline decodes messages and calls your typed behavior
- allocates a small “shim” object that stores
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 duringloop.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.
- the message is sent before
- If you were scheduling messages with timers or sending from another thread, you would typically heap-allocate the payload (or ensure lifetime another way).
- This is safe in this example because:
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});
}