Smithay project

The rusty smithy for wayland compositors

Version 0.7 of calloop


We just released version 0.7 of calloop, the callback-based event loop that backs Smithay and optionaly SCTK. As the crate is now getting mature, it is time for a formal presentation!

Callbacks, in Rust?

It is commonly acknowledged that in Rust, callback-based designs induce a lot of friction, in particular regarding state sharing. Nobody wants to have Rc<RefCell<_>> littered all around their codebase, and so naturally most of the ecosystem turned to async/await, a design much more natural given the constraints of the language.

However, not all usecases play well with async/await, notably cases were you are in a purely reactive context: waiting for events and quickly reacting to them. Indeed, why would you use async/await if you never need to .await anything? This is for a large part our context of work in Smithay, and so we developed calloop.

Calloop is designed trying to ease the friction of using callbacks in Rust as much as possible. The central issue, state sharing, is managed though some global shared data. A mutable reference to some value of your choosing is passed down to all callbacks. As the whole event loop is single-threaded, this allows them access to your state without any synchronization or reference-counted pointers, as if they were all methods of the same object.

In a nutshell, calloop is mostly targeted at apps that spend a large portion of their time just waiting for something to happen, and need to quickly react to it as opposed to apps that need to manage numerous computations or sockets in parallel efficiently, which is the more common use of async/await. This processing can be more practical in GUI apps notably, in particular in X11 or Wayland contexts, where the communication with the display server is made over an Unix socket, which can be monitored by APIs like epoll or kqueue.

How is it used?

Calloop's core is the EventLoop type. As you can tell by its name, it is the event loop. Once you've registered your event sources and their callbacks, you just call EventLoop::run(...) and that method will block, running your event loop and dispatching the events appropriately. Setting up these callbacks is done though two main components: the LoopHandle object, and the EventSource trait.

The LoopHandle is a handle to the EventLoop (duh). This handle can be cloned and shared in different parts of your program. This handle is what is used to setup new callbacks, which can thus be done from mostly anywhere, even within a callback! The notable constraint to this is that the handle is not shareable between threads. Calloop is meant to run the event loop on a single thread.

The EventSource trait is a generic interface for "something that can generate events". Anything implementing that trait can be associated to a callback in the EventLoop. It will be monitored and the associated callback will be invoked every time the event source generates an event. This trait is meant to be composable: one can implement a high-level event source by wrapping a low-level one and further processing its event before passing them along.

To give you a feel of what it looks like, take a look at this example extracted from the documentation:

use calloop::{generic::Generic, EventLoop, Interest, Mode};

use std::time::Duration;

fn main() {
    // Create the event loop
    let mut event_loop = EventLoop::try_new()
                .expect("Failed to initialize the event loop!");
    // Retrieve a handle. It is used to insert new sources into the event loop
    // It can be cloned, allowing you to insert sources from within callbacks
    let handle = event_loop.handle();

    // Inserting an event source takes this general form
    // it can also be done from within the callback of an other event source
    handle.insert_source(
        // a type implementing the EventSource trait
        source,
        // a callback that is invoked whenever this source generates an event
        |event, metadata, shared_data| {
            // This callback is given 3 values:
            // - the event generated by the source
            // - &mut access to some metadata, specific to the event source
            // - &mut access to the global shared data that was passed to EventLoop::dispatch
        }
    );

    // Actual run of your loop
    //
    // Dispatch received events to their callbacks, waiting at most 20 ms for
    // new events between each batch.
    //
    // The `&mut shared_data` is a mutable reference that will be forwarded to all
    // your callbacks, allowing them to easily share some state
    event_loop.run(Duration::from_millis(20), &mut shared_data, |shared_data| {
        /*
        * Insert here the processing you need to do between each waiting batch (if any)
        * like invoking your drawing logic if you're writing a GUI app for example.
        */
    });
}

Calloop provides a few implementation of the EventSource trait such as a timer, a channel, and a generic adapter for monitoring a file descriptor for read/write readiness. At the moment calloop only supports Linux and the *BSD systems, but adding support for other platforms would be a welcome contribution!

Can it work alongside async/await?

Even if the core of your app works best will callbacks, some part of your needs might benefit from async/await. Since 0.7, calloop allows you to mix both paradigms. This is supported by two separate parts newly added to calloop: an async IO adapter, and an executor.

The async IO adapter provides an Async type similar to the one provided by the async-io crate, but instead of spawning a background thread, it monitors the readiness of IO objects directy in the associated EventLoop.

The executor is, unsuprisingly, yet another implementation of the EventSource trait, which can thus be inserted into an EventLoop just like any other event source. It is paired with a cloneable Scheduler, which can be used to spawn futures into the executor from anywhere.