Calloop 0.1.1 -- A callback-based event loop

(Note: This crate is the extraction of the event loop machinery of wayland-server into its own crate.)

Calloop is an event loop center around event sources associated with callbacks: a source generates events, which are then provided to the callback associated with this source.

What it is

It is built on mio to track readiness of sources and the run part of a program based on it is centered on the EventLoop::dispatch() method:

loop {
    event_loop.dispatch(Some(Duration::from_millis(20)).unwrap();
}

This method blocks during at most the supplied time until any event source becomes ready, and then dispatches all the generated events to their associated callbacks before returning.

From an EventLoop you can get a LoopHandle which can be cloned, and is used to insert new event sources into the event loop, it is thus notably possible to insert a new event source into the loop from within the callback of an other event source.

Currently the following kind of event sources are supported:

  • timers (generates events on timeouts)
  • MPSC channels (the event source is the receiver end)
  • Unix signals (generates an event when chosen signal is received)
  • Generic mio::Evented types (the generated events are just a forward of the mio readiness)

It is also possible to create new kinds of event sources by implementing the mio::Evented and calloop::EventSource traits.

calloop also provides "idle callbacks": callback that are fired only once, when all pending events of the sources have been processed. This makes it possible to schedule tasks for "later, when the loop is no longer busy".

Why this format

This kind of event loop handling, similar to the GLib event loop for example (but simpler), proved to be very adequate for wayland workflow, notably on smithay.

The main take-away is that it allows easy sharing of the event loop between different modules without needing these modules to know about each other. Having a single method fetching the next event would require creating a large enum of all possible events, which would completely breaks encapsulation.

A wayland compositor based on smithay actually has several modules that are mostly independent (session management, input processing, client protocol), but all require to wait on event sources. This callback-based approach allows all of them to share the same event loop without any need to have common code gluing them together, allowing much easier modularity.

I decided to extract this into a new crate because I felt this event-loop logic was actually quite independent of the wayland logic, and could probably be re-used by others. Typically, it might be used for client-side applications, that want to juggle between their connection to the display server and other event sources without necessarily spawning new threads.

Trade-offs

calloop makes large internal use of Rc, RefCell and trait objects, and you will too need to do it if you need to share data between some of your callbacks. A fundamental assumption is that the considered workload from event sources will be sufficiently low that this will not be a performance bottleneck.

(This is typically the case on a wayland server, were most of the work is spent in compositing the windows and drawing on the screen.)

2 Likes

Really like the idea of the idle callback.

Do you have any links to where you have shared data between tasks? (I would look into the code now, but am on mobile)

I don't have a lot of examples right now, as I just finished extracting the functionality from wayland-server into its own crate. But mostly it's about putting the shared data into a Rc<RefCell<...>> and cloning the Rc into the different closures.

Though, following a suggestion on reddit, I think I'll add the possibility to provide some &mut Data to the EventLoop::dispatch(..) method, that would be passed along to every callback. Allowing an easy sharing of one global state between the callbacks.