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);
}
1 Answer 1
Rust standard indentation is 4 spaces. The code currently has 1 and 2 space indents.
where
clauses should be placed on the next line, one line per restriction.extern crate
usually precedesuse
statements.There's no need for
use self::uuid::...
, you can just useuse uuid::...
.#[inline]
is implicit when there are type parameters — the same mechanism for inlining code is how it is monomorphized.Why include the
PhantomData
in the equality check? That implementation always returnstrue
.What's the benefit of a UUID? For example, would there be a downside in using a monotonically incrementing atomic variable?
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?
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?
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.
-
\$\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– SoniEx22016年04月13日 23:34:56 +00:00Commented Apr 13, 2016 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– Veedrac2016年04月20日 06:04:18 +00:00Commented Apr 20, 2016 at 6:04
Explore related questions
See similar questions with these tags.