Reduce lifetime of fields in generic parameter

I've recently written a bunch of unsafe code and am now fighting with an invariant which I don't know how to deal with. I stripped the example bellow down as much as possible, but it's still quite a lot of code, sorry. In the real solution, I make sure that the scarry generic parameter in get_second_as just affects *const Fn() of a list which have the correct signature. Link to the full project is here.
Does anybody know how to restrict all inner references of T to the lifetime of &'a self intead of 'static in get_second_as? As lifetimes are defined by the caller, I guess that this must somehow be embedded in RefFamily?

I know that I could just use Wrapper<Rc> and everything works fine. But I'd prefer a solution, where references could be used, as i make sure, that all references in these constructor-Lambdas have a shorter or same lifetime as Container.

use core::{any::Any, marker::PhantomData};

#[test]
fn lifetime_demo() {
    let c = Container::new(3);
    let w = c.get_second_as::<Query<Wrapper>>();
    // Shouldn't compile anymore when drop is uncommented. 
    // Unfortunately it does, because Wrapper.0 is &'static (results in undefined behaviour)
    // drop(c); 
    assert_eq!(w.0, &3);
}
pub trait FamilyLt<'a> {
    type Out: 'a;
}
pub struct RefFamily<T: Any>(PhantomData<T>);
impl<'a, T: 'a + Any> FamilyLt<'a> for RefFamily<T> {
    type Out = T;
}

trait QueryTrait {
    type Item: for<'a> FamilyLt<'a>;
}

struct Query<T: Any>(PhantomData<T>);
impl<T: Any> QueryTrait for Query<T> {
    type Item = RefFamily<T>;
}

struct Wrapper<'a>(&'a i32);
struct Container(*const (), *const dyn Fn());

impl Container {
    fn new(a: i32) -> Self {
        let nr_pointer = Box::into_raw(Box::new(a));
        let wrapper_generator: Box<dyn Fn() -> Wrapper<'static>> = Box::new(
            || Wrapper(unsafe {&*nr_pointer}) 
        );
        Container(nr_pointer as *const (), Box::into_raw(wrapper_generator) as *const dyn Fn())
    }

    fn get_second_as<'a, T: QueryTrait>(&'a self) -> <T::Item as FamilyLt<'a>>::Out {
        let func_ptr = self.1 as *const dyn Fn() -> <T::Item as FamilyLt<'a>>::Out;
        let ptr = unsafe { &* func_ptr };
        (ptr)()
    }
}
impl Drop for Container {
    fn drop(&mut self) {
        unsafe {
            drop(Box::from_raw(self.0 as *mut i32));
            drop(Box::from_raw(self.1 as *mut dyn Fn()));
        }
    }
}

Please include your imports in the snippets.

I just updated the snippet :sweat_smile:

There are no good solutions to this, ideally you would want a bound like 'a: T which means 'a is at least as long as all the most restrictive lifetime in T, but we don't have that bound. You can simulate this with the following trait.

// simulates the 'a: T bound
pub trait MaxLifetime<'a> {}
impl<'a: 'b, 'b> MaxLifetime<'a> for Wrapper<'b> {}

Second, ditch Any. You can't use it. Any has a 'static bound, but you want fundamentally non-'static values, so you can't use Any. For the same reasons you can't use for<'a> FamilyLt<'a>

Third, get_second_as should be unsafe because you could pass in an incorrect query and get undefined results.

Here's is a version with these fixes: Rust Playground


I'll now explain how to correctly implement MaxLifetime.

All the fields of T should implement MaxLifetime<'a>. All types with no type or lifetime parameters implement for<'a> MaxLifetime<'a>, and references MaxLifetime<'a> like so

impl<'a: 'b, 'b, T: ?Sized> MaxLifetime<'a> for &'b T {}

Here's a playground to test this out

edit: fixed rules for implementing MaxLifetime

3 Likes

I might have found the problem but i don't understand it. Can someone explain why the first of bellow snippets works and the second doesn't?

let wrapper_generator = Box::new(
    |c: &i32| &1
) as Box<dyn for <'a> Fn(&'a i32) -> &'static i32>;
let wrapper_generator = Box::new(
    |i: &i32| i
) as Box<dyn for <'a> Fn(&'a i32) -> &'a i32>;

The first one puts 1 into static storage and returns a reference to that, so it will always be &'static i32, the second should work but closures don't interact with lifetimes nicely, so it doesn't. If you replace |i: &i32| i with a named function fn foo(i: &i32) -> &i32 { i } it does work. I think I saw an issue about this, but I can't find it.

2 Likes

Oh I see... Either T could be Any or have Lifetime information... Maybe I can get around this with another trait layer and some unsafe code, because I need Any to find the appropriate lambdas in a HashMap. Thank you very much for your time, maybe I can get it working with the max lifetime idea. I'll come back to you if it works :slight_smile:
Very strange behaviour with the boxed lambda though... I'd even consider it to be a bug.

Nope, it would be unsound to work around this, any usage of downcasting requires 'static bouds in order to be sound. Otherwise you could transmute lifetimes in safe code (which is widely unsafe)

So would I :slight_smile:

1 Like

I actually don't need downcasting, just the type_id to lookup the appropriate constructor function pointers (fn(&'a Container) -> T+'a). The library has full controll over all pointers. When get() assures to return only stuff the Container must outlive, there shouldn't be any unsound API... If you find some time I would be very grateful for your opinion about the project. This "outliving the container" is the only unsound behaviour I could spot. But maybe you find some more and I'd be very interested in your overall opinion about the project. The example workspace should be a good starting point.

But it already got wayy more complicated than i expected... Maybe I'll just go back to Arc for the sake of simplicity :frowning:

PS: You can do the above without a named function:

let wrapper_generator = Box::new((|c: &i32| { &c }) as fn(&i32) -> &i32) 
            as Box<dyn for <'a> Fn(&'a i32) -> &'a i32>;

That is downcasting. Casting Fn() to Fn() -> SomeRandomOutput is downcasting because you're treating Fn() as some arbitrary trait which doesn't actually matter.

1 Like

So it's UB to cast a Fn(&Container) -> T to Fn() and back to Fn(&Container) -> T? Can you recommend a good source to read more about this?

No, it's just that there is no way to automatically verify the correctness of this at compile time or runtime. You will have to manually verify the correctness of the code. This is because there isn't a way to distinguish between lifetimes. For example

struct Foo<'a, 'b> { ... }

trait Erased {}

// how would you verify the correctness of `foo`?
fn foo<'a, 'b>(erased: Box<dyn Erased + 'a>) -> Foo<'a, 'b> { ... }

The user of the library doesn't have the possibility to pass any Fn() to my lib. Just context less fn(). All contexted Fn are created within the library...

Ok, can the user manipulate the type parameter T in your lib's version of

fn get_second_as<'a, T: QueryTrait>(&'a self) -> <T::Item as FamilyLt<'a>>::Out

Yes he can... Maybe I should tell you a little more about the program-flow:
First you generate a ServiceCollection, where you register new Services:

let mut col = ServiceCollection::new();
col.register_transient::<i32>(|| 1);
let provider = col.build().unwrap();
assert_eq!(Some(1), provider.get::<Transient<i32>>());

I register the fn into a Hashmap with key "TypeId::of::", when someone requests a T, I look for a TypeId::of::() in the hashMap and know, that it will return a T and I can transpile safely... Or am I wrong about that?

Oh, I see. In that case it should be fine as long as the registered types are 'static, otherwise you could transmute lifetimes. I would stick to Arc as you mentioned before to sidestep the lifetime issues entirely.

I'll probbably wrap Arc in a newtype which implements just AsRef to prevent Users to make more copies.

I just hate the fact that the concept totally works but is not expressable... :frowning:

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.