Expand description

Integrating x11rb with an Event Loop

To integrate x11rb with an event loop, std::os::unix::io::AsRawFd is implemented by RustConnection’s DefaultStream and XCBConnection. This allows to integrate with an event loop that also handles timeouts or network I/O. See xclock_utc for an example.

The general form of such an integration could be as follows:

#[cfg(unix)]
use std::os::unix::io::{AsRawFd, RawFd};
#[cfg(windows)]
use std::os::windows::io::{AsRawSocket, RawSocket};
use x11rb::connection::Connection;
use x11rb::rust_connection::RustConnection;
use x11rb::errors::ConnectionError;

fn main_loop(conn: &RustConnection) -> Result<(), ConnectionError> {
    #[cfg(unix)]
    let raw_handle = conn.stream().as_raw_fd();
    #[cfg(windows)]
    let raw_handle = conn.stream().as_raw_socket();
    loop {
        while let Some(event) = conn.poll_for_event()? {
            handle_event(event);
        }

        poll_for_readable(raw_handle);

        // Do other work here.
    }
}

The function poll_for_readable could wait for any number of I/O streams (besides the one from x11rb) to become readable. It can also implement timeouts, as seen in the xclock_utc example.

Threads and Races

Both RustConnection and XCBConnection are Sync+Send. However, it is still possible to see races in the presence of threads and an event loop.

The underlying problem is that the following two points are not equivalent:

  1. A new event is available and can be returned from conn.poll_for_event().
  2. The underlying I/O stream is readable.

The reason for this is an internal buffer that is required: When an event is received from the X11 server, but we are currently not in conn.poll_for_event(), then this event is added to an internal buffer. Thus, it can happen that there is an event available, but the stream is not readable.

An example for such an other function is conn.get_input_focus()?.reply()?: The GetInputFocus request is sent to the server and then reply() waits for the reply. It does so by reading X11 packets from the X11 server until the right reply arrives. Any events that are read during this are buffered internally in the Connection.

If this race occurs, the main loop would sit in poll_for_readable and wait, while the already buffered event is available. When something else wakes up the main loop and conn.poll_for_event() is called the next time, the event is finally processed.

There are two ways around this:

  1. Only interact with x11rb from one thread.
  2. Use a dedicated thread for waiting for event.

In case (1), one can call conn.poll_for_event() before waiting for the underlying I/O stream to be readable. Since there are no other threads, nothing can read a new event from the stream after conn.poll_for_event() returned None.

Option (2) is to start a thread that calls conn.wait_for_event() in a loop. This is basically a dedicated event loop for fetching events from the X11 server. All other threads can now freely use the X11 connection without events possibly getting stuck and only being processed later.