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

Haiku BMessageQueue: Message Ownership, Borrowed Lookups, and Compound Locking

Use Haiku BMessageQueue with explicit heap ownership, locked compound operations, borrowed-pointer boundaries, bounded queues, and coordinated shutdown.

BMessageQueue is a first-in, first-out container used by Haiku’s application messaging machinery. Its pointer-based API makes simple operations look inexpensive, but ownership and lifetime are inseparable from those pointers. Enqueuing a message transfers responsibility; finding a message does not remove it; dequeuing changes who must eventually destroy it.

This article focuses on explicit queue use and compound operations. It distinguishes the Haiku Book’s guidance from implementation details observed at source revision b52e0e4935aa749a857a0f6a649e91134a6538c8. It does not recommend bypassing a looper’s normal delivery protocol or treating a queue lock as a complete application shutdown mechanism.

The queue owns enqueued messages

The Haiku Book requires messages passed to AddMessage to be heap allocated with new, because the queue claims ownership. A stack-allocated message is not an acceptable shortcut. When the queue is destroyed, it deletes messages still contained in it.

Consequently, do not enqueue a pointer and then delete it in the producer. Do not retain a unique_ptr that still owns the same enqueued object. If using an ownership wrapper, transfer ownership only at the accepted enqueue boundary, under the component’s verified queue-lifetime and locking protocol.

A message must not already belong to another queue. The pinned implementation links messages through an internal field in BMessage; it is not a separate wrapper node holding an independent copy. Adding the same object twice or to two queues can corrupt linkage and produce double destruction. If two recipients need equivalent payloads, create separate messages rather than sharing one queue-owned pointer.

AddMessage returns void. It is not a status-bearing publication acknowledgement that callers can use to distinguish every failure. Check the outer lock and subsystem state before committing the ownership transfer; do not invent a returned status or claim that a void call confirms delivery to a handler.

Dequeue transfers a different responsibility

NextMessage removes and returns the first message, or returns null when there is none. After successful removal, the consumer becomes responsible for processing and destroying that object. Wrapping the removed pointer immediately in std::unique_ptr<BMessage> makes that responsibility visible.

#include <Message.h>
#include <MessageQueue.h>
#include <memory>

status_t TakeOne(BMessageQueue& queue, std::unique_ptr<BMessage>& output)
{
    // Caller guarantees the queue remains alive throughout this operation.
    if (!queue.Lock())
        return B_ERROR;

    BMessage* message = queue.NextMessage();
    queue.Unlock();
    output.reset(message);
    return message != nullptr ? B_OK : B_ENTRY_NOT_FOUND;
}

The output parameter is replaced, so its prior owned message is destroyed by reset. Callers that need to preserve an existing result should use an empty output or another interface. The helper separates lock failure from an empty queue, but its generic B_ERROR is application policy rather than a detailed queue-provided error code.

The queue lock is released before the consumer processes the message. Holding it across arbitrary handlers, disk work, or external callbacks increases contention and can create reentrant deadlocks. Once removed, the message no longer needs the queue lock for its own lifetime, although application data accessed while processing needs its own synchronization.

Lookup returns a borrowed pointer

FindMessage searches without removing. Its result remains queue-owned. It is not an owned copy or a reference-counted handle that survives concurrent removal or queue destruction.

Keep the outer lock while inspecting or copying the found message, and keep queue lifetime stable for the operation. Returning that borrowed pointer to code that will use it after unlocking allows another consumer to remove and delete it first. A valid pointer observed under a lock can become invalid immediately after the lock is released.

If an operation needs to remove a selected message, perform lookup and RemoveMessage while holding the same outer lock. RemoveMessage does not return a success status, so prove membership through the locked lookup instead of blindly deleting an arbitrary supplied pointer after the call.

The two FindMessage overloads have different indexing domains. The plain-index variant counts all messages; the what variant counts matching messages. Keep the event code’s type explicit when calling overloaded methods, and test cases with interleaved message types. An integer that accidentally selects another overload can return a plausible but wrong object.

Lock the whole decision, not just one method

The Book encourages locking before operations and says that NextMessage needs that lock. The pinned source internally acquires a BAutolock in NextMessage, AddMessage, removal, and lookup. That implementation detail does not eliminate the need for an outer lock around a multi-step decision.

For example, checking a count and then adding a message is not an atomic capacity check if another producer can add between the two calls. Finding a message and then removing it is not safe when another consumer can intervene. Hold the queue lock across the complete state transition while keeping the queue object alive.

Haiku’s BLocker supports repeated acquisition by its owning thread, which explains why an outer queue lock can coexist with the implementation’s internal locks. Balance the external lock once for each successful external acquisition. Do not unlock because a method happened to use an internal lock; its internal scope is separate from the caller’s lock scope.

In the pinned source, CountMessages and IsEmpty read the stored count without taking an internal lock. Treat those observations as requiring the same caller-side synchronization as other compound decisions. A count is not a durable promise that a subsequent dequeue will return a message after releasing the lock.

Capacity and wakeup are separate protocols

The queue provides storage and FIFO operations, not a built-in bounded producer/consumer scheduler. Define a maximum queue depth or byte budget if producers can outrun consumers. Check and update that budget atomically with enqueue under the chosen lock.

Choose an overflow policy appropriate to message meaning. Dropping an old redraw request may be acceptable; dropping a financial transaction or shutdown acknowledgement is not. Coalescing messages by what alone can erase distinct payloads if that code represents several resources. Define a stable coalescing key and preserve required completion semantics.

A separate semaphore or condition mechanism can notify consumers about work. Its accounting must track successful queue publication and removal; the queue pointer alone does not wake a worker. Avoid publishing a notification for an item the capacity policy rejected, and avoid assuming IsEmpty plus sleep is free from lost wakeups.

Do not use this discussion to manipulate a BLooper’s live queue indiscriminately. Normal posting and delivery maintain application-level behavior beyond a bare container. Filtering or removing pending looper messages requires review of the looper’s ownership, synchronization, and event semantics, not just the queue method signatures.

Destruction is not an admission barrier

The pinned destructor locks and deletes the remaining messages. The Book describes why the implementation does not unlock before destroying its lock. This defensive implementation behavior is not permission for application threads to call methods on an object whose C++ lifetime is ending.

First stop new producers, notify waiting consumers through their normal stop mechanism, wait for users to leave, and then destroy the queue. Decide whether shutdown drains, cancels, or discards pending messages; deleting them silently is not equivalent to delivering cancellation callbacks or completing outstanding requests.

Keep producer references and borrowed lookup pointers out of the retired lifecycle. A queue-specific mutex protects content while the object exists; it cannot keep a dangling BMessageQueue* alive after its owner deletes the queue. Lifetime synchronization must exist outside that object’s disappearing lock.

Verify ownership and concurrency boundaries

On Haiku, enqueue distinct heap messages and verify FIFO removal, empty results, both lookup index domains, and removal of head, middle, and tail. Track destruction in a controlled fixture to prove that removed messages are destroyed by consumers and remaining messages by the queue exactly once.

Test the bounded-enqueue decision with competing producers and prove the limit cannot be exceeded. Exercise shutdown with pending messages and waiting consumers without calling methods after destruction. Avoid using undefined duplicate-enqueue or stack-pointer cases as production probes; review or isolate those misuse cases in dedicated diagnostic tooling.

The C++ helper was source-reviewed, not executed on a Haiku host during authoring. Deployment evidence should name the target build and retain native ownership, queue-depth, and shutdown fixture results. Queue correctness comes from matching pointer responsibility to one locked transition and one valid object lifetime.

Related:

Sources:

Comments