Trying to implement a TypeMap that can hold references, having problems with their lifetimes

I'm trying to create a component system where any component can "publish" an instance of a type to components rendering under it, which is indexable by it's type. It resembles Flutter's BuilderContext.

I simplified the example to the following:

use std::any::{Any, TypeId};
use std::collections::HashMap;

// State
pub struct Ctx {
    map: HashMap<TypeId, Box<dyn Any>>
}

impl Ctx {
    pub fn new() -> Self {
        Ctx {
            map: HashMap::new()
        }
    }

    pub fn inject<W, T: 'static, F: Fn(&mut Self) -> W>(&mut self, value: T, cb: F) -> W {
        let maybe_old_val = self.map.insert(TypeId::of::<T>(), Box::new(value));
        let ret_val = cb(self);
        match maybe_old_val {
            Some(old_val) => {
                self.map.insert(TypeId::of::<T>(), old_val);
            }
            None => {
                self.map.remove(&TypeId::of::<T>());
            }
        };

        ret_val
    }

    pub fn get<T: 'static>(&self) -> Option<&T> {
        match self.map.get(&TypeId::of::<T>()) {
            Some(the_box) => {
                the_box.downcast_ref().take()
            }
            None => None
        }
    }

    pub fn get_mut<T: 'static>(&mut self) -> Option<&mut T> {
        match self.map.get_mut(&TypeId::of::<T>()) {
            Some(the_box) => {
                the_box.downcast_mut().take()
            }
            None => None
        }
    }
}

fn main() {
    let mut ctx = Ctx::new();
    
    println!("outer f64: {:?}", ctx.get::<f64>());
    println!("outer i32: {:?}", ctx.get::<i32>());
    
    ctx.inject(123.0, |ctx| {
        println!("inner f64: {:?}", ctx.get::<f64>());
        println!("inner i32: {:?}", ctx.get::<i32>());
        
        ctx.inject(321, |ctx| {
            println!("inner-er f64: {:?}", ctx.get::<f64>());
            println!("inner-er i32: {:?}", ctx.get::<i32>());
        });
    });
}

Playground link

This gives:

outer f64: None
outer i32: None
inner f64: Some(123.0)
inner i32: None
inner-er f64: Some(123.0)
inner-er i32: Some(321)

As expected.

But if I try to pass a reference to something on the stack (In the real application I'm passing SDL2's context, it cannot be static), the compiler gives an error:

struct ABC(i32);

fn main() {
    let mut ctx = Ctx::new();
    let abc = ABC(123);

    ctx.inject(&abc, |ctx| {
        println!("inner abc: {:?}", ctx.get::<&ABC>().unwrap().0);
    });
}
error[E0597]: `abc` does not live long enough
  --> src/main.rs:56:16
   |
56 |       ctx.inject(&abc, |ctx| {
   |       -          ^^^^ borrowed value does not live long enough
   |  _____|
   | |
57 | |         println!("inner abc: {:?}", ctx.get::<&ABC>().unwrap().0);
58 | |     });
   | |______- argument requires that `abc` is borrowed for `'static`
59 |   }
   |   - `abc` dropped here while still borrowed

Playground link

I noticed that encasing the struct in a Box works, but I see no reason to allocate it on the stack (also I want to support passing references, I may not own everything, or it might be stored in a struct)

Cloning is also not a solution because they may not be clonable (like the SDL2 context) or be very big (image or video resources).

I have been trying to solve this for hours now, any ideas?

1 Like

Sorry, this is not possible because of limitations on std::any. If the limitations weren't there you could simply do this:

pub struct Ctx<'a> {
    map: HashMap<TypeId, Box<dyn Any + 'a>>,
}
impl<'a> Ctx<'a> {
    // and replace all the T: 'static bounds with T: 'a
}

Unfortunately, as also written in the doc, type ids are not available for types that are not static. Being 'static means that it contains no references.

The compiler error regarding the struct is because the Any trait requires the type to be static. Take a look at the definition:

pub trait Any: 'static {
    fn type_id(&self) -> TypeId;
}

The compiler errors related to T: 'static vs T: 'a in the methods are because TypeId::of require that the type is 'static.

2 Likes

I see.
Is it possible to tell the compiler the lifetime is OK (eg. transmute it to static for the duration of the closure)?
I haven't been able to make that work, but I'm rather inexperienced with rust (let alone unsafe rust :slight_smile:)

Well you can fix the Any: 'static issue using unsafe and pointers, but you will have to find a replacement for TypeId in your hashmap.

I would try and find another way to solve your problem, using unsafe can lead to lots of subtle bugs which will be a nightmare to debug. For example, you could have a seperate buffer where you store your sdl2 Context, and then just store the index into the buffer into the typemap.

struct Index(usize);

let mut buffer = Vec::new();

let index = Index(0);
buffer.push(context);

typemap.insert(index);

Then whenever you get Index you access that buffer. Because Index is always 'static, you are guaranteed to be able to put it in the typemap.

I'd like to preserve the ctx.get::<T>() signature, because opaque indicies for every type can get confusing (also I'd like to avoid having to declare each struct which can be used with the context, to generate the index for example).
Thanks for the suggestion though! :slight_smile:

Is it not possible to transmute &'a T to &'static T and get the TypeId of that?

Well, the issue is that your type is T, and it just so happens that T = &R, so the reference is hidden inside T. You can't talk about &'static R, because it has nothing to do with T.

To make it worse, you may have types like &'a std::slice::Iter<'b>, so it's not only the lifetime in the reference you need to change.

1 Like

You could make something like this:

fn insert<T>(item: &'a T)

since then you can mention T instead of the reference.

That doesn't solve the &'a std::slice::Iter<'b> problem though, right?

Nope!

bummer :confused:

Oh well, Boxes it is
Thanks for everyone who helped :slight_smile: