Pre-RFC: `Weak::upgrade_or_recover()`


#1

Summary

This is an RFC to add APIs to std::rc::Weak to allow recover shared object after every Rcs are dropped.

Identical APIs will be added for std::sync::Weak.

Also, it proposes additional guarantee to align of Rc's and Arc's underlying pointer that it should be greater than or equal to align of usize.

Motivation

Currently rc::Weak doesn’t own shared object, but still own memory allocation itself. It means shared object will be drop()ed when every Rcs are dropped, but underlying memory will not be freed until every Weaks also are dropped.

But after shared object is dropped, Weak becomes completely useless even it still owns shared allocation. It’s conceptually like shared Box<Option<T>>, but cannot turn it “on” after turned off.

Guide-level explanation

This RFC proposes some methods to allow re-construct shared object from std::rc::Weak handle using existing allocated memory after shared object is dropped. It also covers std::sync::Weak, with non-blocking atomic operations.

Also, with this RFC Weak::new() becomes useful as it can be used for lazy-construction of shared object. So this RFC also propose small change to semantics of Weak::new() that it does not allocate memory until recovered or cloned. To do this without change Weak's size and make it zero-cost, additional guarantee will be added that align of RcBox is equal or greater than of usize.

Reference-level explanation

Pointer alignment

Result of Rc::into_raw(Self) -> *const T and Arc::into_raw(Self) -> *const T pointer must be aligned with at least mem::align_of::<usize>().

Additional methods of std::rc::Weak<T>

/// Returns `true` if underlying object is alive
fn can_upgrade(&self) -> bool;

/// Try upgrade, and recover if failed
fn upgrade_or_recover<F: FnOnce() -> T>(&self, f: F) -> Rc<T>;

Additional methods of std::sync::Weak<T>

/// Returns `true` if underlying object is alive
/// Note that this state can always be changed by other threads
fn can_upgrade(&self) -> bool;

/// Try upgrade, and try recover if failed
/// Return `Err` if another thread is already doing it.
fn try_upgrade_or_recover<F: FnOnce() -> T>(&self, f: F) -> Result<Arc<T>, RecoverError>;

Changed semantics

Both std::rc::Weak::new() and std::sync::Weak::new() will not allocate memory until cloned or recovered.

Drawbacks

This adds complexities to stdlib.

Alternatives

  1. Simulate this as a crate using Rc<RefCell<Option<T>>>.

  2. Skip changing semantics of Weak::new().

  3. Skip guaranteed pointer alignment.

  4. Only implement this for Rc and skip for Arc

Unresolved questions

Should we need another recovery method that does not tries to upgrade first?


#2

Are there concrete use cases for an API like this, where the alternatives are somehow not good enough?

This proposal makes perfect sense to me, so I would expect that the decision to actually add these methods/guarantees is going to come down to how compelling the use cases are.


#3

First of all, RcBox contains 2 usize fields to store weak/strong refcounts so it’s already aligned with it and very unlikely to be changed. internship(written by me) relies on this assumption to distinguish inline strings while still be NonNull-able.

Currently I’m writting mqtt brocker(highly WIP) and need some channel state that shared between publishers and subscribers. To minimize memory usage, it should be lazily-constructed and cleared when nobody subscribes it, even some publishers remaining. But recover its state when new subscriber arrives.

For now I’m using some types like Option<Rc<RefCell<Option<Channel>>>>, seriously. First Option is for lazy construction, and second one allows publishers to hold Rc while nobody subscribes it, so actual Channel is not necessary. With this RFC I can rewrite that type to Weak<Channel> with some fine-grained interior mutability within Channel.