lives 0.1.0

Lifetime-dynamic smart pointers
Documentation

Lifetime-dynamic smart pointers.

In short, this crate is useful when you want to deal with objects annotated with lifetime. More specifically, when you create weak pointers [LifeWeak] to these objects, so that:

  • The pointee objects live in their own lifetimes, respecting the borrow checker.
  • With the weak pointer, the caller may operate on pointee objects that are still alive, and ignore those who have gone out of their own lifetimes.
  • The lifetime parameters of pointee type are masked on the weak pointer (e.g. Obj<'a> -> LifeWeak<Obj<'static>>) to annotate lifetime checking as dynamic, so that the weak pointers can be held as fields or stored in containers.

This crate works under these premises:

  1. If a lifetime 'a is alive before calling a function, then it will at least be alive until returning from this function.
  2. The pointee Obj<'a> is (reducible to be) parameterized by a single lifetime parameter 'a and covariant over lifetime 'a. That is, if 'b is a lifetime completely contained in 'a, then Obj<'a> can be used in place of Obj<'b>.
  3. When operating on the pointee object from [LifeWeak<Obj<'static>>], either no [LifeRc<Obj<'a>>] is dropped, or the [Drop::drop] behavior of Obj<'a> is completely irrelevant to its lifetime parameter 'a.

These premises can be held at most of cases. But if any of these premises is broken, then the behavior of this crate is undefined.

Why [Rc] and [Weak] don't suffice?

To illustrate the problem, imagine a scenario where lifetime annotated object must be used:

Let's assume the attention of writing so is: While some of these Str<'_> object may go out of scope, we just care about the ones that are still alive, and print them out.

When you plug the code into rust compiler, the borrow checker will soon complain about that a.as_str() borrows a while it does not live long enough. However, the intention of this code is to allow a to drop, in that case, the weak pointer Rc::downgrade(&a_rc) should dereference to None.

While the error originates from the borrow checker, I would say it's rather a type error. We know lifetimes are built into rust types, and in the code above, the borrow checker will attempt to infer the ellided lifetime of Vec<Weak<Str<'_>>>. However, it's impossible to infer such a correct lifetime, since it will be otherwise self-contradictory:

  • If the lifetime is as long as the block where a is in, let it be 'a, then weaks: Vec<Weak<Str<'a>>> continues to live after exiting the block a is in, which violates the rule of an object cannot outlives its lifetime annotation.
  • If the lifetime is as long as the one where weaks and b is in, let it be 'b, then Rc::downgrade(&a_rc) is not assignable to Weak<Str<'b>>, as it does not live long enough to be 'b.

In fact, although we won't be able to access a dead Str<'_> with Weak<Str<'_>>, there's no way to "slack" the lifetime on Weak<Str<'_>>, since the standard library has a good reason to adhere the lifetime: Let there be a weak pointer weak: Weak<Str<'c>> from rc: Rc<Str<'c>>, if we upgrade it back to Some(Rc<Str<'c>>) by weak.upgrade() successfully, then such an upgraded pointer truly holds an object of Str<'c>, which must not outlive 'c. Therefore, as long as we are able to recover a Weak<Str<'_>> back to a Rc<Str<'_>>, the lifetime is mandatory.

Our method

Generally speaking, we diverge from the standard library by forbidding the upgrade of a weak pointer [LifeWeak]. Instead, the caller pass a callback function to [LifeWeak::with], then the weak pointer checks if the pointee is still alive, "fix" the lifetime of the reference to the pointee, to a reasonably short one, and finally pass the reference into the callback to perform the operation.

Let the pointee type be t: Obj<'a>, and we use pseudo rust code to illustrate the idea.

First, the user create reference counting [LifeRc<Obj<'a>>] out of t. The reference-counting pointer is annotated with the lifetime 'a and respect the borrow checker:

Then, the user downgrade [LifeRc<Obj<'a>>] to weak pointer [LifeWeak<Obj<'static>>], and send it to the weak consumer of t. The lifetime 'static means we want to detach the lifetime from 'a and check it at runtime:

Finally, the weak consumer of t pass a callback to [LifeWeak<Obj<'static>>::with], to perform acton on t:

Let's give a closer look at why [LifeWeak<Obj<'static>>::with] works. Recall that we made some premises, they are applicable now:

  1. In the [LifeWeak<Obj<'static>>::with] function, if we are able to upgrade the internal weak pointer, then at least one reference counting [Rc<Obj<'a>>] is still alive, since a [Rc<Obj<'a>>] cannot outlives 'a, lifetime 'a must have been alive before entering [LifeWeak<Obj<'static>>::with], By premise #1, 'a is alive during the whole execution of the [LifeWeak<Obj<'static>>::with] function.

  2. Therefore lifetime 'b is completely contained in 'a. In the function [LifeWeak<Obj<'static>>::with], we reconstruct the lifetime of Obj<'a> into Obj<'b>, and by premise #2 that Obj<'a> is covariant, plus the variance rule of an immutable pointer, we can use &*rc: &'b Obj<'a> in the place of &'b Obj<'b>. Thus t is well-formed.

  3. By premise #3, either we won't drop any [LifeRc<Obj<'a>>] during the execution of callback, so that we don't ever need to care about calling Obj<'static>::drop; or the [Drop::drop] of Obj<'a> is irrelevant to its lifetime 'a, and thus calling Obj<'static>::drop and Obj<'a>::drop are equivalent.In either way, Rc<Obj<'a>> will be maintained correctly.

With our method, we can fix the previous example into: