Haiku BMessageFilter: Message Admission, Routing, and Handler Ownership
Apply Haiku message filters at handler or looper scope, route messages safely, and avoid ownership, locking, and dispatch-order bugs in native applications.
A Haiku message filter is a synchronous hook in the route from a BLooper’s incoming message queue to a BHandler. It can admit or discard a message, inspect its delivery source and mode, and redirect it to another handler in the same looper. This makes BMessageFilter useful for cross-cutting message policy, but it is not a durable queue, a global event bus, or an authentication boundary.
The design decision is where the policy belongs. A filter attached to one BHandler runs for messages targeted at that handler. A common filter attached to a BLooper sees the looper’s incoming traffic before normal dispatch. The narrower attachment is easier to reason about; the common filter is appropriate when one invariant genuinely applies across the whole looper. Both execute on the looper’s message-processing path, so expensive work or lock-order mistakes can stall every message that depends on that thread.
Match built-in criteria before writing a callback
BMessageFilter supports three built-in criteria: message what code, delivery mode, and source locality. The what code is the message’s command constant. Delivery distinguishes messages posted programmatically from dropped messages. Source distinguishes local from remote senders relative to the application. A filter can use any source and delivery mode, or constrain one or both.
class DropFilter : public BMessageFilter {
public:
DropFilter()
: BMessageFilter(B_ANY_DELIVERY, B_ANY_SOURCE, kInternalUpdate)
{
}
};
This filter selects one command regardless of whether it was posted or dropped and regardless of source. The constructor criteria decide which messages reach custom filtering at all. Use that to keep the hook small and avoid treating every message as a candidate. If a filter does not constrain a command, check FiltersAnyCommand() before relying on Command(); a numeric zero is a valid message code and is not a universal-filter sentinel.
Source and delivery criteria describe the message path, not a trust decision. A local sender is not automatically authorized to mutate sensitive state, and a remote-source filter does not replace validation of message fields. Messages can be malformed, stale, or generated by another application with a valid route. Validate every field needed by the handler, reject unexpected repeated values, and enforce the actual capability or permission in the operation that performs the work.
Implement a custom admission or routing hook
Override Filter() when policy depends on message fields or runtime state. The hook receives the candidate BMessage and a pointer to the proposed BHandler target. Returning B_DISPATCH_MESSAGE allows normal dispatch to continue; returning B_SKIP_MESSAGE discards the message rather than forwarding it to a fallback handler.
class UpdateFilter : public BMessageFilter {
public:
UpdateFilter()
: BMessageFilter(B_ANY_DELIVERY, B_ANY_SOURCE)
{
}
filter_result Filter(BMessage* message, BHandler** target) override
{
if (message == NULL || target == NULL || *target == NULL)
return B_SKIP_MESSAGE;
int32 schemaVersion;
if (message->FindInt32("schema_version", &schemaVersion) != B_OK
|| schemaVersion != 1) {
return B_SKIP_MESSAGE;
}
return B_DISPATCH_MESSAGE;
}
};
The null checks make the example defensive at the hook boundary; the most important validation is the field lookup and explicit version check. A filter should not silently rewrite data into a different command unless that transformation is a documented part of the message protocol. Keep parsing bounded, avoid file or network I/O in Filter(), and do not wait for another looper whose progress might itself depend on this one.
Redirection is possible by replacing the handler through the target pointer. The replacement must belong to the same BLooper as the originally proposed handler. This is an intra-looper routing decision, not a way to transfer ownership of a message to another application’s thread. If a message needs asynchronous work in a different looper, validate it here and post a new, well-defined message using a BMessenger or another explicit handoff.
When a filter function pointer and a Filter() override are both supplied, the callback takes precedence; the override is not a second stage. Choose one extension mechanism so reviewers can see a single policy path. A small subclass is usually clearer when the filter needs object state. A free function can be appropriate for stateless screening that is reused across filters.
Attach filters under the owning looper’s lock
Adding, removing, or replacing a handler’s filter list requires the handler to belong to a BLooper and that looper to be locked. For a common filter, use the BLooper’s corresponding methods while controlling the looper’s lifetime and lock. Do not mutate filter lists from arbitrary worker threads just because the filter object itself is a C++ object.
if (window->Lock()) {
window->AddCommonFilter(new UpdateFilter());
window->Unlock();
}
In a real application, pair the lock with a scope guard or otherwise guarantee unlock on every exit path. The example assumes window is still valid and lock acquisition succeeds. A BLooper may be shutting down or deleted; ownership and lifetime must be established before attempting to attach a filter. Never hold the lock while calling code that can synchronously wait for another looper or call back into the window.
An attached filter participates in the receiving handler or looper’s lifetime. BHandler’s API documents that its filter list is managed by the handler; RemoveFilter() removes a filter without deleting it, so the caller must decide whether to delete or reuse the detached object. The same discipline applies to common filters. Do not manually delete a filter while it remains registered, and do not retain a raw filter pointer after the owner can be destroyed. When replacing a filter list, read the SetFilterList() ownership contract carefully because replacing the list also affects the old list and its contents.
If removal is needed, lock the owning looper first, remove the exact pointer, release the lock, and then dispose of a successfully detached filter if the application no longer needs it. Do not assume RemoveFilter() returning false means the filter is safe to delete; it may mean it was never in that list or the wrong owner was used. Track registration state explicitly when filters have more than one lifecycle path.
Keep dispatch ordering and handler state simple
Filters run on the path that consumes a looper’s messages. A slow filter increases latency for unrelated messages in that looper, including interface updates if the looper is a window. A filter that takes a lock used by the target handler can create lock-order inversion. Define lock ordering once, keep filter work bounded, and copy only the message data needed for later processing.
Do not assume multiple filters behave like a transactional rule engine. Each filter sees a candidate under its own criteria, and the routing result can affect which handler receives the message. Avoid making one filter depend on side effects from another. If a multi-step protocol requires ordering, create one explicit dispatcher that performs the ordered checks and records a clear result.
Use a filter to centralize local policy, not to hide protocol decisions. A handler should still validate its own invariants because messages may reach it through another route or future code path. Filters are especially useful for generic tracing, blocking a class of unwanted UI messages, and routing messages to alternate handlers within one looper. They are a poor place for business state transitions that only one handler understands.
Test the route, not only the predicate
Build a small test matrix for each filter: matching and nonmatching what codes; local and remote sources if relevant; posted and dropped delivery if relevant; valid, missing, duplicate, and wrong-type fields; normal dispatch and deliberate rejection; and alternate target handling if implemented. Confirm a skipped message does not accidentally trigger a fallback path that the application expected.
Exercise removal during shutdown, failed lock acquisition, handler destruction, and repeated installation. Verify that filters are detached before their owner disappears when the application retains references, and ensure no other thread can access the filter after its lifetime ends. Test with a deliberately slow hook and inspect message latency to reveal work that should be moved out of the dispatch path.
BMessageFilter is a compact routing hook with precise boundaries: built-in criteria narrow candidates, Filter() decides dispatch or skip, and redirection remains inside one looper. Keep the hook quick, attach it while the owner is locked, honor the filter-list lifecycle, and retain validation in the target handler. Those rules make filtering a maintainable part of the message architecture rather than a hidden second dispatcher.
Related:
- BMessage Flattening and IPC: How Haiku Moves Typed Data Between Processes
- Haiku BMessenger Delivery: Target Identity, Replies, Timeouts, and Shutdown
Sources: