# How LibCore Handles File Descriptor Events and I/O Multiplexing in Ladybird

> Learn how LibCore uses poll poll system call and Core Notifier to manage file descriptor events and I/O multiplexing efficiently in Ladybird with ThreadData and wake pipes.

- Repository: [Ladybird/ladybird](https://github.com/LadybirdBrowser/ladybird)
- Tags: internals
- Published: 2026-03-05

---

**LibCore implements I/O multiplexing using the POSIX `poll()` system call, wrapping file descriptors in `Core::Notifier` objects and managing per-thread state through `ThreadData` structures that include a wake pipe for interrupting blocking operations.**

The Ladybird browser engine relies on LibCore for cross-platform asynchronous I/O operations. In the Unix implementation found in [`Libraries/LibCore/EventLoopImplementationUnix.cpp`](https://github.com/LadybirdBrowser/ladybird/blob/main/Libraries/LibCore/EventLoopImplementationUnix.cpp), the library abstracts raw file descriptor readiness into high-level events using a centralized polling mechanism that scales across multiple threads without global lock contention.

## Core Architecture: ThreadData and Notifier Abstractions

At the heart of LibCore's event system lies the **`ThreadData`** structure, defined in [`Libraries/LibCore/EventLoopImplementationUnix.cpp`](https://github.com/LadybirdBrowser/ladybird/blob/main/Libraries/LibCore/EventLoopImplementationUnix.cpp) (lines 23-88). This per-thread singleton maintains the complete state for I/O multiplexing, including a `Vector<pollfd>` for the underlying `poll()` call, a `HashMap<Notifier*, size_t>` mapping notifiers to indices, a `TimeoutSet` binary heap for timer management, and a dedicated wake pipe.

The **`Core::Notifier`** class acts as a thin wrapper around a file descriptor, specifying which events—read, write, hang-up, or error—the application wishes to monitor. Each `Notifier` registers itself with the thread-local `EventLoopManagerUnix` upon creation, translating high-level `NotificationType` flags into the `POLLIN` and `POLLOUT` bits required by the POSIX `poll()` system call.

## Registering File Descriptors with the Event Loop

When you instantiate a `Notifier` and enable it, the library executes a precise registration sequence. First, the `Notifier::set_enabled` method invokes `Core::EventLoop::register_notifier`, which delegates to `EventLoopManagerUnix::register_notifier` in [`EventLoopImplementationUnix.cpp`](https://github.com/LadybirdBrowser/ladybird/blob/main/EventLoopImplementationUnix.cpp).

The manager appends the file descriptor to the thread's `poll_fds` vector and updates the `notifier_to_index` HashMap for O(1) lookup. The helper function **`notification_type_to_poll_events`** (lines 37-45) performs the bitwise translation between LibCore's `NotificationType` enum and the standard `pollfd` event flags.

```cpp
// From EventLoopImplementationUnix.cpp
thread_data.notifier_to_index.set(&notifier, thread_data.poll_fds.size());
thread_data.notifiers.append(&notifier);
thread_data.poll_fds.append({
    .fd = notifier.fd(),
    .events = notification_type_to_poll_events(notifier.type()),
    .revents = 0
});

```

## The Wake Pipe Mechanism for Interrupting poll()

Each thread maintains a private two-ended pipe called the **wake pipe**. The read end occupies index 0 of the `poll_fds` array, ensuring it is always the first descriptor checked when `poll()` returns. The write end is used exclusively by the **`EventLoopImplementationUnix::wake()`** method (lines 21-30) to inject a zero-valued integer, forcing the blocking `poll()` call to return immediately.

```cpp
void EventLoopImplementationUnix::wake()
{
    int wake_event = 0;
    Core::System::write(m_wake_pipe_write_fd, { &wake_event, sizeof(wake_event) });
}

```

This design allows other threads or signal handlers to wake the event loop for processing queued events or POSIX signals without waiting for the full timeout duration.

## The Main Poll Loop and Event Dispatch

The **`EventLoopManagerUnix::wait_for_events`** method drives the event loop lifecycle. Before calling `poll()`, the manager queries `thread_data.timeouts.next_timer_expiration()` to compute the maximum blocking duration. If timers are pending, it calculates the remaining milliseconds; otherwise, it passes `-1` to wait indefinitely.

The `System::poll()` call (lines 60-68) is wrapped in a retry loop to handle `EINTR` interruptions gracefully. Upon return, the loop processes results in strict priority order:

1. **Wake Pipe (Index 0):** If `poll_fds[0].revents & POLLIN` is true, the code drains the pipe. A non-zero value indicates a POSIX signal requires handling, while zero indicates a standard wake request.
2. **Notifier Events (Indices ≥1):** For each subsequent entry, the loop reconstructs a `NotificationType` from the `revents` field (`POLLIN`, `POLLOUT`, `POLLHUP`, `POLLERR`). If the detected events intersect with the notifier's registered interests, the system posts a `NotifierActivation` event to the `ThreadEventQueue` (lines 104-128).
3. **Timer Firing:** Finally, the loop invokes `thread_data.timeouts.fire_expired(time_after_poll)` (lines 130-133) to dispatch `Timer` events to their associated `EventReceiver` objects.

## Unregistering Notifiers and Resource Cleanup

When a `Notifier` is destroyed or explicitly disabled, **`EventLoopManagerUnix::unregister_notifier`** (lines 160-182) performs an efficient removal. The method locates the notifier's index in the `poll_fds` vector, removes the entry by swapping the last element into the vacant slot, and updates the `notifier_to_index` map to reflect the new indices. This swap-and-pop strategy maintains a compact array, ensuring the `poll()` call iterates only over active descriptors.

## Practical Code Examples

### Monitoring a Socket for Readability

```cpp
// Assume `socket_fd` is a non-blocking TCP socket.
Core::Notifier socket_notifier(socket_fd, Core::Notifier::Type::Read);
socket_notifier.on_activation = [&] {
    // This runs inside the event loop when data is available.
    char buffer[1024];
    ssize_t n = read(socket_notifier.fd(), buffer, sizeof(buffer));
    if (n > 0) {
        // Process incoming data …
    }
};

```

The notifier registers itself automatically via the constructor; the event loop invokes the lambda whenever `poll()` reports `POLLIN` on `socket_fd`.

### Waking the Loop from Another Thread

```cpp
// In thread A:
Core::EventLoop::current().wake();

```

Calling `wake()` writes to the wake pipe, causing the `poll()` in thread A's loop to return immediately and process pending events.

### Setting a One-Shot Timer

```cpp
auto timer_id = Core::EventLoop::register_timer(
    Core::EventReceiver::current(),
    500,               // 500 ms
    false              // do not reload
);

```

When the timer expires, the associated `EventReceiver` receives a `Timer` event via the same `ThreadEventQueue` used for notifier activations.

## Summary

- LibCore uses POSIX **`poll()`** for I/O multiplexing, implemented in [`Libraries/LibCore/EventLoopImplementationUnix.cpp`](https://github.com/LadybirdBrowser/ladybird/blob/main/Libraries/LibCore/EventLoopImplementationUnix.cpp).
- Each thread maintains isolated state via **`ThreadData`**, eliminating global lock contention through per-thread event loops.
- **`Core::Notifier`** objects wrap file descriptors and translate `POLLIN`/`POLLOUT` events into activation callbacks posted to the `ThreadEventQueue`.
- A dedicated **wake pipe** at index 0 of the poll array allows immediate interruption of blocking `poll()` calls via the `wake()` method.
- The **`TimeoutSet`** binary heap efficiently schedules timer events alongside file descriptor monitoring, calculating optimal timeout values before each poll cycle.

## Frequently Asked Questions

### How does LibCore handle file descriptor events and I/O multiplexing?

LibCore implements I/O multiplexing using the POSIX `poll()` system call, managing per-thread file descriptor state through the `ThreadData` structure. The system wraps descriptors in `Core::Notifier` objects that map `POLLIN` and `POLLOUT` events to user-defined activation callbacks, processed in the `wait_for_events` method of `EventLoopManagerUnix`.

### What is the purpose of the wake pipe in LibCore's event loop?

The wake pipe is a per-thread pipe where the read end occupies index 0 of the `poll_fds` array, allowing the `EventLoopImplementationUnix::wake()` method to interrupt blocking `poll()` calls by writing a zero-valued integer to the write end. This mechanism enables immediate processing of queued events or POSIX signals without waiting for the full timeout period.

### How does LibCore manage timers alongside file descriptor polling?

LibCore uses a `TimeoutSet` class implementing a binary heap priority queue to track timer expirations, calculating the next timeout before each `poll()` call via `next_timer_expiration()`. When `poll()` returns, the system fires expired timers by posting `Timer` events to their associated `EventReceiver` objects before processing file descriptor events.

### How are Notifier objects unregistered from the event loop?

When a `Notifier` is disabled or destroyed, `EventLoopManagerUnix::unregister_notifier` removes the file descriptor from the `poll_fds` vector and updates the `notifier_to_index` map, swapping the last element into the removed position to maintain a compact array for efficient polling.