A small, synchronous, trait-object event bus for Rust — type-safe handlers, TTL, and a static global for low-ceremony use.
use std::any::{Any, TypeId};
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use barker::{Message, MessageBus, MessageHandler};
// 1. Any '`static + Send + Sync + Debug` type can be a message.
#[derive(Debug)]
struct Ping(&'static str);
impl Message for Ping {
fn as_any(&self) -> &dyn Any { self }
fn as_any_mut(&mut self) -> &mut dyn Any { self }
}
// 2. Handlers see `&dyn Message`; downcast to the concrete type to read fields.
struct Counter(Arc<AtomicUsize>);
impl MessageHandler for Counter {
fn call(&self, msg: &dyn Message) {
if msg.as_any().downcast_ref::<Ping>().is_some() {
self.0.fetch_add(1, Ordering::SeqCst);
}
}
}
fn main() {
let bus = MessageBus::new();
let count = Arc::new(AtomicUsize::new(0));
bus.register_handler(
Box::new(Counter(count.clone())),
Some(TypeId::of::<Ping>()),
).unwrap();
bus.send(Ping("hello, crowd")).unwrap();
bus.process_messages(None).unwrap();
assert_eq!(count.load(Ordering::SeqCst), 1);
}Prefer a process-wide bus? barker::send, barker::register_handler, and barker::process_messages are free-function wrappers around MessageBus::global().
- Trait-based messages. Any
'static + Send + Sync + Debugtype implementingMessagecan flow through the bus. There is no centralenum, so downstream crates extend the message vocabulary without modifying barker. - Filtering: typed vs generic handlers. Registering a handler with
Some(TypeId::of::<T>())makes it fire only for messages of typeT. Registering withNonemakes it a generic handler that fires for every message. Matching usesstd::any::TypeId. - Buffered send, explicit drain.
sendenqueues onto an internalflumechannel and returns. Handlers do not run untilprocess_messagesis called — typically once per frame, per tick, or at some other coarse cadence your application controls. - TTL enforcement. A message can return
Some(Duration)fromMessage::ttl(); if the drain encounters it past its expiry, it is silently skipped. - Registration-order dispatch. Within a single drain, handlers fire in the order they were registered. Fan-out (multiple handlers for the same
TypeId) is supported and each handler runs to completion before the next.
The Message trait declares priority() and requires_ack(), but the drain does not currently consult either:
priority()is not used to reorder dispatch. Handlers fire in registration order regardless of the priorities of pending messages.requires_ack()has no acknowledgement plumbing. Senders cannot observe whether a message was received.
These methods are preserved on the trait for forward compatibility; document your own conventions per-message, but do not rely on the bus to honour them.
Extracted from the VITRIOL game engine, where the bus decouples input, window, and system events across the plugin architecture.
Dual-licensed under either of MIT or Apache-2.0 at your option.