Learning by Doing: Event Loop in Rust
Tags: rust, programming, tutorial, learning
Published: 2024-06-09 09:53:56


Before you read, concepts like:  concurrency, traits, Arc/Mutex and Rust Channles (crossbeam lib) should be clear for you.

Image by [Huso112](https://www.deviantart.com/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:

```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

```rust
#[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
```rust
pub type Payload = Vec<u8>;
```
`Payload` is defined as a type alias for `Vec<u8>`, representing the data associated with an event.

#### Handler
```rust
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
```rust
#[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
```rust
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>>>>>`. 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

``` rust
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);

    }
}
```

``` rust
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

``` rust
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: 

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