Skip to content
Haiku OSDeep Dive Published Updated 6 min readViews unavailable

Haiku BMessenger Delivery: Target Identity, Replies, Timeouts, and Shutdown

Use BMessenger as a message endpoint while accounting for handler lifetime, ambiguous signatures, synchronous reply waits, and remote delivery failures.

BMessenger is Haiku’s value-like endpoint for sending a BMessage to a BHandler or BLooper. It supports both targets in the same team and applications in other teams, so it provides a useful boundary between event-driven components without handing out a raw pointer as the communication contract. It is not, however, a durable guarantee that a particular handler will remain alive, nor does a successful send necessarily mean the requested operation has completed.

Those distinctions matter most during shutdown and error recovery. An application can still have a messenger whose target looper exists while its specific handler has gone away. A message may be accepted for delivery, processed later, or fail because the target disappeared. A synchronous reply can block the caller until the target responds or a timeout expires. Treat target identity, delivery status, and operation completion as separate states.

Construct the endpoint for a stable target

A local messenger can be initialized from a handler and, optionally, its looper. The handler must already belong to a looper; if both are supplied, it must belong to that looper. If the handler is null, the looper’s preferred handler is targeted. Capture the status_t returned through the optional result parameter instead of silently storing a potentially uninitialized messenger.

A messenger can also identify a running application by signature and team ID. A signature alone is ambiguous when multiple instances are running: the documentation says the selected instance is indeterminate. When a particular instance matters, use its team ID and verify the signature when appropriate. Conversely, if the intent is to address any running instance, state that explicitly in the application protocol instead of assuming the signature uniquely names one process.

IsValid() is a liveness hint, not a lease

BMessenger::IsValid() reports whether its target looper still exists. The Haiku Book explicitly warns that this does not check whether the target handler still exists. It also cannot reserve the target so it remains alive until a later call. Between checking validity and sending, the application can exit or the target can be removed.

For that reason, avoid this pattern as if it made delivery certain:

if (messenger.IsValid()) {
    messenger.SendMessage(&request);
}

The check may be useful for updating UI state or avoiding an obviously pointless request, but the SendMessage() result remains authoritative for delivery status. Robust callers handle failure even after a successful validity check. If a handler’s lifetime is shorter than the looper’s, design an explicit registration/unregistration or request/reply protocol instead of relying on IsValid() to represent handler liveness.

Delivery, asynchronous replies, and synchronous replies

SendMessage() has overloads with different reply behavior. Sending with a reply handler or reply messenger allows the response to be delivered asynchronously. Sending with a reply BMessage waits synchronously for a response. Do not conflate “message delivered to the target queue” with “the target completed the requested work”; those are different protocol milestones.

Use asynchronous replies for UI and cross-team work when the sender must remain responsive. Give each request a correlation identifier if several may be outstanding, and define how cancellation, duplicate delivery, target restart, and late replies are handled. A response can arrive after the initiating UI has changed state or been closed, so the reply handler should validate that the result still belongs to the current operation.

Synchronous reply calls are appropriate only when blocking is acceptable and the wait cannot form a dependency cycle. Never synchronously wait on the application’s own looper for a response that must be produced by that same looper: the receiver cannot process the request while the sender is blocked. The same issue can arise indirectly when two teams or threads synchronously call one another in opposite order.

Treat timeout parameters as separate boundaries

The synchronous message/reply overload exposes a delivery timeout and a reply timeout. A timeout is not proof that the target did nothing. It may have received and started the operation while the response was delayed or lost to a shutdown race. If retrying could repeat a destructive action, include an idempotency key or operation identifier and let the receiver deduplicate.

The asynchronous overload with a reply endpoint also has a delivery timeout. Check the exact overload and default values in the SDK reference rather than assuming all timeout parameters bound the same phase. Record the actual status_t and operation ID in diagnostics. A single UI message such as “request failed” obscures whether endpoint setup, delivery, or work completion failed.

Local target locks are not remote locks

LockTarget() and LockTargetWithTimeout() are limited to local targets; the reference documents failure for remote targets. They are convenience operations for accessing the target looper under its lock, not a general cross-team mutual-exclusion primitive. Prefer sending a message that asks the target to perform an operation on its own state. Locking another component and directly reaching into it couples lifetime, thread ownership, and implementation details.

Also avoid holding unrelated application locks while sending a synchronous request. The target may call back, acquire a lock in the reverse order, or wait for work that depends on the sender. Message passing is most valuable when it preserves ownership boundaries, so keep the protocol asynchronous unless the synchronous contract is demonstrably safe.

Make shutdown an explicit protocol state

When a component or application begins shutdown, stop scheduling new work, invalidate or replace UI-facing request state, and arrange for late responses to be harmless. The sender should be prepared for delivery errors; the receiver should be prepared for incomplete clients and should not retain pointers that outlive the target. For application signatures, remember that a new process instance may later appear with the same signature but a different team ID.

For a long-running operation, model states such as created, sent, accepted, completed, failed, timed out, and cancelled explicitly. The messenger transports the message; the protocol defines what each state means. Include a version or capability field when independently released applications can evolve their message format, and reject unsupported operations with a structured response rather than silently ignoring them.

Test endpoint failure deliberately

Exercise local and remote targets separately. Test an absent application signature, multiple instances, a stale team ID, a handler removed while its looper remains alive, an application exit immediately after enqueue, a delayed reply, a reply after the UI is closed, and a synchronous timeout. Verify that UI state and resources recover without leaking workers or retrying unsafe actions.

Log the target’s team ID when it is known, request identifiers, send status, reply status, and timing for delivery and processing. Avoid logging sensitive payload fields indiscriminately; a transport trace needs protocol metadata, not user content. Confirm all timeout and retry semantics against the actual installed Haiku SDK.

The Haiku Book’s BMessenger API reference is authoritative for constructor, validity, locality, lock, send, and reply behavior. Use BMessenger as a communication endpoint, not as a substitute for a lifetime protocol. Its safest use is one where both sides make failures, asynchronous completion, and shutdown behavior explicit.

Related:

Sources:

Comments