The Looper/Handler Pattern: Message-Passing at the Core of Every Haiku App
Haiku applications don't poll for events in a manual loop — they define Handlers, and let a Looper thread dispatch messages to the right one automatically.
Haiku’s application framework is built around message passing between objects, using a specific pattern inherited directly from BeOS: BLooper and BHandler. Understanding this pair explains how virtually every Haiku application — GUI or not — actually processes events internally.
BHandler: something that can receive a message
A BHandler is any object that can process a BMessage — a typed, structured message containing a “what” code and an arbitrary set of named data fields. A button, a text view, a window itself — all of these are BHandlers, each implementing a MessageReceived() method that acts on whichever messages it cares about and ignores the rest.
void MyHandler::MessageReceived(BMessage* message) {
switch (message->what) {
case kButtonPressed:
// handle it
break;
default:
BHandler::MessageReceived(message); // let the base class handle it
}
}
BLooper: a thread with a message queue
A BLooper owns a dedicated thread and an internal message queue, continuously pulling the next message off that queue and dispatching it to the appropriate BHandler:
BLooper's thread, running continuously:
loop:
message = queue.pop_next()
handler = determine_target_handler(message)
handler->MessageReceived(message)
A BWindow — the base class every Haiku application window derives from — is a BLooper, meaning every window in a Haiku application already has its own dedicated thread pulling and dispatching its messages, independent of every other window’s thread.
Why a message can target one specific handler among many
A single BLooper commonly owns several BHandlers — a window might contain dozens of BHandlers for its individual buttons, text fields, and views. Each BHandler can be looked up by the BLooper via AddHandler()/RemoveHandler(), and incoming messages carry enough addressing information (via the message’s target, set when it was sent) for the BLooper to route it to the specific BHandler responsible, rather than every handler needing to inspect every message and decide for itself whether to act.
Why this is genuinely different from a single shared event loop
Many GUI frameworks route all events through one central, application-wide event loop, with individual widgets registering callbacks against it. Haiku’s model instead gives each BLooper its own thread and its own queue — a window’s message processing genuinely runs independently of another window’s, rather than all UI events funneling through one shared dispatch loop. This is a direct expression of the kernel’s pervasively multithreaded design: rather than treating “give this window its own thread” as an unusual, advanced technique, it’s simply the normal, expected structure of a Haiku application.
The trade-off: message ordering and synchronization become explicit concerns
Genuine per-window concurrency means a developer has to think about synchronization between loopers explicitly — two BLoopers’ threads can genuinely execute at the same time, so shared state between them needs the same care any multithreaded code requires. BMessage passing itself is thread-safe by design (a message can be safely sent from any thread to any BLooper’s queue), which contains most of the danger to the boundary between loopers rather than eliminating the need to think about concurrency altogether.
Why this pattern has aged well
Message-passing concurrency — independent units of execution communicating through queued, typed messages rather than shared mutable state accessed from multiple threads directly — is a pattern that shows up repeatedly across different eras of software design, under different names, precisely because it sidesteps a large class of concurrency bugs by construction. BeOS built this pattern into the absolute core of its application framework in the 1990s; Haiku’s continued use of the exact same BLooper/BHandler design today reflects how well that original decision has held up, rather than being retained purely out of backward-compatibility inertia.