Expand description
§Heavy Elastic - almost-safe abstractions for “stretching” lifetimes (and dealing with what
happens when they have to snap back.)
This crate provides six main abstractions:
Stretchable<'a>
, a trait indicating that some type with a lifetime'a
can have that lifetime'a
removed (and virtually set to'static
.)Stretched
, an unsafe trait denoting that a type is a stretched version of aStretchable<'a>
type.Elastic<T>
, a'static
“slot” which can be temporarily loaned a non-'static
value.ScopeGuard
, a collection allocated in aScopeArena
which gathersElasticGuard
s and ensures that they are not mishandled.ScopeArena
, an arena allocator specialized for safely allocatingScopeGuard
s.ElasticGuard<'a, T>
, a guard which takes on the lifetime of loans to anElastic
and handles expiring the loans.
Requires nightly for #![feature(generic_associated_types, allocator_api)]
.
§Why would I want this?
Elastic
excels at “re-loaning” objects across “'static
boundaries” and “Send
boundaries”.
For example, the Rust standard library’s std::thread::spawn
function requires that the
FnOnce
closure you use to start a new thread is Send + 'static
. I’m calling this a
“'static
boundary” because it effectively partitions your code into two different sets of
lifetimes - the lifetimes on the parent thread, and the lifetimes on the child thread, and
you’re forced to separate these because of a 'static
bound on the closure. So what happens if
you need to send over a reference to something which isn’t 'static
? Without unsafe
abstractions or refactoring to remove the lifetime (which in some cases won’t be possible
because the type isn’t from your crate in the first place) you’re, generally speaking, screwed.
Elastic
lets you get around this problem by providing a “slot” which can have a value safely
and remotely loaned to it.
§Using Elastic
for crossing Send + 'static
boundaries
Let’s first look at the problem without Elastic
:
let my_special_u32 = RefCell::new(23);
// This fails for two reasons: first, RefCell is not Sync, so it's unsafe to Send any kind
// of reference to it across a thread boundary. Second, the `&'a mut RefCell<u32>` created
// implicitly by this closure borrowing its environment is non-static.
std::thread::spawn(|| {
*my_special_u32.borrow_mut() *= 3;
}).join().unwrap();
// We know that the thread will have returned by now thanks to the use of `.join()`, but
// the borrowchecker has no way of proving that! Which is why it requires the closure to be
// static in the first place.
let important_computed_value = *my_special_u32.borrow() * 6 + 6;
If you’re stuck with a RefCell<T>
, it may be hard to see a way to get a mutable reference to
its contained value across a combined 'static + Send
boundary. However, with Elastic
, you
can deal with the situation cleanly, no matter where the &'a mut T
comes from:
// Create an empty "scope arena", which we need for allocating scope guards.
let mut scope_arena = ScopeArena::new();
// Create an empty elastic which expects to be loaned an `&'a mut u32`.
let empty_elastic = ElasticMut::<u32>::new();
{
// Elastics are shared, using an Arc under the hood. Cloning them is cheap
// and does not clone any data inside.
let shared_elastic = empty_elastic.clone();
let mut refcell_guard = my_special_u32.borrow_mut();
scope_arena.scope(|guard| {
// If you can get an `&'a mut T` out of it, you can loan it to an `ElasticMut<T>`.
guard.loan(&shared_elastic, &mut *refcell_guard);
// Spawn a thread to do some computation with our borrowed u32, and take ownership of
// the clone we made of our Elastic.
std::thread::spawn(move || {
// Internally, `Elastic` contains an atomic refcell. This is necessary for safety,
// so unfortunately we have to suck it up and take the extra verbosity.
*shared_elastic.borrow_mut() *= 3;
}).join().unwrap();
// At the end of this scope, the borrowed reference is forcibly removed from the
// shared Elastic, and the lifetime bounds on `scope` ensure that the refcell guard is
// still valid at this point. However, the value inside the refcell has long since been
// modified by our spawned thread!
});
// Now, the refcell guard drops.
}
// The elastic never took ownership of the refcell or the value inside in any way - it was
// temporarily loaned an `&'a mut u32` which came from inside a `core::cell::RefMut<'a, u32>`
// and did any modifications directly on that reference. So code "after" any elastic
// manipulation looks exactly the same as before - no awkward wrappers or anything.
let important_computed_value = *my_special_u32.borrow() * 6 + 6;
// With the current design of Elastic, the scope arena will not automagically release the memory
// it allocated, so if it's used in a loop, you'll probably want to call `.reset()` occasionally
// to release the memory used to allocate the scope guards:
scope_arena.reset();
§Using Elastic
for enabling dynamic typing with non-'static
values
Rust’s Any
trait is an invaluable tool for writing code which doesn’t always
have static knowledge of the types involved. However, when it comes to lifetimes, the question
of how to handle dynamic typing is complex and unresolved. Should a Foo<'a>
have a different
type ID from a Foo<'static>
? As the thread discussing this is the greatest thread in the
history of forums, locked by a moderator after 12,239 pages of heated debate, (it was not and I
am not aware of any such thread; this is a joke) Any
has a 'static
bound
on it. Which is very inconvenient if what you want to do is completely ignore any lifetimes in
your dynamically typed code by treating them as if they’re all equal (and/or 'static
.)
Elastic can help here. If the type you want to stick into an Any
is an &T
or &mut T
, it’s
very straightforward as ElasticRef<T>
and ElasticMut<T>
are both 'static
. If the type
you have is not a plain old reference, it’s a bit nastier; you need to ensure the safety of
lifetime manipulation on the type in question, and then manually construct a type which
represents (as plain old data) a lifetime-erased version of that type. Then, manually implement
Stretched
and Stretchable
on it. This is highly unsafe! Please take special care
to respect the requirements on implementations of Stretchable
. Size and alignment of the
stretched type must match. This is pretty much the only requirement though. The
impl_stretched_methods
macro exists to help you safely implement the methods required by
Stretched
once you ensure the lifetimes are correct. Just note that it does this by swinging
a giant hammer named core::mem::transmute
, and it does this by design, and that if you
screw up on the lifetime safety requirements you are headed on a one way trip to
Undefinedbehaviortown.
§Yes, there are twelve different ways to borrow from an Elastic
, and every single one is useful
How may I borrow from thee? Well, let me count the ways:
§Dereferenced borrows (the kind you want most of the time)
Eight of the borrow methods are “dereferenced” - they expect you to be stretching references or
smart pointers/guards with lifetimes in them. If you’re using ElasticRef
/ElasticMut
or
StretchedRef
/StretchedMut
, these are what you want; they’re much more convenient than
the parameterized borrows.
Elastic::borrow
andElastic::borrow_mut
; most of the time, if you’re working with references being extended, and you don’t care about handling borrow errors, you’ll use these. 99% of the time, they do what you want, and you’re probably going to be enforcing invariants to make sure it wouldn’t error anyways. These are theElastic
versions ofRefCell::borrow
andRefCell::borrow_mut
, and they pretty much behave identicaly.Elastic::borrow_arc
andElastic::borrow_arc_mut
; these come from theArcCell
which lives inside anElastic
, and offer guards juts likeborrow
andborrow_mut
but, those guards are reference counted and don’t have a lifetime attached. So instead ofAtomicRef<'a, T>
, you getArcRef<T>
, which has the same lifetime asT
… and ifT
is'static
, so isArcRef<T>
/ArcRefMut<T>
, which can be very useful, again for passing acrossSend
/'static
/whatnot boundaries.Elastic::try_borrow
,Elastic::try_borrow_mut
,Elastic::try_borrow_arc
,Elastic::try_borrow_arc_mut
; these are just versions of the fourborrow
/borrow_mut
/borrow_arc
/borrow_arc_mut
methods which don’t panic on failure, and returnResult
instead.
§Parameterized borrows (you’re in the deep end, now)
The last four methods are what the other eight are all implemented on. These return Result
instead of panicking, and provide direct access to whatever [T::Parameterized<'a>
] is. In the
case of StretchedRef
and StretchedMut
, we have <StretchedRef<T>>::Parameterized<'a> = &'a T
and <StretchedMut<T>>::Parameterized<'a> = &'a mut T
; when we use a method like
Elastic::try_borrow_as_parameterized_mut
on an Elastic<StretchedMut<T>>
, we’ll get back
Result<AtomicRefMut<'_, &'_ mut T>, BorrowMutError>
which is pretty obviously redundant. It’s
for this reason that the other eight methods exist to handle the common cases and abstract away
the fact that Elastic
is more than just an AtomicRefCell
.
§Safety: ElasticGuard
, ScopeGuard
and ScopeArena
Elastic
works by erasing the lifetime on the type and then leaving you with an
[ElasticGuard<'a>
] which preserves the lifetime. This ElasticGuard
is a “drop guard” - in
its Drop
implementation, it tries to take back the loaned value, preventing it from being
used after the loan expires. There are a couple ways this can go wrong:
- If
Elastic
is currently borrowed when the guard drops, a panic will occur, because the guard’s drop impl needs mutable access to the “slot” inside theElastic
. - If you
core::mem::forget
theElasticGuard
, the slot inside theElastic
will never be cleared, which is highly unsafe, as you now have a stretched value running around which is no longer bounded by any lifetime. This is a recipe for undefined behavior and use-after-free bugs.
The first error case is unavoidable; if you’re loaning stuff out, you might have to drop
something while it’s in use. To avoid this, loaning should be done in phases; loan a bunch of
things at once, ensure whatever is using those loans finishes, and then expire those loans.
Thankfully, the solution to making this easy and avoiding the possibility of the second
failure mode can exist in one primitive: ScopeArena
. ScopeArena
provides a method
ScopeArena::scope
, which allows you to create scopes in which a ScopeGuard
takes
ownership of the ElasticGuard
s produced by the loaning operation. Since the ScopeGuard
is owned by the caller - ScopeArena::scope
- the user cannot accidentally or intentionally
core::mem::forget
a guard, and in addition, the guard ensures that all of the loans made to
it have the same parameterized lifetime, which encourages the phase-loaning pattern.
In short, always use ScopeArena
and ScopeGuard
- if you think you have to use
ElasticGuard
for some reason, double check!
Macros§
- impl_
stretched_ methods - Small convenience macro for correctly implementing the four unsafe methods of
Stretched
.
Structs§
- Elastic
- A container for a stretched value.
- Elastic
Guard - A guard representing a loan of some stretchable value to some
Elastic
. On drop, it expires the loan, borrowing the elastic’s inner slot mutably (panicking if not possible) and - Scope
Arena - An arena for allocating
ScopeGuard
s. - Scope
Guard - A guard which allows “stashing”
ElasticGuard
s for safe loaning. - Stretched
Mut - A type representing a stretched
&mut T
reference. Has the same representation as a*mut T
. - Stretched
Ref - A type representing a stretched
&T
reference. Has the same representation as a*const T
.
Enums§
- Borrow
Error - The error returned when an immutable borrow fails.
- Borrow
MutError - The error returned when a mutable borrow fails.
Traits§
- Stretchable
- Marker trait indicating that a type can be stretched (has a type for which there is an
implementation of
Stretched
, and which properly “translates back” with itsParameterized
associated type.) - Stretched
- A type which is a “stretched” version of a second type, representing the result of having every
single lifetime in that type set to
'static
.
Type Aliases§
- Elastic
Mut - An
Elastic
which is specialized for the task of loaning&'a mut T
s. This is a type synonym forElastic<StretchedMut<T>>
. - Elastic
Ref - An
Elastic
which is specialized for the task of loaning&'a T
s. This is a type synonym forElastic<StretchedRef<T>>
.