Calloop: A callback-based event loop

repository·master·Indexed 43 days ago

https://github.com/smithay/calloop

A lightweight callback-based event loop for Rust applications that spend most of their time waiting for events. It provides an abstraction over OS polling mechanisms like epoll and includes built-in support for timers, MPSC channels, Unix signals, and file descriptors. Calloop v0.14.4 supports integration with the futures ecosystem as an executor or for monitoring async IO, and allows for the creation of custom event sources via the EventSource trait.

Tokens
23.2K
Snippets
55
Records
91
Agent score
55%

What's inside Calloop

  1. How composed EventSources process events

    master

    When building a complex EventSource that is composed of multiple sub-sources (e.g., a channel, a ping source, and a socket), the process_events implementation should recursively call the process_events method of each sub-source.

    The Role of the Token

    The token passed into the top-level process_events is the mechanism used to determine which sub-source is responsible for the current wakeup. Each sub-source compares the provided token against its own registered token. If they match, the sub-source executes its logic and calls the provided callback.

    Workflow

    1. The loop wakes up and calls process_events on the composed source.
    2. The composed source calls sub_source.process_events(readiness, token, callback) for each component.
    3. If token == sub_source.token, the sub-source processes its internal events.
    4. The composed source then returns a PostAction (usually PostAction::Continue) to the loop.
  2. Understand event loop terminology: sources, events, and callbacks

    master

    To use Calloop effectively, you must understand its core mental model:

    • Sources: The underlying objects that generate activity, such as files, network sockets, or timers.
    • Events: The actual occurrences signaled by a source (e.g., a socket becoming ready for reading, a timer elapsing, or an error occurring).
    • Callbacks: Functions you provide to Calloop. When an event occurs, Calloop executes your callback. The callback may receive data related to the event, the state of the event source, and the overall program state.

    Calloop processes these events sequentially, one by one, in a loop.

  3. How to handle errors when implementing the EventSource trait

    master

    When implementing the EventSource trait, you must define an associated type Error. This error type is returned by the process_events() method and represents fatal errors that prevent the event source from processing any further events (e.g., a failure in the underlying file descriptor or executor).

    To ensure compatibility with Calloop's event loop, your Error type must be convertible into Box<dyn std::error::Error + Sync + Send>. This allows you to use:

    • Your own custom error types that implement std::error::Error.
    • Structured error types created with crates like thiserror.
    • Flexible error types like anyhow::Error.
    • Box<dyn std::error::Error + Sync + Send> directly.
  4. Build complex logic via composition

    master

    Calloop is designed around the principle of composition. Instead of creating one massive, complex event source, you should build complex logic by combining simpler, specialized event sources.

    For example, to implement a network socket with custom backoff/timeout logic, you would:

    1. Use a Generic file descriptor adapter to wrap the network socket.
    2. Use a timer to handle timeouts.
    3. Combine these sources with your own state and logic to manage the backoff behavior.

    This approach keeps individual components simple and testable while allowing for sophisticated program behavior.

  5. Monitor a file descriptor with a Generic event source

    master

    The Generic event source is a fundamental building block in calloop that wraps a file descriptor (fd) and triggers a callback whenever the descriptor becomes readable, writable, or encounters an error. This is the primary way to integrate Linux-based fd interfaces (such as GPIO, I2C, USB, UART, network interfaces, or timers) into the calloop event loop.

    You can provide any type that implements the AsFd trait to the Generic source, allowing you to use high-level wrapper types that manage their own allocation and disposal.

  6. Design a generic message type for event sources

    master

    If your event source needs to handle sequences of data (like multipart messages), you can use generics to allow flexibility in how users provide input.

    By constraining a generic type T to implement IntoIterator where the items can be converted into your target message type, you support various input formats (like Vec<T> or Option<T>).

    To enforce that a user provides exactly one message part, they can use std::iter::Once<T> or a single-element array [T; 1] as the generic type parameter.

    pub struct ZeroMQSource<T>
    where
        T: IntoIterator,
        T::Item: Into<zmq::Message>,
    {
        mpsc_receiver: calloop::channel::Channel<T>,
        // ...
    }
  7. How to schedule timer callbacks and handle rescheduling

    master

    When adding a Timer to a calloop event loop, you provide a callback function. This callback receives three arguments:

    1. Instant: The exact time the timer was scheduled to expire. This allows for precise time tracking even if the event loop was busy and the callback is invoked slightly late.
    2. &mut (): A placeholder argument (timers do not use the EventSource functionality).
    3. SharedData: The shared data passed to your event loop.

    Your callback must return a TimeoutAction to tell calloop how to handle the timer after the event fires:

    • TimeoutAction::Drop: Disables and destroys the timer.
    • TimeoutAction::ToInstant(Instant): Reschedules the timer to fire at a specific Instant. This is ideal for maintaining a consistent frequency (e.g., key repetition) because you can calculate the next deadline relative to the previous one, preventing drift even if the loop lags.
    • TimeoutAction::ToDuration(Duration): Reschedules the timer to fire after a specific Duration from the current moment. This is useful for background tasks where catching up on missed intervals is not required.
    // Example of a callback returning a TimeoutAction
    use calloop::timer::TimeoutAction;
    use std::time::{Duration, Instant};
    
    let callback = |instant: Instant, _: &mut (), data: &mut MyData| {
        // Perform task...
        
        // Reschedule to fire again in 1 second
        TimeoutAction::ToDuration(Duration::from_secs(1))
    };
  8. Pattern for custom event source constructors

    master

    A common pattern for creating custom event sources in Calloop is to return a tuple containing the source itself and a handle used to interact with it (such as a channel sender). This allows the caller to own the source for registration while maintaining a way to trigger events or send data to the source.

    Example pattern:

    pub fn from_socket(socket: zmq::Socket) -> io::Result<(Self, calloop::channel::Sender<T>)> {
        // ... initialization logic ...
        Ok((Self { ... }, mpsc_sender))
    }
    pub fn from_socket(socket: zmq::Socket) -> io::Result<(Self, calloop::channel::Sender<T>)> {
        let (mpsc_sender, mpsc_receiver) = calloop::channel::channel();
        let (wake_ping_sender, wake_ping_receiver) = calloop::ping::make_ping()?;
    
        let fd = socket.get_fd()?;
    
        let socket_source =
            calloop::generic::Generic::from_fd(fd, calloop::Interest::READ, calloop::Mode::Edge);
    
        Ok((
            Self {
                socket,
                socket_source,
                mpsc_receiver,
                wake_ping_receiver,
                wake_ping_sender,
            },
            mpsc_sender,
        ))
    }
  9. How to wake up the event loop using the Ping source

    master

    The Ping event source is used to manually wake up the calloop event loop. This is necessary when you have events that cannot wake the loop themselves (for example, when using a standard Rust MPSC channel for a Channel source).

    To use it, you must manage two parts:

    1. PingSource: The event source component that is registered with the event loop.
    2. Ping: The sending end used to trigger the wake-up.

    To wake the loop, call the ping() method on the Ping handle.

    CRITICAL: You must ensure that the PingSource events are processed. Even though the events carry no data (they are ()), you must call process_events() on the PingSource to "reset" it. If you fail to process the events, the event loop will enter a busy-loop, continually waking up without doing useful work.

    // Conceptual usage pattern:
    // 1. Register PingSource in the event loop
    // 2. Keep the Ping handle to trigger wake-ups
    // 3. When an external event occurs (e.g. MPSC message):
    //    ping.ping();
    // 4. Ensure the loop calls process_events() on the PingSource to avoid busy-looping
  10. Implement a custom EventSource

    master

    To create a custom event source, implement the EventSource trait. This allows you to:

    • Register one or more file descriptors to be monitored by the loop.
    • Wrap existing event sources to process or transform their events.
    • Aggregate multiple sources into a single logical source.
  11. How event sources work in calloop

    master

    Calloop uses a push-based model centered around the EventSource trait. An EventSource represents an entity capable of generating events.

    To use an event source, you must:

    1. Provide ownership of the event source to calloop.
    2. Provide a closure that defines the logic to be executed whenever that specific source generates an event.

    This model is designed for reacting to unpredictable external events rather than waiting for the completion of operations initiated by your program.

  12. Distinguishing between Event errors and EventSource errors

    master

    When implementing an EventSource, you should categorize errors into two distinct types to ensure the event loop behaves correctly:

    1. Event-level errors: These are errors that occur during normal operation but do not stop the source from functioning. These should be part of your Event associated type (e.g., as a variant in an enum or a Result).

      • Example: A channel being closed is an event (Closed) because you may still need to process messages already in the queue.
      • Example: A subprocess exiting with a non-zero status code is an event.
    2. Source-level errors: These are errors indicating the event source is no longer capable of processing events. These should be returned via the Error associated type in process_events().

      • Example: An internal failure in the underlying mechanism (like a failed file descriptor) that makes further polling impossible.