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
use serde::Deserialize; #[derive(Debug, Deserialize)] #[serde(untagged)]
Parsing flexible JSON shapes with serde
deserialization
enums
json
Intermediate
6 steps
rust
use std::cmp::{Ordering, Reverse}; use std::collections::BinaryHeap; use std::fs::File; use std::io::{self, BufRead, BufReader, BufWriter, Lines, Write};
K-way merge of sorted logs in Rust
binary-heap
k-way-merge
streaming-io
Intermediate
8 steps
rust
use axum::{ extract::{Path, State}, response::sse::{Event, KeepAlive, Sse}, };
Streaming import progress with SSE in Axum
server-sent-events
streams
watch-channel
Advanced
7 steps
rust
use std::f64::consts::PI; #[derive(Debug, Clone, Copy)] pub struct LatLng {
Building geographic bounding boxes in Rust
geospatial
value-types
option
Intermediate
7 steps
rust
use chrono::{Duration, NaiveDate}; #[derive(Debug)] pub struct DateRange {
Parsing and iterating date ranges in Rust
error-handling
iterators
parsing
Intermediate
7 steps
rust
use std::collections::VecDeque; use std::sync::{Arc, Condvar, Mutex}; use std::time::{Duration, Instant};
Building a counting semaphore in Rust
concurrency
synchronization
condition-variable
Advanced
9 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.