Help with using `Any` to cast `T<'a>` back and forth

I really need to temporarily cast T<'a> to Any so I'm able to later on cast it back to T<'a> again (well, actually 'b which must be outlived by 'a. Unfortunately as things are right now Any has an artificial limitation of requiring 'static.

I think I've figured out a way to use transmute to cheat the system, yet put a wrapper around the whole thing to enforce safety. Unfortunately my lifetime-fu is way too weak to have confidence in the whole thing.

The code in question:

I also wonder if there are existing libraries that would provide this in a more generic way, and is it even possible and/or worthwhile.

The limitation isn't "artificial".

Any works by using a TypeId (essentially just an integer), where each type gets a unique TypeId and we check that before doing any downcasting.

However, the TypeId doesn't/can't contain any lifetime information, so allowing downcast() with non-'static types would let people trivially break Rust by transmuting between lifetimes.

Your Caster type uses the lifetime parameters on new_transmute() and as_mut() to make sure lifetimes aren't violated, so it should be sound. You might want to try running a bunch of tests under Miri to double-check, though.

2 Likes

Transmuting references is usually better expressed with pointer casts: &mut *(t as *mut Ta as *mut Tstatic).

2 Likes

It's not sound -- I can obey the safety restrictions on new_transmute and cause UB with safe code elsewhere, because as_ref returns the lifetime-erased version. Playground.

If instead it returned something with the lifetime 'caster (which might be shorter than the original struct's lifetime), it would still be unsound as I could push something with a shorter lifetime into the Vec, and then pop it out of the original Vec with a longer lifetime.

Perhaps if Caster carried around a PhantomData<&'caster mut Ta>, you could transmute to that in as_ref. (Or pointer cast.)

4 Likes

Ugh, I just reread my own post and it's unclear, because I used 'caster in two places to refer to two different things. You can't return "Tstatic but with lifetime 'caster" anyway, for the same reason it had to be a separate type parameter on new_transmute.

Anyway, I still think you need something like

pub struct Caster<'caster, Ta>(
    &'caster mut dyn Any,
    PhantomData<&'caster mut Ta>,
);

(Edit to s/Tstatic/Ta/g. Taking this as a cue to shut the laptop for now.)

2 Likes

That's a very good point. Thanks a lot! I'll try to fix it once I find some time.

I don't think there's a way to extract the precise lifetime from a non-static generic type. (You can't give it an upper limit*.) Instead I think you'll need implementations that explicitly tie lifetimes together.

Like so maybe. Someone else should review it. Sorry it's low on comments.

(If it happens to be sound, there's probably a crate for this?)

(Edit: *: Unless something like that well-formed-references hack could apply... I shouldn't make unqualified statements :slightly_smiling_face:)

1 Like

Perhaps some of the tricks that are being proposed for the standard library to deal with downcasting of error contexts like backtraces could be of use? The dyno crate (mentioned under Prior art in the RFC text) for example seems like it could be used to solve this problem.

use dyno::{Tag, Tagged};

#[derive(Debug)]
pub enum Error {
    WrongType,
}

/// Dynamic cast helper
///
/// This struct allows an implementation of a Repository
/// to cast at runtime a type-erased [`Transaction`] or [`Connection`]
/// instance to back to a concrete type that it needs and expects.
pub struct Caster<'borrow, 'value>(&'borrow mut (dyn Tagged<'value> + 'value));

impl<'borrow, 'value> Caster<'borrow, 'value> {
    pub fn new<I: Tag<'value>>(any: &'borrow mut I::Type) -> Self {
        Self(<dyn Tagged>::tag_mut::<I>(any))
    }

    // Returns `Result` so it's easier to handle with ? than an option
    pub fn as_mut<I: Tag<'value>>(self) -> Result<&'borrow mut I::Type, Error> {
        self.0.downcast_mut::<I>().ok_or_else(|| Error::WrongType)
    }
}

struct Transaction<'a> {
    data: Vec<&'a str>,
}

impl<'a> Tag<'a> for Transaction<'static> {
    type Type = Transaction<'a>;
}

fn main() {
    let s = "local".to_string();
    let mut v = Transaction { data: vec![&s] };
    let caster = Caster::new::<Transaction<'static>>(&mut v);
    let erased_v = caster.as_mut::<Transaction<'static>>().unwrap();
    let yoinks: &'_ str = erased_v.data.pop().unwrap();
    drop(s);
    // This line won't compile:
    //println!("{}", yoinks);
}
2 Likes

I was able to convert the code to use dyno sniper/persistence.rs at d423eb20cba7bcaef17596cc5b19e24b8dac7718 · dpc/sniper · GitHub

Thank you all for a lot of great feedback in this thread. It's much appreciated and even though these complex mechanisms a little beyond my lifetime-fu, with your help and little bit of dancing with compiler errors I am able to piece things together to work.

1 Like