2
\$\begingroup\$

I'm creating a relatively simple type-safe and thread-safe Rust event system. It is to be used with and within an IRC library I'm making, but should work just fine for other use-cases. It needs to be reasonably good and safe. Using it might require lazy-static, which makes me slightly worried, but even then it should be safe.

This is the event.rs code:

use std::collections::HashMap;
use std::marker::PhantomData;
extern crate uuid;
use self::uuid::Uuid;

// Note: This doesn't support Copy or Clone for safety reasons.
// More specifically, it should be impossible to unregister the same handler more than once.
pub struct EventHandlerId<T: Event + ?Sized> {
  id: Uuid,
  _t: PhantomData<T>,
}
impl<T: Event + ?Sized> Eq for EventHandlerId<T> {}
impl<T: Event + ?Sized> PartialEq for EventHandlerId<T> {
  fn eq(&self, other: &Self) -> bool {
    self.id == other.id && self._t == other._t
  }
}

struct EventHandler<T: Event + ?Sized> {
  priority: i32,
  f: fn(&mut T),
  id: EventHandlerId<T>,
}

pub struct EventMetadata<T: Event + ?Sized> {
  handlers: HashMap<&'static EventBus, Vec<EventHandler<T>>>,
}

impl<T: Event + ?Sized> EventMetadata<T> {
  pub fn new() -> EventMetadata<T> {
    EventMetadata { handlers: HashMap::new() }
  }

  fn put(&mut self, bus: &'static EventBus, f: fn(&mut T), priority: i32) -> EventHandlerId<T> {
    let vec = self.handlers.entry(bus).or_insert_with(Vec::new);
    let pos = vec.binary_search_by(|a| a.priority.cmp(&priority)).unwrap_or_else(|e| e);
    let id = Uuid::new_v4();
    vec.insert(pos, EventHandler { f: f, priority: priority, id: EventHandlerId { id: id, _t: PhantomData } });
    EventHandlerId { id: id, _t: PhantomData }
  }

  fn remove(&mut self, bus: &EventBus, f: EventHandlerId<T>) {
    let flag = self.handlers.get_mut(bus).iter_mut().any(|v| { v.retain(|x| x.id != f); v.is_empty() });
    if flag { self.handlers.remove(bus); }
  }

  #[inline]
  fn post(&self, bus: &EventBus, event: &mut T) -> bool {
    self.handlers.get(bus).iter().flat_map(|x| x.iter()).any(|h| {
      (h.f)(event);
      event.cancelled()
    })
  }
}

pub trait Event {
  // type properties
  fn event_metadata<F, R>(F) -> R where F: FnOnce(&EventMetadata<Self>) -> R;

  fn mut_metadata<F, R>(F) -> R where F: FnOnce(&mut EventMetadata<Self>) -> R;

  fn cancellable() -> bool { false }

  // instance properties
  fn cancelled(&self) -> bool { false }

  fn cancel(&self, bool) { panic!() }
}

#[derive(PartialEq, Eq, Hash)]
pub struct EventBus {
  uuid: Uuid
}

impl EventBus {
 pub fn new() -> EventBus {
   EventBus { uuid: Uuid::new_v4() }
 }

 pub fn register<T>(&'static self, f: fn(&mut T), priority: i32) -> EventHandlerId<T> where T: Event {
   T::mut_metadata(|x| x.put(self, f, priority))
 }

 pub fn unregister<T>(&self, f: EventHandlerId<T>) where T: Event {
   T::mut_metadata(|x| x.remove(self, f))
 }

 pub fn post<T>(&self, event: &mut T) -> bool where T: Event {
   T::event_metadata(|x| x.post(self, event))
 }
}

It is to be used like this: (main.rs)

mod event;
use event::{EventBus, EventMetadata, Event};
use std::sync::RwLock;

#[macro_use]
extern crate lazy_static;

struct NoEvent {
  i: i32
}

lazy_static! {
  static ref NOEVENT_METADATA: RwLock<EventMetadata<NoEvent>> = RwLock::new(EventMetadata::new());
  static ref EVENT_BUS: EventBus = EventBus::new();
}

impl Event for NoEvent {
  fn event_metadata<F, R>(f: F) -> R where F: FnOnce(&EventMetadata<Self>) -> R {
    f(&*NOEVENT_METADATA.read().unwrap())
  }

  fn mut_metadata<F, R>(f: F) -> R where F: FnOnce(&mut EventMetadata<Self>) -> R {
    f(&mut *NOEVENT_METADATA.write().unwrap())
  }
}

fn test(e: &mut NoEvent) {
  println!("{}", e.i);
  e.i += 1;
}

fn test2(e: &mut NoEvent) {
  println!("{}", e.i);
}

fn main() {
  let test_id = EVENT_BUS.register(test, 0);
  let mut event = NoEvent { i: 3 };
  EVENT_BUS.post(&mut event);
  EVENT_BUS.register(test2, 1);
  EVENT_BUS.post(&mut event);
  EVENT_BUS.unregister(test_id);
  EVENT_BUS.post(&mut event);
}
\$\endgroup\$
3
\$\begingroup\$
  1. Rust standard indentation is 4 spaces. The code currently has 1 and 2 space indents.

  2. where clauses should be placed on the next line, one line per restriction.

  3. extern crate usually precedes use statements.

  4. There's no need for use self::uuid::..., you can just use use uuid::....

  5. #[inline] is implicit when there are type parameters — the same mechanism for inlining code is how it is monomorphized.

  6. Why include the PhantomData in the equality check? That implementation always returns true.

  7. What's the benefit of a UUID? For example, would there be a downside in using a monotonically incrementing atomic variable?

  8. When I think of an event bus, I assume that I'm going to give an entire value to the bus, not a reference to one. Why does the code make this decision?

  9. Overall, there's a lot of complexity that isn't immediately driven out from the examples. Can you explain more about how the current design came to be?

  10. You may want to look into using a BTreeMap for holding the handlers. The key can be the priority and the values can be a vector of handlers. The docs don't guarantee this, but experimentally the iterator is in sorted order and inserting should be efficient as well.

\$\endgroup\$
  • \$\begingroup\$ 1) I use 2 space indents. They're just more readable. 3) It does, did you mean "all use statements", perhaps? 5) Docs? 6) For completeness. 7) UUIDs are sane and don't conflict very often. Adding atomic variables would be a pain. 8) Better than returning the event after we're done with it. The event object is mutable so you can add data to it. In a game, these data could be a mutable world, a target position, etc. 9) It's inspired by the MinecraftForge event system. By putting a bus->hook map on the event type, I get easy type-safety, but buses must be static. 10) Better be safe than sorry. \$\endgroup\$ – SoniEx2 Apr 13 '16 at 23:34
  • \$\begingroup\$ "The docs don't guarantee this, but experimentally the iterator is in sorted order" → Am I reading different docs? \$\endgroup\$ – Veedrac Apr 20 '16 at 6:04

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.