Skip to content

Weak References Proposal

Jens Alfke edited this page Feb 13, 2026 · 2 revisions

It would help make LiteCore safer if Fleece supported weak references to RefCounted objects, i.e. a WeakRetained<> / WeakRef<> type. The difference is that

  • The existence of a WeakRef to an object does not prevent that object from being destructed when all strong references are gone.
  • Dereferencing a WeakRef whose object has been destructed returns nullptr, not a dangling pointer.

Weak references are notoriously tricky to implement in a multithreaded environment. Here's how I plan to do it.

API

The only visible API changes are the addition of a WeakRetained<> template, and an accessor that returns a RefCounted object's weak ref-count (for debugging purposes.)

WeakRetained<> has almost the same API as Retained<> except that it's missing get(), operator * and operator ->. That's because dereferencing has to be done with caution since the value can unpredictably change to nullptr. Instead there's a method tryGet() that returns a Retained<>. If the object still exists this creates a new strong reference that remains valid as long as the returned Retained<> does. Otherwise the return value will contain nullptr. You have to check.

Implementation

High Level

We give RefCounted a second ref-count, weakCount. It starts at 0. Methods _weakRetain and _weakRelease increment and decrement it. The WeakRetained<> template calls these, the same way that Retained calls _retain / _release.

The magic happens when a release call drops the refCount to 0 while there is a non-zero weakCount. In this situation it does not delete the object; instead it puts it into a "zombie" state by calling its destructor but not freeing the heap block. This way the weak references still point to valid memory. (Calling the destructor makes the object free any auxiliary memory it's holding onto, and -- importantly! -- release any references to other RefCounted objects. This minimizes the amount of left-over memory.)

WeakRetained::tryGet() checks the object's refCount; if it's zero it returns nullptr. Otherwise it calls retain and returns a Retained<> pointing to the object. That ensures that the object will remain intact until the caller is done using it.

The private RefCounted::_weakRelease decrements the weakCount. If it hits zero and the refCount is also zero, it calls operator delete to free the object's heap block.

Low Level

The hard part is how to make this thread-safe (and fast.)

As before, we use atomic. This time both ref-counts are stored in a single atomic<uint64_t> called _bothCounts: the regular refCount in the lower 32 bits, the weakCount in the upper 32 bits. This allows us to atomically update and view both of them.

We slightly change the interpretation of the regular refCount. Basically, we add 1 to it, so it's initialized to 1, and when it's released back down to 1 the object now has no strong references (but is still alive). A refCount of 0, with a nonzero weakCount, now denotes the zombie state.

Here's what the various RefCounted methods do:

constructor

  • Initializes refCount to 1 and weakCount to 0.

Postcondition: Object is in the "initialized" state.

retain

Precondition: A strong reference exists on this thread, or the object is in the "initialized" state.

  • Increments refCount. (Actually it increments _bothCounts directly, which has the same effect.)

Postcondition: A new strong reference is created.

release

Precondition: A strong reference exists on this thread

  • Decrements & reads refCount. (Actually it decrements _bothCounts directly, which has the same effect.)
  • If the new value of refCount is 1:
    • If weakCount is 0: deletes this as usual.
    • Else:
      • Calls the destructor, i.e. this->~RefCounted().
      • Decrements & reads refCount again, leaving it 0 -- this marks the object as a zombie.
      • If weakCount is now 0: frees the block by calling operator delete(this).

Postcondition: The strong reference no longer exists.

weakRetain

Precondition: A strong or weak reference exists on this thread.

  • Increments weakCount (by adding 1 << 32 to _bothCounts.)

Postcondition: A new weak reference is created.

weakRelease

Precondition: A weak reference exists on this thread (but will no longer exist upon return.)

  • Decrements & reads weakCount (by subtracting 1 << 32 from _bothCounts.)
  • If weakCount is 0 and refCount is 0: calls operator delete(this).

Postcondition: The weak reference no longer exists.

weakToStrong

Precondition: A weak reference exists on this thread.

  • Repeat:
    • Read _bothRefCounts.
    • If the refCount is 0 or 1, return false
    • Call compare_exchange_strong() to add 1 to _bothRefCounts if it still has the same value
    • If it succeeded: return true.
    • Else: restart the loop.

Postcondition: If true was returned, a new strong reference is created; on false, the object no longer exists.

Implementation of WeakRetained

The WeakRetained class is mostly identical to Retained except that

  • It calls weakRetain and weakRelease instead of retain and release
  • The tryGet() method calls weakToStrong.
    • on success it returns a Retained instance adopting the new strong ref.
    • on failure it clears itself (releases its weak ref and clears its pointer) and returns nullptr.

Clone this wiki locally