#static #downcast #type-id #any #macro-derive #invariants

transient

Reimplementation of std::any::Any with support for non-'static types

8 unstable releases (3 breaking)

0.4.1 Sep 8, 2024
0.4.0 Jun 30, 2024
0.3.0 Jun 23, 2024
0.2.1 Jun 19, 2024
0.1.2 Jun 15, 2024

#570 in Rust patterns

Download history 120/week @ 2024-08-22 14/week @ 2024-08-29 223/week @ 2024-09-05 28/week @ 2024-09-12 12/week @ 2024-09-19 23/week @ 2024-09-26 17/week @ 2024-10-03 8/week @ 2024-10-10 2/week @ 2024-11-14 2/week @ 2024-11-21 29/week @ 2024-11-28 48/week @ 2024-12-05

81 downloads per month
Used in 3 crates (via rspc)

MIT/Apache

115KB
1K SLoC

transient

This crate provides a reimplementation of the std::any::Any trait supporting types with non-'static lifetimes.

Crates.io

Documentation

Module documentation with examples

Usage

To bring this crate into your repository, either add transient to your Cargo.toml, or run cargo add transient.

Using this crate starts by implementing the provided Transient trait for a type, which can be derived using the included derive macro or implemented by hand by simply defining two associated types. Implementing this trait manually is unsafe but straightforward and extensively documented, and once implemented it enables this crate's functionality to be used safely.

The following example demonstrates the trivial case of deriving the Transient trait for a 'static type, and then casting it to a dyn Any trait object to emulate dynamic typing just as you would using the stdlib's implementation:

use transient::{Transient, Any, Downcast};

#[derive(Transient, Debug, PartialEq)]
struct MyUsize(usize);

fn main() {
    let orig = MyUsize(5);
    // we can erase the 'static type...
    let erased: &dyn Any = &orig;
    assert!(erased.is::<MyUsize>());
    // and restore it...
    let restored: &MyUsize = erased.downcast_ref().unwrap();
    assert_eq!(restored, &orig);
    // and use it in dynamically-typed shenanigans...
    let bananas = "bananas".to_string();
    let stuff = vec![erased, &orig.0, restored, &bananas];
    assert_eq!(stuff[0].downcast_ref::<MyUsize>().unwrap(), &orig);
}

Where it get's interesting is when you have a non-'static type containing borrowed data, which would be ineligable for use with the std::any::Any implementation due to its 'static bound. The following example will demonstrate using the transient crate's implementation to utilize the same functionality as for 'static types by simply parameterizing the Any trait by Inv, which is a Transience implementation that binds the lifetime and variance information that the stdlib would not be able to handle:

use transient::{Transient, Any, Inv, Downcast};

#[derive(Transient, Debug, PartialEq)]
struct MyUsizeRef<'a>(&'a usize);

fn main() {
    let five = 5;
    let orig = MyUsizeRef(&five);
    // we can erase the non-'static type...
    let erased: &dyn Any<Inv> = &orig;
    assert!(erased.is::<MyUsizeRef>());
    // and restore it...
    let restored: &MyUsizeRef = erased.downcast_ref().unwrap();
    assert_eq!(restored, &orig);
    // and use it in dynamically-typed shenanigans...
    let stuff = vec![erased, &five, restored, &"bananas"];
    assert_eq!(stuff[0].downcast_ref::<MyUsizeRef>().unwrap(), &orig);
}

The Inv type used above stands for "invariant", which is the most conservative form of a property known as variance that describes the behavior of a type with respect to a lifetime parameter. An understanding of variance will let you utilize the advanced features of this crate, but is not necessary for most purposes since the Inv type shown above be safely used for any type with a single lifetime parameter.

In the first example where we cast our type to a naked dyn Any without specifying a Transience type, the Any trait's default type parameter () was chosen implicitly which causes it to behave like the stdlib's implementation by only accepting 'static types; trying this with MyUsizeRef defined in the second example would have been rejected by the compiler. This hints at the underlying mechanism used to implement this crate, wherein types declare their temporal relationships (i.e. Transience) when implementing the Transient trait, which then bounds the range of dyn Any<_> flavors they are allowed to utilize. Non-'static types with a single lifetime 'a that implement Transient using the derive macro are (by default) assigned a Transience of Inv<'a>, which limits them to being erased to (and restored from) the dyn Any<Inv<'a>> trait object. By contrast, 'static types implement the most flexible Transience of () which allows them to be be cast to any dyn Any<_> they want, up to and including the default dyn Any().

There is a large amount of middle-ground between these two extremes which is discussed in-depth throughout the documentation (hint - there are Co and Contra types as well), but the key takeaway is that types make a single unsafe but straight-forward decision about their temporal behavior when implementing the Transient trait, and then everything else is kept safe using the type system and trait bounds.

Usage: multiple life parameters

The mechanism demonstrated above extends naturally to types with more than one lifetime parameter by instead parameterizing the dyn Any<_> with a tuple as shown in the following example; however, the included derive macro currently only supports types with zero or one lifetime parameters, so we will implement the Transient trait ourselves this time:

use transient::{Transient, Any, Inv, Downcast};

#[derive(Debug, PartialEq)]
struct TwoRefs<'a, 'b>(&'a i32, &'b i32);

unsafe impl<'a, 'b> Transient for TwoRefs<'a, 'b> {
    // the `Static` type is simply the same as the `Self` type with its 
    // lifetimes replaced by `'static`
    type Static = TwoRefs<'static, 'static>;
    // we use a tuple for the `Transience` that covers both lifetimes, using 
    // `Inv` for each element since this is always safe
    type Transience = (Inv<'a>, Inv<'b>);
}

fn main() {
    let (five, seven) = (5, 7);
    let orig = TwoRefs(&five, &seven);
    let erased: &dyn Any<Inv> = &orig;
    assert!(erased.is::<TwoRefs>());
    let restored: &TwoRefs = erased.downcast_ref().unwrap();
    assert_eq!(restored, &orig);
    let stuff = vec![erased, &five, restored, &"bananas"];
    assert_eq!(stuff[0].downcast_ref::<TwoRefs>().unwrap(), &orig);
}

License

This project is licensed under either of

at your option.

Dependencies

~0–6MB
~32K SLoC