EventLoop system for handling application tasks concurrently on a single thread. This event-driven architecture is fundamental to how Ladybird processes user input, network events, timers, and other asynchronous operations.
What is an event loop?
The event loop is a design pattern found in many systems. It can be briefly summarized as:An in-process loop, running continuously, which processes incoming events from signals, notifiers, file watchers, timers, etc. by running associated callbacks.The event loop relies on callbacks eventually returning control to it, enabling other events to be processed. In this way, it operates like a cooperative multitasking scheduler in userspace.
LibCore’s event loop system is primarily used with graphical applications. LibGUI and IPC are deeply integrated into it. Command-line applications may not need to interact with the event loop.
How it works
The execution cycle
When an event loop runs, it sits inexec() repeatedly pump()ing the loop:
exec()waits for an event to happen- Runs all callbacks associated with that event
- Goes back to sleep and waits for more events
Low-level mechanics
Here’s what happens under the hood:Enter event loop
On
exec(), the event loop enters the event loop stack. Then, the loop is pumped repeatedly.Wait for events
Each
pump() first puts the event loop to sleep with wait_for_event(), then handles all events.System-level waiting
wait_for_event() uses select(2) to wait until one of the prepared file descriptors becomes readable or writable, or until a timeout expires.Handle events
After
select(2) returns, signals are dispatched to handlers. Other new events (expired timers, file notifications) are added to the event queue.The select(2) system call
The event loop usesselect(2) to efficiently wait for events:
- File notifiers: Registered by the application
- Wake pipe: Used for signal handling and cross-thread wakeups
- Infinite: When there are no time-based wake conditions
- Minimum timer: When timers are registered, timeout is the shortest
- Zero: When events are already available (immediate return)
The wake pipe mechanism
The wake pipe serves two critical purposes:- Signal handling: When a POSIX signal is received that the event loop has a handler for,
handle_signal()writes the handler number to the pipe - Cross-thread waking: The
wake()function writes 0 (not a valid signal number) to wake the event loop from other threads
Thread-local state
All event loop state is thread-local:- Notifiers
- Timers
- Event loop stack
- Event queues
EventLoop::current() returns the topmost event loop on the calling thread.
Event loop stack
The event loop stack enables nested event loops, primarily used for modal GUI windows.How the stack works
- Each window can add another event loop onto the stack
- When an event loop exits, remaining events are added to the lower event loop’s queue
- This allows
GUI::Windowand similar systems to create event loops without interfering with existing ones
Event types
The event loop handles several kinds of events:POSIX signals
Register signal handlers withEventLoop::register_signal():
The event loop registers the specified POSIX signal with the kernel, and the handler runs as a normal event without the complexity of traditional signal handlers.
Posted events
EventLoop::post_event() fires an event targeting a specific Core::EventReceiver:
Deferred callbacks
EventLoop::deferred_invoke() calls an arbitrary callback on the next iteration:
Timer events
Create timers that fire after a timeout, possibly repeatedly:EventLoop::register_timer: Low-level timer registrationObject::start_timer(): Object-based timer APICore::Timer: User-friendly utility class
File notifications
Core::Notifier handles file descriptor readiness:
All events are registered and fire on the event loop of the thread that created them.
Best practices
Do’s
Create in main()
Create your main event loop in
main() and pass it to classes that need itKnow your thread
Only access the event loop when you know which thread you’re on
Signal across threads
Use event signaling to communicate with event loops on other threads
Handle events anywhere
It’s fine to handle events from any thread
Don’ts
- DO NOT store an event loop in a global variable
- The event loop relies on global variable initialization
- UBSAN will catch initialization order fiascos
- DO NOT access the current event loop if you don’t know your thread
- The program will crash if there’s no event loop on your thread
- DO NOT
pump()orexec()the event loop of another thread- Sleeping and waking relies on thread-local variables
- Only signal events to other threads’ event loops
Example usage
Here’s a typical pattern for GUI applications:Return value pattern
The patternreturn app->exec() is common in GUI applications because:
exec()returns a value comparable to process return codes- The event loop processes events until the application exits
- The return value propagates through to the process exit code
Integration with LibGUI
LibGUI is deeply integrated with the event loop:- GUI::Application: Creates and manages the main event loop
- GUI::Window: Can create nested event loops for modal dialogs
- Widget events: Mouse, keyboard, and paint events delivered via event loop
- Timers: Used for animations, periodic updates, and timeouts
Integration with IPC
Ladybird’s IPC system relies heavily on the event loop:- IPC connections register file notifiers for socket readiness
- Messages are dispatched as events through the event loop
- Asynchronous IPC responses delivered via deferred callbacks
Related documentation
Architecture overview
Overview of Ladybird’s architecture
Process architecture
Multi-process design and IPC