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

Haiku BReference: Shared Lifetimes Without Shared-State Guarantees

Use Haiku BReference and BReferenceable for explicit shared object lifetimes, transfer semantics, final release, and thread-safe ownership boundaries.

BReferenceable and the BReference<T> template implement intrusive reference counting in Haiku’s Support Kit. The count lives in the object; reference wrappers acquire and release it as they are copied, reassigned, or destroyed. This can express shared lifetime across asynchronous work and message-driven components without a separate control block.

It does not provide shared ownership of mutable fields, a cycle collector, weak references, or protection from stale raw pointers. The most important design task is to make each reference-count transition represent a real ownership transfer. A counter can keep an object allocated; it cannot make an unsynchronized update safe or repair a reference to an object whose count already reached zero.

The initial reference and adopt semantics

The current BReferenceable constructor initializes the count to one. When the final reference is released, the default LastReferenceReleased() implementation deletes the object. This initial reference belongs to whoever creates the object and must be adopted or released exactly once. A newly allocated object is therefore not automatically owned correctly just because it is immediately wrapped.

#include <Referenceable.h>

class Record : public BReferenceable {
public:
    void SetLabel(const char* label);
};

BReference<Record>
CreateRecord()
{
    // The initial count of one becomes the wrapper's owned reference.
    return BReference<Record>(new Record(), true);
}

The second argument true means that the incoming pointer already carries a reference, so the wrapper adopts that count instead of incrementing it. It is appropriate here because the count of one created by BReferenceable is the ownership being transferred. If you instead construct BReference<Record>(raw) with the default false, the wrapper acquires another count; the original creator must then release its own initial reference with ReleaseReference() when it has actually transferred ownership. Omitting that release leaks the object. Releasing twice risks deleting it while a wrapper still holds a pointer.

Prefer factory functions that return a BReference<T> or accept an explicit ownership contract, so callers do not need to infer who owns the initial reference. Avoid storing a newly allocated referenceable object on the stack while allowing BReference instances to outlive the stack frame: the default final-release hook uses delete this, and that conflicts with stack ownership.

Copies, assignment, detach, and release

Copying a BReference<T> creates another owning reference by calling AcquireReference() on the object. Destruction calls Unset(), which releases the wrapper’s reference. SetTo() can replace its held object; the implementation acquires a non-null new object before unsetting the old one, which makes self/aliasing transitions safe under the object lifetime contract. Get() exposes a non-owning raw pointer for immediate access while the wrapper remains alive.

Detach() returns the pointer and clears the wrapper without releasing it. That is a transfer of an existing reference, not a way to “get a pointer” while leaving ownership unchanged. After detaching, the caller must eventually put that reference into another owner or call ReleaseReference() exactly once. Conversely, Unset() releases and sets the wrapper empty. Check IsSet() before dereferencing a nullable wrapper, and do not dereference it after Unset() or Detach().

Avoid APIs that take both an owning wrapper and an unannotated raw pointer with unclear lifetime. Choose one convention for function boundaries:

  • Pass const BReference<T>& when the callee only needs the object to remain alive for the duration of the call and should not retain ownership.
  • Pass or return a BReference<T> by value when ownership is intentionally shared or transferred.
  • Use a raw T* only when the surrounding code clearly guarantees lifetime for the full access and retention is forbidden.

Document whether the callee stores another reference. That distinction determines whether the caller may release its own wrapper immediately after the call returns.

Final release is a destruction callback

LastReferenceReleased() is invoked when the previous count was one and the release operation reaches the final reference. The default implementation deletes the object. A subclass may override this hook to implement a special final-release policy, but any override must preserve a well-defined destruction path and must not assume the object can be resurrected safely by acquiring a reference after the count has reached zero.

Do not block for arbitrary work in final release. Destruction can occur on whichever thread drops the last wrapper, not necessarily the UI thread or a dedicated worker. If cleanup must occur on a particular looper, arrange an explicit shutdown/owner-thread handoff before the reference count reaches its terminal release, and keep the object alive until that handoff finishes. A destructor should not synchronously wait for a callback that itself needs the thread currently running the destructor.

The current AcquireReference() and ReleaseReference() use atomic operations for the count. That protects the counter transition, not the object’s data members. Two threads may safely own references concurrently while still racing on Record::SetLabel() or a mutable cache. Add a lock, publish immutable state, or confine mutations to one thread. Keep the lifetime and data-race questions separate in code review.

Avoid resurrection, dangling pointers, and cycles

Never create a new BReference<T> from a raw pointer that may already have reached zero. Once the final release begins, deletion can happen immediately, and a raw pointer retained elsewhere is already invalid. Use an existing live BReference to acquire another reference. Do not build a “weak” relationship by storing a raw pointer and hoping to check a count later; reading freed memory to ask for its count is itself invalid.

Intrusive reference counting also cannot collect cycles. If object A owns B through a BReference, and B owns A through another BReference, both counts remain nonzero after external owners disappear. Break cycles with a single-owner direction, explicit detach on shutdown, a non-owning relationship protected by another lifetime mechanism, or an application-level graph cleanup. A raw pointer observer is only safe if its owner explicitly unregisters it before destruction and races are controlled.

Take special care with callbacks, listeners, and parent/child graphs. A listener retaining its publisher while the publisher retains the listener can form a cycle. A worker task retaining a model is often correct, but if the model retains the task handle, there may be a cycle until cancellation. Write a lifecycle diagram, identify the external roots, and test that the count returns to zero after success, failure, cancellation, and shutdown paths.

Cross-thread messages and object data

Reference-count ownership does not automatically extend through a serialized BMessage. If a message contains a flattened object snapshot, it has its own value data. If it carries an in-process pointer through a supported mechanism, the recipient must still have a valid lifetime contract. Do not place a raw pointer into message data and assume serialization makes it safe or portable.

For worker handoff, the task can hold a BReference<Model> while computing. If it needs to publish a result, send an immutable result message or a separately reference-counted result object. The UI handler should check a request/generation identifier, update its own model state, and release the result when done. Cancelled work may still finish; the held reference keeps the object alive but does not mean the result is still current.

Object destruction may run on the worker that released the final reference. If the object includes Interface Kit views or other thread-affine resources, do not rely on BReferenceable to make destruction happen in the correct thread. Separate thread-affine resources into an owner-thread component and arrange explicit disposal there before dropping the last reference to the plain data object.

Test ownership as a state machine

Instrument a test subclass with construction/destruction counters and exercise the full ownership graph. Start at the initial count of one; adopt it; copy wrappers; assign one wrapper to another live object; call Unset(); Detach() and re-adopt; then let the final wrapper leave scope. Assert exactly one destruction. Add tests for a null wrapper and assigning an empty wrapper. Use a deliberate cycle test to prove your application cleanup breaks the graph rather than expecting reference counting to do it.

For concurrency, test that wrapper copies and releases can occur on separate threads while object data is either immutable or protected by its own lock. Use sanitizers where available in the build environment, but remember that a clean test run cannot justify unsynchronized mutable access. For final-release work, record the thread identity and verify the application has no hidden UI-thread requirement in the destructor path.

Review every alreadyHasReference use as a transfer assertion. Check every Detach() for a matching new owner or release. Check every raw pointer that survives a method call and every observer registration for an unregister path. These checks catch the common leak and use-after-free failures more effectively than merely printing CountReferences() during normal execution.

BReference is a compact ownership tool when a graph genuinely needs shared lifetime. Correctness still depends on explicit initial ownership, a one-way acyclic or manually broken graph, data synchronization separate from reference counting, and final cleanup that respects thread affinity.

Related:

Sources:

Comments