Fast & flexible actors for Zig, C & C++ applications
To build:
zig build
See tests/* for many interesting examples.
To run the tests:
zig build test
Thespian is an actor framework where concurrent units ("instances") communicate exclusively via message passing, with no shared mutable state. Messages are encoded using CBOR (Concise Binary Object Representation).
The library has three layers:
- C++ core - the primary implementation (
src/instance.cpp,src/executor_asio.cpp,src/hub.cpp) - C API - a thin wrapper layer in
src/c/exposing the C++ core to other languages - Zig module -
src/thespian.zigwraps the C API into idiomatic Zig types with comptime features
context - the runtime environment. You create one, spawn actors into
it, then call run(). It owns the executor thread pool.
instance / actor - each spawned actor is a behaviour (a
std::function<result()>) that registers a receiver to handle incoming
messages. Actors run on an executor. Currently the only executor provided
in this repo is one backed by ASIO.
handle / pid - a reference to a live actor. In C++ this is
thespian::handle; in Zig it is pid (owned) or pid_ref (borrowed).
Sending to an expired handle is safe and fails gracefully.
message - a CBOR-encoded byte buffer. Pattern matching uses
extract/match with a composable matcher DSL (string, array, any,
more, etc.).
hub - a pub/sub broadcast actor. Subscribers receive all broadcasts;
supports filtering and Unix socket-based IPC.
| Primitive | Purpose |
|---|---|
timeout |
One-shot timer |
metronome |
Repeating timer |
signal |
OS signal handling |
tcp_acceptor / tcp_connector |
TCP networking |
unx_acceptor / unx_connector |
Unix domain sockets |
socket |
Raw FD-backed socket I/O |
file_descriptor |
Async wait on arbitrary file descriptors (posix) |
file_stream |
Async read/write on file handles (win32) |
subprocess |
Child process management (platform-specific) |
Every I/O primitive is owned by the actor that was executing at the time it was created. Async completion messages from a primitive are delivered only to that owning actor. There is no way to transfer ownership or redirect delivery to a different actor after construction.
A common mistake is to create an I/O primitive in one actor and then spawn a separate "handler" actor, passing the primitive (or its handle) to it. The handler will never receive any events - they will continue to be delivered to the original actor that created the primitive.
The correct pattern: the actor responsible for handling a primitive's events must be the one that creates it. If you need a dedicated handler actor, have it create its own primitives from within its own behaviour.
- Zig's
pidisOwned(must calldeinit());pid_refisWrapped(borrowed, no deinit required) - Actors can
trap()exit signals andlink()to other actors for supervision spawn_linkties an actor's lifetime to its parent
- Zig 0.15.2+ required
- Dependencies:
cbor(CBOR codec),asio(ASIO network executor),tracy(optional profiling via-Denable_tracy=true) - Produces a static library
libthespianand a Zig module - Tests live in
test/and cover both the C++ and Zig APIs