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:
- The pointee
Obj<'a>is (reducible to be) parameterized by a single lifetime parameter'aand covariant over lifetime'a. That is, if'bis a lifetime completely contained in'a, thenObj<'a>can be used in place ofObj<'b>. - When operating the pointee from
LifeWeak<Obj<'static>>::with, noLifeRc<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
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
ais in, let it be'a, thenweaks: Vec<Weak<Str<'a>>>continues to live after exiting the blockais in, which violates the rule of an object cannot outlives its lifetime annotation. - If the lifetime is as long as the one
where
weaksandbis in, let it be'b, thenRc::downgrade(&a_rc)is not assignable toWeak<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:
-
In the
LifeWeak<Obj<'static>>::withfunction, if we are able to upgrade the internal weak pointer, then at least one reference countingRc<Obj<'a>>is still alive, rendering at least oneLifeRc<Obj<'a>>to be alive before enteringLifeWeak<Obj<'static>>::with. By premise #2, since we cannot drop the anyLifeRc<Obj<'a>>during the execution ofLifeWeak<Obj<'static>>::with, theLifeRc<Obj<'a>>which has been inspected to be alive will remains to be alive before exitingLifeWeak<Obj<'static>>::with. Since [LifeRc<Obj<’a>>] cannot outlives outlives'a, lifetime'amust be alive during the whole execution ofLifeWeak<Obj<'static>>::with. -
Therefore lifetime
'bis completely contained in'a. In the functionLifeWeak<Obj<'static>>::with, we reconstruct the lifetime ofObj<'a>intoObj<'b>, and by premise #1 thatObj<'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>. Thustis 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§
Traits§
- Life
- Lifetime-bounded type trait.