DEV Community

Cover image for Learning by Doing: Event Loop in Rust
LuisC
LuisC

Posted on • Updated on

Learning by Doing: Event Loop in Rust

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());
                    }
                }
            }
        });
    }
}
Enter fullscreen mode Exit fullscreen mode

Explanation

Event Enum

#[derive(Clone, Debug, PartialEq, Eq, Hash, Display, EnumString)]
pub enum Event {
    Dummy,
    TestEvent,
}
Enter fullscreen mode Exit fullscreen mode

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>;
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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>,
}
Enter fullscreen mode Exit fullscreen mode

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>>>>>,
}
Enter fullscreen mode Exit fullscreen mode

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>>>>>. The Arc (atomic reference count) allows multiple threads to share ownership of the handlers without needing to clone them. By requiring Sync, 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);

    }
}
Enter fullscreen mode Exit fullscreen mode
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!");

    }
}
Enter fullscreen mode Exit fullscreen mode

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());
    }
}

Enter fullscreen mode Exit fullscreen mode

What's next

I have several ideas running through my head, but the ones I would like to implement are:

  1. A shared, observable state between handlers. It would be very interesting to have handlers that are activated based on a certain state.

  2. 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 remote rust channels.

Stay tuned and.. "A Presto!"

Luis

Top comments (0)