Crate lives

Crate lives 

Source
Expand description

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. 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>.
  2. When operating the pointee from LifeWeak<Obj<'static>>::with, no LifeRc<Obj<'a>> can be dropped.

The crate currently verifies the preimse #1 by the Life::covariance method, and the premise #2 by putting a guard upon dropping LifeRc<Obj<'a>>.

§Why Rc and Weak don’t suffice?

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

use std::rc::{Rc, Weak};

// While it's possible to hold a String here
// to get rid of the lifetime problem, we are
// just writing so to simulate an object
// annotated with lifetime.
struct Str<'a> (&'a str);

fn main() {
    let mut weaks: Vec<Weak<Str<'_>>> = Vec::new();
    
    {
        let a = format!("{}", 123456);
        let a_rc = Rc::new(Str(a.as_str()));
        weaks.push(Rc::downgrade(&a_rc));
        // Dropping `a` here is intended, to
        // simulate the case when the pointee
        // of weak pointer goes out of lifetime.
    }
    
    let b = format!("{}", 789012);
    let b_rc = Rc::new(Str(b.as_str()));
    weaks.push(Rc::downgrade(&b_rc));
    
    for weak in weaks {
        if let Some(rc) = weak.upgrade() {
            println!("{}", rc.0);
        }
    }
}

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:

use std::rc::Rc;
struct LifeRc<T> {
    internal: Rc<T>,
}

fn new<'a>(t: Obj<'a>) -> LifeRc<Obj<'a>> {
    LifeRc { internal: Rc::new(t) }
}

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:

use std::rc::Weak;
struct LifeWeak<T> {
    internal: Weak<T>,
}

fn downgrade<'a>(rc: &LifeRc<Obj<'a>>) -> LifeWeak<Obj<'static>> {
    let weak: Weak<Obj<'a>> = Rc::downgrade(&rc.internal);
    let internal: Weak<Obj<'static>> = edit_lifetime::<'static>(weak);
    LifeWeak { internal }
}

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

fn with<F, U>(
    weak: &LifeWeak<Obj<'static>>, callback: F,
) -> Option<U>
where
    F: for<'b> FnOnce(&'b Obj<'b>) -> U,
{
    'b: {
        let rc: Rc<Obj<'static>> = weak.internal.upgrade()?;
        let t: &'b Obj<'b> = edit_lifetime::<'b>(&*rc);
        Some(callback(t))
    } // end of 'b
}

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, rendering at least one LifeRc<Obj<'a>> to be alive before entering LifeWeak<Obj<'static>>::with. By premise #2, since we cannot drop the any LifeRc<Obj<'a>> during the execution of LifeWeak<Obj<'static>>::with, the LifeRc<Obj<'a>> which has been inspected to be alive will remains to be alive before exiting LifeWeak<Obj<'static>>::with. Since [LifeRc<Obj<’a>>] cannot outlives outlives 'a, lifetime 'a must be alive during the whole execution of LifeWeak<Obj<'static>>::with.

  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 #1 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.

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

use lives::{Life, LifeRc, LifeWeak};

#[derive(Life)]
struct Str<'a> (&'a str);

fn main() {
    let mut weaks: Vec<LifeWeak<Str<'static>>> = Vec::new();
    
    {
        let a = format!("{}", 123456);
        let a_rc = LifeRc::new(Str(a.as_str()));
        weaks.push(LifeRc::downgrade(&a_rc));
    }
    
    let b = format!("{}", 789012);
    let b_rc = LifeRc::new(Str(b.as_str()));
    weaks.push(LifeRc::downgrade(&b_rc));
    
    for weak in weaks {
        weak.with(|r| println!("{}", r.0));
    }
}

Structs§

LifeRc
Lifetime-bounding reference-counting pointer.
LifeWeak
Lifetime-dynamic weak pointer.

Traits§

Life
Lifetime-bounded type trait.

Derive Macros§

Life
Simply implement Life for the specific type.