Before you read, concepts like: concurrency, traits, Arc/Mutex and Rust Channles (crossbeam lib) should be clear for you.
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::{Receiver, Sender};
use strum_macros::{Display, EnumString}; #[derive(Clone, Debug, PartialEq, Eq, Hash, Display, EnumString)]
pub enum EventName { Dummy, TestEvent, } pub type EventPayload = Vec<u8>; pub trait EventHandlerTrait: Send + Sync { fn handle_event(&self, event_name: EventName, payload: EventPayload);
} #[derive(Clone)]
pub struct EventHandler { pub event_name: EventName, pub handler: Arc<dyn EventHandlerTrait>,
} pub struct EventLoop { sender: Sender<(EventName, EventPayload)>, receiver: Receiver<(EventName, EventPayload)>, handlers: Arc<Mutex<HashMap<EventName, Vec<Arc<dyn EventHandlerTrait>>>>>,
} impl EventLoop { pub fn new() -> Self { let (sender, receiver) = crossbeam::channel::unbounded(); EventLoop { sender, receiver, handlers: Arc::new(Mutex::new(HashMap::new())), } } pub fn add_handler(&mut self, event_name: EventName, handler: Arc<dyn EventHandlerTrait>) { let mut handlers = self.handlers.lock().unwrap(); handlers .entry(event_name) .or_insert_with(Vec::new) .push(handler); } pub fn push_event(&mut self, event_name: EventName, event_data: EventPayload) { self.sender.send((event_name, event_data)).unwrap(); } pub fn run(&self) { let handlers = Arc::clone(&self.handlers); let receiver = self.receiver.clone(); thread::spawn(move || loop { if let Ok((event_name, event_data)) = receiver.recv() { let handlers = handlers.lock().unwrap(); if let Some(handlers_for_event) = handlers.get(&event_name) { for handler in handlers_for_event { handler.handle_event(event_name.clone(), event_data.clone()); } } } }); }
}
Explanation
EventName Enum
#[derive(Clone, Debug, PartialEq, Eq, Hash, Display, EnumString)]
pub enum EventName { Dummy, TestEvent,
}
EventName
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 EventPayload = Vec<u8>;
EventPayload
is defined as a type alias for Vec<u8>
, representing the data associated with an event.
EventHandlerTrait
pub trait EventHandlerTrait: Send + Sync { fn handle_event(&self, event_name: EventName, payload: EventPayload);
}
EventHandlerTrait
is a trait that any event handler must implement. It requires a single method, handle_event
, which takes an EventName
and EventPayload
. We will elaborate more on the Send
& Sync
traits later.
EventHandler Struct
#[derive(Clone)]
pub struct EventHandler { pub event_name: EventName, pub handler: Arc<dyn EventHandlerTrait>,
}
The EventHandler
struct binds an event_name
to a handler
implementing the EventHandlerTrait
, allowing the addition of multiple handlers for each event.
EventLoop Struct
pub struct EventLoop { sender: Sender<(EventName, EventPayload)>, receiver: Receiver<(EventName, EventPayload)>, handlers: Arc<Mutex<HashMap<EventName, Vec<Arc<dyn EventHandlerTrait>>>>>,
}
EventLoop
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<EventName, Vec<Arc<dyn EventHandlerTrait>>>>>
. 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 EventHandlerTrait for TestEventHandler { fn handle_event(&self, event_name: EventName, payload: EventPayload) { let data = String::from_utf8(payload).unwrap(); let message = format!("{} => {}", event_name, data); info!("TestEvent: {}", message); }
}
pub struct DBTestEventHandler; impl EventHandlerTrait for DBTestEventHandler { fn handle_event(&self, event_name: EventName, payload: EventPayload) { let data = String::from_utf8(payload).unwrap(); let message = format!("{} => {}", event_name, data); // Persist data into db info!("Data saved on DB!"); }
}
Main function example
fn main() { env_logger::builder() .filter_level(log::LevelFilter::Info) .init(); info!("Starting event loop example"); info!("------------------------------"); info!("Events: {:?}", EventName::Dummy); info!("Events: {:?}", EventName::TestEvent); info!("------------------------------"); let mut event_loop = EventLoop::new(); event_loop.add_handler(EventName::TestEvent, Arc::new(TestEventHandler)); event_loop.add_handler(EventName::TestEvent, Arc::new(DBTestEventHandler)); // Start the event loop event_loop.run(); loop { info!("Give me some input, type 'exit' to quit"); let mut input = String::new(); io::stdin() .read_line(&mut input) .expect("Errore during input reading..."); 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_name = EventName::from_str(&name_data.0).unwrap_or_else(|_| EventName::Dummy); event_loop.push_event(event_name, 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
Discover more from Coursity
Subscribe to get the latest posts sent to your email.