Skip to content

This repository contains an experimental actor framework built using the Zig programming language. The framework implements actor-based concurrent programming patterns, including message passing between actors, actor lifecycle management, and state isolation. It leverages the libxev library for its event loop and concurrency.

Notifications You must be signed in to change notification settings

Thomvanoorschot/backstage

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Backstage: Actor Framework for Zig

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 with automatic proxy generation.

Key Features

  • Proxy-Based Actors: Automatic code generation creates type-safe proxies for seamless actor communication
  • Method-Based Communication: Actors expose public methods with automatic parameter serialization
  • Stream-Based Pub/Sub: High-performance streaming system for publish/subscribe patterns
  • Event-Driven Architecture: Built on libxev for high-performance, non-blocking I/O operations
  • Runtime Inspector: Real-time monitoring and debugging of actor systems with a graphical interface
  • Type Safety: Compile-time validation of actor method calls and parameters

How It Works

Backstage uses code generation to create type-safe proxies for your actors. Simply mark your actor structs with // @generate-proxy, define public methods for your business logic, and the framework handles the rest.

Simple Actor Example

const backstage = @import("backstage");
const std = @import("std");
const Context = backstage.Context;
const HelloWorldActorProxy = @import("generated/hello_world_actor_proxy.gen.zig").HelloWorldActorProxy;

// @generate-proxy
pub const HelloWorldActor = struct {
    ctx: *Context,
    allocator: std.mem.Allocator,
    message_count: u32 = 0,

    const Self = @This();

    pub fn init(ctx: *Context, allocator: std.mem.Allocator) !*Self {
        const self = try allocator.create(Self);
        self.* = .{
            .ctx = ctx,
            .allocator = allocator,
        };
        return self;
    }

    pub fn sayHello(self: *Self, name: []const u8) !void {
        self.message_count += 1;
        std.log.info("Hello, {s}! (Message #{d})", .{ name, self.message_count });
    }

    pub fn getCount(self: *Self) u32 {
        return self.message_count;
    }

    pub fn deinit(_: *Self) !void {}
};

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    var engine = try backstage.Engine.init(allocator);
    defer engine.deinit();

    const actor = try engine.getActor(HelloWorldActorProxy, "hello_actor");
    try actor.sayHello("World");
    try actor.sayHello("Zig");

    try engine.loop.run(.once);
}

Actor-to-Actor Communication

// @generate-proxy
pub const SenderActor = struct {
    ctx: *Context,
    allocator: std.mem.Allocator,

    pub fn init(ctx: *Context, allocator: std.mem.Allocator) !*Self {
        // Initialize actor
    }

    pub fn sendMessage(self: *Self, message: []const u8) !void {
        const receiver = try self.ctx.getActor(ReceiverActorProxy, "receiver");
        try receiver.processMessage(message);
    }

    pub fn deinit(_: *Self) !void {}
};

// @generate-proxy
pub const ReceiverActor = struct {
    ctx: *Context,
    allocator: std.mem.Allocator,

    pub fn init(ctx: *Context, allocator: std.mem.Allocator) !*Self {
        // Initialize actor
    }

    pub fn processMessage(self: *Self, message: []const u8) !void {
        std.log.info("Received: {s}", .{message});
    }

    pub fn deinit(_: *Self) !void {}
};

Publish/Subscribe with Streams

// Publisher Actor
pub fn publishNews(self: *Self, headline: []const u8) !void {
    const stream = try self.ctx.getStream([]const u8, "news");
    try stream.next(headline);
}

// Subscriber Actor
pub fn subscribeToNews(self: *Self) !void {
    const stream = try self.ctx.getStream([]const u8, "news");
    try stream.subscribe(
        backstage.newSubscriber("subscriber_id", SubscriberProxy.Method.handleNews)
    );
}

pub fn handleNews(self: *Self, headline: []const u8) !void {
    std.log.info("Breaking news: {s}", .{headline});
}

Installation

Add Backstage to your build.zig.zon:

.dependencies = .{
    .backstage = .{
        .url = "https://github.com/Thomvanoorschot/backstage/archive/main.tar.gz",
        .hash = "...", // Update with actual hash
    },
},

Setup

1. Configure Build System

In your build.zig:

const backstage_dep = b.dependency("backstage", .{
    .target = target,
    .optimize = optimize,
    .generate_proxies = true,
});

// Set up proxy generation
const generator = backstage_dep.artifact("generator");
const run_generator = b.addRunArtifact(generator);
run_generator.addArg("src/generated"); // Output directory
run_generator.addArg("src");           // Source directory to scan
run_generator.addArg("other_dir");     // Multiple directories can be scanned by adding args

const gen_proxies = b.step("gen-proxies", "Generate actor proxies");
gen_proxies.dependOn(&run_generator.step);
b.getInstallStep().dependOn(gen_proxies);

// Add the backstage module to your executable
exe.root_module.addImport("backstage", backstage_dep.module("backstage"));

2. Generate Proxies

The generator can be invoked to create proxy files, but adding the above configuration will generate the proxies on every build.

zig build gen-proxies

This scans your source code for actors marked with // @generate-proxy and creates corresponding proxy files in the src/generated directory.

3. Import and Use

const MyActorProxy = @import("generated/my_actor_proxy.gen.zig").MyActorProxy;

// Use the proxy to interact with your actor
const actor = try engine.getActor(MyActorProxy, "unique_actor_id");
try actor.myMethod(parameters);

Examples

The framework includes comprehensive examples demonstrating various patterns:

  • hello_world_string.zig - Basic actor with simple method calls
  • hello_world_struct.zig - Passing custom structs as parameters
  • actor_to_actor.zig - Direct actor-to-actor communication
  • pub_sub.zig - Publish/subscribe messaging with streams
  • multiple_methods.zig - Actors with multiple business methods
  • large_struct.zig - Handling complex data structures
  • array_list_actor.zig - Working with collections and dynamic data
  • poison_pill.zig - Actor shutdown patterns

Run any example:

cd examples
zig build hello_world_string

Inspector

The inspector provides real-time visibility into your actor system through a graphical interface:

Enable Inspector

zig build -Denable_inspector=true

In your build.zig:

const backstage_dep = b.dependency("backstage", .{
    .target = target,
    .optimize = optimize,
    .enable_inspector = true,
});

if (enable_inspector) {
    const inspector = backstage_dep.artifact("inspector");
    b.installArtifact(inspector);
}

Features

  • Actor Metrics: Monitor all active actors with their IDs and types
  • Message Throughput: Real-time messages per second for individual actors
  • Performance Monitoring: Rolling average throughput calculations
  • Visual Interface: Live updating graphical display of actor system state

Inspector Demo

API Reference

Engine

Core engine for managing the actor system:

var engine = try backstage.Engine.init(allocator);
defer engine.deinit();

// Get or create an actor
const actor = try engine.getActor(ActorProxy, "actor_id");

// Get or create a stream
const stream = try engine.getStream(MessageType, "stream_id");

// Run the event loop
try engine.loop.run(.once);  // Run once
try engine.loop.run(.until_done);  // Run until completion

Context

Actor context provides access to other actors and streams:

// Get another actor
const other_actor = try self.ctx.getActor(OtherActorProxy, "other_id");

// Get a stream
const stream = try self.ctx.getStream(DataType, "stream_id");

// Access current actor ID
const my_id = self.ctx.actor_id;

Streams

Type-safe publish/subscribe communication:

// Publisher
const stream = try ctx.getStream(MessageType, "topic");
try stream.next(message);

// Subscriber
try stream.subscribe(backstage.newSubscriber("actor_id", ActorProxy.Method.handler));

Actor Requirements

Actors must implement:

  • init(ctx: *Context, allocator: std.mem.Allocator) !*Self - Constructor
  • deinit(self: *Self) !void - Destructor
  • Any number of public methods for business logic

Architecture

Code Generation

The proxy generator uses Zig's AST parser to:

  • Discover actors marked with // @generate-proxy
  • Extract public method signatures
  • Generate type-safe proxy wrappers
  • Handle automatic parameter serialization/deserialization

Method Dispatch

  • Method calls are serialized using CBOR
  • Calls are queued and executed asynchronously
  • Type safety is maintained through generated proxy interfaces

Memory Management

  • Actors are lazily initialized on first access
  • Efficient message passing with minimal allocations
  • Stream subscriptions are automatically managed

Performance

  • Single-threaded design: Eliminates locking overhead while maintaining concurrency
  • Efficient serialization: CBOR-based encoding for compact message format
  • Event-driven I/O: Built on libxev for high-performance networking

License

MIT License - see LICENSE file for details.

About

This repository contains an experimental actor framework built using the Zig programming language. The framework implements actor-based concurrent programming patterns, including message passing between actors, actor lifecycle management, and state isolation. It leverages the libxev library for its event loop and concurrency.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages