rust
61 lines · 8 steps
A type-safe event bus in Rust
How type erasure and TypeId let one registry route events to strongly-typed handlers.
Explained by
highlit
1use std::any::{Any, TypeId};
2use std::collections::HashMap;
3
4pub trait Event: Any {
5 fn as_any(&self) -> &dyn Any;
6}
7
8pub trait Handler<E: Event>: Send + Sync {
9 fn handle(&self, event: &E);
10}
11
12trait ErasedHandler: Send + Sync {
13 fn call(&self, event: &dyn Any);
14}
15
16struct HandlerWrapper<E, H> {
17 handler: H,
18 _marker: std::marker::PhantomData<fn(E)>,
19}
20
21impl<E: Event, H: Handler<E>> ErasedHandler for HandlerWrapper<E, H> {
22 fn call(&self, event: &dyn Any) {
23 if let Some(concrete) = event.downcast_ref::<E>() {
24 self.handler.handle(concrete);
25 }
26 }
27}
28
29#[derive(Default)]
30pub struct Registry {
31 handlers: HashMap<TypeId, Vec<Box<dyn ErasedHandler>>>,
32}
33
34impl Registry {
35 pub fn new() -> Self {
36 Self { handlers: HashMap::new() }
37 }
38
39 pub fn subscribe<E, H>(&mut self, handler: H)
40 where
41 E: Event,
42 H: Handler<E> + 'static,
43 {
44 let wrapper = HandlerWrapper::<E, H> {
45 handler,
46 _marker: std::marker::PhantomData,
47 };
48 self.handlers
49 .entry(TypeId::of::<E>())
50 .or_default()
51 .push(Box::new(wrapper));
52 }
53
54 pub fn dispatch<E: Event>(&self, event: &E) {
55 if let Some(handlers) = self.handlers.get(&TypeId::of::<E>()) {
56 for handler in handlers {
57 handler.call(event.as_any());
58 }
59 }
60 }
61}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Type erasure lets you store heterogeneous generic handlers in one collection behind a uniform trait object.
- 2TypeId keys give you runtime type identity so events reach only handlers registered for their exact type.
- 3A PhantomData marker preserves the erased type parameter needed later for a safe downcast.
Related explainers
rust
pub struct Fibonacci { current: u64, next: u64, }
A Fibonacci iterator in Rust
iterators
traits
lazy-evaluation
Intermediate
6 steps
rust
use std::cmp::Ordering; pub struct PrefixIndex { entries: Vec<String>,
Prefix search with binary partitioning in Rust
binary-search
sorting
case-insensitive
Intermediate
7 steps
rust
use std::cmp::Ordering; use std::str::FromStr; #[derive(Debug, Clone, PartialEq, Eq)]
Parsing and ordering semantic versions in Rust
parsing
trait-implementation
ordering
Intermediate
8 steps
rust
use axum::{ extract::State, routing::{get, post, MethodRouter}, Json, Router,
Self-documenting routes in Axum
builder-pattern
closures
shared-state
Intermediate
8 steps
rust
use std::collections::HashMap; use std::fs::File; use std::io::{self, BufRead, BufReader}; use std::path::Path;
Parsing INI files in Rust
parsing
file-io
hashmap
Intermediate
9 steps
rust
use axum::{ extract::{Path, Query}, http::Uri, response::{IntoResponse, Redirect},
Building HTTP redirect handlers in Axum
redirects
extractors
url-handling
Intermediate
7 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/a-type-safe-event-bus-in-rust-explained-rust-2557/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.