Learning by Doing: Event Loop in Rust
LuisC
Posted on June 9, 2024
Before you read, concepts like: concurrency, traits, Arc/Mutex and Rust Channles (crossbeam lib) should be clear for you.
Image by Huso112
Discover rust using known concepts
As a seasoned Java software developer, Iām always on the lookout for new challenges to further my understanding of different programming languages. Recently, after completing a project involving Event Loops with Vert.x, I found myself intrigued by the idea of exploring Rust more deeply. With its rising popularity in the tech community, Rust seemed like the perfect next step in my programming journey. And so, with the experience of working with Vert.x still fresh in my mind, I thought, "Why not continue this exploration with Rust?"
What is and why an event loop?
An event loop is a fundamental concept in programming that serves as a mechanism for managing asynchronous operations and ensuring system responsiveness. It functions by continuously checking for new events or messages within a program, processing them as needed, and then returning to check for more. This pattern is essential for handling tasks such as input/output operations, network communication, and user interactions without blocking the execution of other code.
The significance of an event loop lies in its ability to efficiently handle multiple tasks concurrently without relying on traditional synchronous methods that can lead to performance bottlenecks and unresponsive applications. By allowing the program to asynchronously process events as they occur, an event loop enables smoother and more efficient execution, making it a crucial component in modern software development, particularly in environments where responsiveness is a must.
Some use cases for event loops
- Graphical User Interfaces (GUIs): Handling user interactions like mouse clicks and key presses.
- Networking: Managing asynchronous I/O operations, such as incoming and outgoing network requests.
- Game Development: Processing game states, rendering, and handling player inputs.
- IoT Devices: Responding to sensor data and external commands asynchronously.
Implementing an event loop in Rust
Here is an example implementation of an event loop in Rust:
#![allow(dead_code)]
use std::sync::{Arc, Mutex};
use std::{collections::HashMap, thread};
use crossbeam::channel::{unbounded, Receiver, Sender};
use strum_macros::{Display, EnumString};
#[derive(Clone, Debug, PartialEq, Eq, Hash, Display, EnumString)]
pub enum Event {
Dummy,
ExampleEvent,
}
pub type Payload = Vec<u8>;
pub trait Handler: Send + Sync {
fn handle(&self, event: Event, payload: Payload);
}
#[derive(Clone)]
pub struct Listener {
pub event: Event,
pub handler: Arc<dyn Handler>,
}
pub struct Dispatcher {
tx: Sender<(Event, Payload)>,
rx: Receiver<(Event, Payload)>,
registry: Arc<Mutex<HashMap<Event, Vec<Arc<dyn Handler>>>>>,
}
impl Dispatcher {
pub fn new() -> Self {
let (tx, rx) = unbounded();
Dispatcher {
tx,
rx,
registry: Arc::new(Mutex::new(HashMap::new())),
}
}
pub fn register_handler(&mut self, event: Event, handler: Arc<dyn Handler>) {
let mut registry = self.registry.lock().unwrap();
registry.entry(event).or_insert_with(Vec::new).push(handler);
}
pub fn trigger_event(&self, event: Event, payload: Payload) {
self.tx.send((event, payload)).unwrap();
}
pub fn start(&self) {
let registry = Arc::clone(&self.registry);
let rx = self.rx.clone();
thread::spawn(move || loop {
if let Ok((event, payload)) = rx.recv() {
let registry = registry.lock().unwrap();
if let Some(handlers) = registry.get(&event) {
for handler in handlers {
handler.handle(event.clone(), payload.clone());
}
}
}
});
}
}
Explanation
Event Enum
#[derive(Clone, Debug, PartialEq, Eq, Hash, Display, EnumString)]
pub enum Event {
Dummy,
TestEvent,
}
Event
is an enumeration representing the types of events that the event loop can handle. The Display
and EnumString
derives are used for easier handling and conversion of enum values.
Type Aliases
pub type Payload = Vec<u8>;
Payload
is defined as a type alias for Vec<u8>
, representing the data associated with an event.
Handler
pub trait Handler: Send + Sync {
fn handle_event(&self, event: Event, payload: Payload);
}
Handler
is a trait that any event handler must implement. It requires a single method, handle_event
, which takes an Event
and Payload
. We will elaborate more on the Send
& Sync
traits later.
Listener Struct
#[derive(Clone)]
pub struct Listener {
pub event: Event,
pub handler: Arc<dyn Handler>,
}
The Listener
struct binds an event
to a handler
implementing the Handler
, allowing the addition of multiple handlers for each event.
Dispatcher Struct
pub struct Dispatcher {
tx: Sender<(Event, Payload)>,
rx: Receiver<(Event, Payload)>,
registry: Arc<Mutex<HashMap<Event, Vec<Arc<dyn Handler>>>>>,
}
Dispatcher
holds the sender and receiver for event channels and a thread-safe collection of handlers.
About Send and Sync traits
Trait
Send
Since the event loop runs in its own thread and potentially interacts with multiple handlers across different threads, handlers might need to be moved between threads. By requiring
Send
, you ensure that your handler implementations can be transferred across thread boundaries, making your event loop safe and robust in a multithreaded context.Trait
Sync
Handlers are stored in a shared
Arc<Mutex<HashMap<Event, Vec<Arc<dyn Handler>>>>>
. TheArc
(atomic reference count) allows multiple threads to share ownership of the handlers without needing to clone them. By requiringSync
, you ensure that multiple threads can hold references to the same handler safely. This means any read access to the handler's state is thread-safe.
Let's try it
Handler examples
pub struct TestEventHandler;
impl Handler for TestEventHandler {
fn handle_event(&self, event: Event, payload: Payload) {
let data = String::from_utf8(payload).unwrap();
let message = format!("{} => {}", event, data);
info!("TestEvent: {}", message);
}
}
pub struct DBTestEventHandler;
impl Handler for DBTestEventHandler {
fn handle_event(&self, event: Event, payload: Payload) {
let data = String::from_utf8(payload).unwrap();
let message = format!("{} => {}", event, data);
// Persist data into db
info!("Data saved on DB!");
}
}
Main function example
fn main() {
env_logger::builder()
.filter_level(log::LevelFilter::Info)
.init();
let mut event_loop = Dispatcher::new();
event_loop.register_handler(Event::TestEvent, Arc::new(TestEventHandler));
event_loop.register_handler(Event::TestEvent, Arc::new(DBTestEventHandler));
// Start the event loop
event_loop.start();
loop {
info!("Give me some input, type 'exit' to quit");
let mut input = String::new();
io::stdin()
.read_line(&mut input)
.expect("Error during input");
let input = input.trim();
if input == "exit" {
break;
}
let mut split = input.split_whitespace();
let name_data = (
split.next().unwrap_or_default().to_string(),
split.next().unwrap_or_default().to_string(),
);
let event = Event::from_str(&name_data.0).unwrap_or_else(|_| Event::Dummy);
event_loop.trigger_event(event, name_data.1.as_bytes().to_vec());
}
}
What's next
I have several ideas running through my head, but the ones I would like to implement are:
A shared, observable state between handlers. It would be very interesting to have handlers that are activated based on a certain state.
Taking a cue from Vert.x, the possibility of having remote handlers. To begin with, one could use the
remoc
library, which allows me to have remoterust channels
.
Stay tuned and.. "A Presto!"
Luis
Posted on June 9, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.