Crate hv_elastic

Source
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:

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.

§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:

  1. 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 the Elastic.
  2. If you core::mem::forget the ElasticGuard, the slot inside the Elastic 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 ElasticGuards 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.
ElasticGuard
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
ScopeArena
An arena for allocating ScopeGuards.
ScopeGuard
A guard which allows “stashing” ElasticGuards for safe loaning.
StretchedMut
A type representing a stretched &mut T reference. Has the same representation as a *mut T.
StretchedRef
A type representing a stretched &T reference. Has the same representation as a *const T.

Enums§

BorrowError
The error returned when an immutable borrow fails.
BorrowMutError
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 its Parameterized 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§

ElasticMut
An Elastic which is specialized for the task of loaning &'a mut Ts. This is a type synonym for Elastic<StretchedMut<T>>.
ElasticRef
An Elastic which is specialized for the task of loaning &'a Ts. This is a type synonym for Elastic<StretchedRef<T>>.