Thomvanoorschot/backstage
This repository contains an experimental actor framework built using the Zig programming language. The framework implements actor-based concurrent pro...
main.tar.gz
Backstage is a high-performance, event-driven actor framework for the Zig programming language. Built on top of libxev, it provides a robust foundation for building concurrent applications using the actor model pattern.
The main goal of this project is to gain deeper understanding of the programming language while building something practical and useful. By implementing an actor framework from scratch, this project explores:
The framework consists of several core components:
Add Backstage to your build.zig.zon
:
.dependencies = .{
.backstage = .{
.url = "https://github.com/Thomvanoorschot/backstage/archive/main.tar.gz",
.hash = "...", // Update with actual hash
},
},
Or use zig fetch:
zig fetch --save https://github.com/Thomvanoorschot/backstage/archive/main.tar.gz
const std = @import("std");
const backstage = @import("backstage");
const Envelope = backstage.Envelope;
// Define your actor
const MyActor = struct {
ctx: *backstage.Context,
allocator: std.mem.Allocator,
const Self = @This();
pub fn init(ctx: *backstage.Context, allocator: std.mem.Allocator) !*Self {
const self = try allocator.create(Self);
self.* = .{
.ctx = ctx,
.allocator = allocator,
};
return self;
}
pub fn receive(self: *Self(), envelope: Envelope) !void {
defer envelope.deinit(self.allocator);
// This example shows zig-protobuf encoded payloads, any encoding (or none at all) would work
const actor_msg: MyActorMessage = try MyActorMessage.decode(message.payload, self.allocator);
if (actor_msg.message == null) {
return error.InvalidMessage;
}
switch (actor_msg.message.?) {
.init => |m| {
std.log.info("Received message {}", .{m.example});
}
}
}
pub fn deinit(self: *Self) !void {
try self.ctx.shutdown();
}
};
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
// Initialize the engine
var engine = try backstage.Engine.init(allocator);
defer engine.deinit();
// Spawn actors
const publisher = try engine.spawnActor(MyActor, .{
.id = "publisher",
.capacity = 1024,
});
const subscriber = try engine.spawnActor(MyActor, .{
.id = "subscriber",
.capacity = 1024,
});
// Subscribe to the publisher's default topic
try subscriber.ctx.subscribeToActor("publisher");
// Or subscribe to a specific topic
try subscriber.ctx.subscribeToActorTopic("publisher", "news");
// Publish messages
// Normaly you would probably send some more complex encoded struct, it is able
// to handle structs that have a method with the following signature:
// pub fn encode(self: Self, allocator: Allocator) anyerror![]u8
try publisher.ctx.publish("Hello, subscribers!");
try publisher.ctx.publishToTopic("news", "Breaking news!");
try publisher.ctx.publishToTopic("news", MyActorMessage{
.message = .{ .input = "Hello, World!" },
});
// Send direct messages
try engine.send(null, "subscriber", "Direct message");
try engine.send(null, "subscriber", MyActorMessage{
.message = .{ .input = "Hello, World!" },
});
// Run the event loop
try engine.run();
}
// Publish to the default topic
try actor.ctx.publish("Hello, world!");
// Publish to a specific topic
try actor.ctx.publishToTopic("events", "Something happened!");
// Subscribe to an actor's default topic
try subscriber.ctx.subscribeToActor("publisher-id");
// Subscribe to a specific topic from an actor
try subscriber.ctx.subscribeToActorTopic("publisher-id", "events");
// Unsubscribe from topics
try subscriber.ctx.unsubscribeFromActor("publisher-id");
try subscriber.ctx.unsubscribeFromActorTopic("publisher-id", "events");
Actors receive both direct messages and published messages through the same receive
method, differentiated by the message_type
field in the envelope. You could add some logic based on this, but you don't have to:
pub fn receive(self: *Self, envelope: Envelope) !void {
switch (envelope.message_type) {
.send => {
// Handle direct point-to-point messages
},
.publish => {
// Handle messages from subscribed topics
},
else => {},
}
}
For comprehensive examples and real-world usage, see the Zigma algorithmic trading framework, which demonstrates advanced patterns.
The Engine
is the central component that manages the actor system:
init(allocator)
- Initialize a new engine instancespawnActor(ActorType, options)
- Create and register a new actorsend(sender, id, message_type, message)
- Send a message to a specific actor by IDrun()
- Start the event loopdeinit()
- Clean up resourcesThe Context
provides actors with communication and lifecycle management capabilities:
send(target_id, message)
- Send a direct message to another actorpublish(message)
- Publish a message to the default topicpublishToTopic(topic, message)
- Publish a message to a specific topicsubscribeToActor(target_id)
- Subscribe to another actor's default topicsubscribeToActorTopic(target_id, topic)
- Subscribe to a specific topic from another actorunsubscribeFromActor(target_id)
- Unsubscribe from another actor's default topicunsubscribeFromActorTopic(target_id, topic)
- Unsubscribe from a specific topicspawnActor(ActorType, options)
- Spawn a new actorspawnChildActor(ActorType, options)
- Spawn a child actor with supervisionshutdown()
- Clean shutdown with automatic subscription cleanupActors must implement:
init(ctx, allocator)
- Actor initializationreceive(envelope)
- Message handling for both direct and published messagesdeinit()
- Optional cleanup (automatically detected)Message wrapper containing:
sender_id
- ID of the sending actor (optional)message_type
- Type of message (send, publish, subscribe, unsubscribe)message
- The actual message payloadThis is an early-stage implementation, focusing on core actor framework concepts with topic-based messaging. The project is primarily meant as a learning exercise and may evolve significantly as understanding of both the language and actor patterns deepens.
MIT License - see LICENSE file for details.