#lifetime #ownership #reference

no-std cryo

Extend the lifetime of a reference. Safely.

16 releases

0.3.1 Oct 26, 2021
0.2.7 Oct 25, 2021
0.2.2 May 8, 2021
0.1.6 Jan 8, 2020
0.1.2 Nov 13, 2018

#624 in Rust patterns

Download history 26/week @ 2023-11-05 8/week @ 2023-11-12 8/week @ 2023-11-19 81/week @ 2023-11-26 4/week @ 2023-12-03 9/week @ 2023-12-10 30/week @ 2023-12-17 56/week @ 2023-12-24 4/week @ 2023-12-31 7/week @ 2024-01-07 11/week @ 2024-01-14 23/week @ 2024-01-21 77/week @ 2024-01-28 4/week @ 2024-02-04 62/week @ 2024-02-11 386/week @ 2024-02-18

529 downloads per month
Used in interlock

MIT/Apache

49KB
691 lines

Cryo — Extend the lifetime of a reference. Safely.

docs.rs

Requires Rust 1.34.0 or later.

This crate provides a cell-like type Cryo that is similar to RefCell except that it constrains the lifetime of its borrowed value through a runtime check mechanism, erasing the compile-time lifetime information. The lock guard CryoRef created from Cryo is 'static and therefore can be used in various situations that require 'static types, including:

  • Storing CryoRef temporarily in a std::any::Any-compatible container.
  • Capturing a reference to create a Objective-C block.

This works by, when a Cryo is dropped, not letting the current thread's execution move forward (at least¹) until all references to the expiring Cryo are dropped so that none of them can outlive the Cryo. This is implemented by readers-writer locks under the hood.

¹ SyncLock blocks the current thread's execution on lock failure. LocalLock, on the other hand, panics because it's designed for single-thread use cases and would deadlock otherwise.

Examples

with_cryo, Cryo, and LocalLock (single-thread lock implementation, used by default):

use std::{thread::spawn, pin::Pin};

let cell: usize = 42;

// `with_cryo` uses `LocalLock` by default
with_cryo(&cell, |cryo: Pin<&Cryo<'_, usize, _>>| {
    // Borrow `cryo` and move it into a `'static` closure.
    let borrow: CryoRef<usize, _> = cryo.borrow();
    let closure: Box<dyn Fn()> =
        Box::new(move || { assert_eq!(*borrow, 42); });
    closure();
    drop(closure);

    // Compile-time lifetime works as well.
    assert_eq!(*cryo.get(), 42);

    // When `cryo` is dropped, it will block until there are no other
    // references to `cryo`. In this case, the program will leave
    // this block immediately because `CryoRef` has already been dropped.
});

with_cryo, Cryo, and SyncLock (thread-safe lock implementation):

use std::{thread::spawn, pin::Pin};

let cell: usize = 42;

// This time we are specifying the lock implementation
with_cryo((&cell, lock_ty::<SyncLock>()), |cryo| {
    // Borrow `cryo` and move it into a `'static` closure.
    // `CryoRef` can be sent to another thread because
    // `SyncLock` is thread-safe.
    let borrow: CryoRef<usize, _> = cryo.borrow();
    spawn(move || { assert_eq!(*borrow, 42); });

    // Compile-time lifetime works as well.
    assert_eq!(*cryo.get(), 42);

    // When `cryo` is dropped, it will block until there are no other
    // references to `cryo`. In this case, the program will not leave
    // this block until the thread we just spawned completes execution.
});

with_cryo, CryoMut, and SyncLock:

with_cryo((&mut cell, lock_ty::<SyncLock>()), |cryo_mut| {
    // Borrow `cryo_mut` and move it into a `'static` closure.
    let mut borrow: CryoMutWriteGuard<usize, _> = cryo_mut.write();
    spawn(move || { *borrow = 1; });

    // When `cryo_mut` is dropped, it will block until there are no other
    // references to `cryo_mut`. In this case, the program will not leave
    // this block until the thread we just spawned completes execution
});
assert_eq!(cell, 1);

Don't do these:

// The following statement will DEADLOCK because it attempts to drop
// `Cryo` while a `CryoRef` is still referencing it, and `Cryo`'s
// destructor will wait for the `CryoRef` to be dropped first (which
// will never happen)
let borrow = with_cryo((&cell, lock_ty::<SyncLock>()), |cryo| cryo.borrow());
// The following statement will ABORT because it attempts to drop
// `Cryo` while a `CryoRef` is still referencing it, and `Cryo`'s
// destructor will panic, knowing no amount of waiting would cause
// the `CryoRef` to be dropped
let borrow = with_cryo(&cell, |cryo| cryo.borrow());

Caveats

  • While it's capable of extending the effective lifetime of a reference, it does not apply to nested references. For example, when &'a NonStaticType<'b> is supplied to Cryo's constructor, the borrowed type is CryoRef<NonStaticType<'b>>, which is still partially bound to the original lifetime.

Details

Feature flags

  • std (enabled by default) enables SyncLock.

  • lock_api enables the blanket implementation of Lock on all types implementing lock_api::RawRwLock, such as spin::RawRwLock and parking_lot::RawRwLock.

  • atomic (enabled by default) enables features that require full atomics, which is not supported by some targets (detecting such targets is still unstable (#32976)). This feature will be deprecated after the stabilization of #32976.

Overhead

Cryo<T, SyncLock>'s creation, destruction, borrowing, and unborrowing each take one or two atomic operations in the best cases.

Neither of SyncLock and LocalLock require dynamic memory allocation.

Nomenclature

From cryopreservation.

License: MIT/Apache-2.0

Dependencies

~56KB