Help to resolve E0521 lifetime problem

I am using the spec crate to implement some framework. When I want to reduce some LazyUpdate boilerplate code like this

pub fn lazy_exec<'a, T, D>(f: T)
where
    D: SystemData<'a>,
    T: Send + Sync + FnOnce(D) + 'static,
{
    let lazy_update = unsafe { ECS_CONTEXT.assume_init_ref().lazy_update.as_ref().unwrap() };
    lazy_update.exec_mut(move |world| {
        world.exec(f);
    })
}

But the compiler complains about E0521 like this

error[E0521]: borrowed data escapes outside of closure
   --> src\lib.rs:273:9
    |
266 | pub fn lazy_exec<'a, T, D>(f: T)
    |                  -- lifetime `'a` defined here
...
272 |     lazy_update.exec_mut(move |world| {
    |                                -----
    |                                |
    |                                `world` is a reference that is only valid in the closure body
    |                                has type `&'1 mut specs::World`
273 | 
        world.exec(f);
    |         ^^^^^^^^^^^^^
    |         |
    |         `world` escapes the closure body here
    |         argument requires that `'1` must outlive `'a`

I can't figure out how to resolve this. Thanks for your help in advance.

As written, you're saying that the caller of lazy_exec specifies the lifetime 'a when it defines T, but the lifetime you need for D is the one chosen for world by exec_mut.

I think that what you want is to say that the closure T has to be correct for all possible lifetimes, so that it's definitely valid for the lifetime of world. You'd do that by changing the function signature as follows:

pub fn lazy_exec<UpdateFn, Data>(f: UpdateFn)
where
    Data: for<'a> SystemData<'a>,
    UpdateFn: Send + Sync + FnOnce(Data) + 'static,
{

I've made the type parameter names longer, because I find that easier to read: the important change is that I've removed 'a from the function's list of type parameters, and instead said that Data must be a valid SystemData<'a> for all possible 'a lifetimes, using the "higher-ranked trait bounds" syntax to tell Rust that Data: SystemData<'a> holds for any possible lifetime 'a (even though that's an infinite set).

1 Like

The Data: for<'a> SystemData<'a> bound effectively means that Data can't be something that borrows from the World. Crafting a bound that can support that is a lot more involved, and tends to wreck inference.

Example.

1 Like

Thanks for the detailed example. I have encountered another problem if my SystemData contains 'a lifetime
example

Here's a midway point that doesn't wreck inference like my last reply did. On the downside, it does require T: 'static when the borrowing implementation is for &'a T.

Then we have your latest edition:

pub struct Read<'a, T>(PhantomData<&'a T>);

impl<'a, T> SystemData<'a> for Read<'a, T> {
    fn fetch(_: &'a World) -> Self {
        Read::<T>(PhantomData::default())
    }
}

This is a borrowing implementation, but not with references. (Anything where the implementation has 'a in both SystemData<'a> and in the implementing type is a borrowing implementation.)

So we add another implementation that follows the same template we used for references:

// A marker type to represent implementors who borrow by `Read<'a, T>`
pub struct SysRead<T>(PhantomData<T>);

impl<T, F> LazyClosure<SysRead<T>> for F
where
    F: Send + Sync + 'static + FnOnce(Read<'_, T>),
    T: 'static,
    for<'a> Read<'a, T>: SystemData<'a>,
{
    type Data<'a> = Read<'a, T> where Self: 'a;
}

(This one doesn't use ?Sized bounds on T because Read<'_, T> doesn't support ?Sized types for T. If you let Read<'_, T> support unsized T, you would want to update this code too.)

And then everything works.

Every borrowing type constructor (&'a T, Read<'a, T>) will need its own sentinel type (like SysRead<T>) and implementation of LazyClosure<Sentinel<T>>. You could make a macro or two if you wanted to reduce boilerplate.

Making this work with T that aren't 'static may be possible with more elbow grease.

Ok, it wasn't even that hard.

The idea is we have broke up our GAT like so:

pub trait LazyClosureBase<'a, U>: Send + Sync + 'static + FnOnce(Self::DataBase) {
    type DataBase: SystemData<'a>;
}

pub trait LazyClosure<T>: for<'a> LazyClosureBase<'a, &'a T> {}

// We supply this implementation for everything that meets the
// supertrait bound, so no one else need implement it.  Instead
// they should implement `LazyClosureBase<'a, &'a T>`.
impl<T, F> LazyClosure<T> for F
where
    F: for<'a> LazyClosureBase<'a, &'a T>
{}

The &'a T in for<'a> LazyClosureBase<'a, &'a T> supplies an implicit where T: 'a bound so that T: 'static is not required even though we're using higher-ranked for <'a> bounds.

Then, we change the implementations to implement LazyClosure<'a, &'a Sentinel<T>> for one specific lifetime 'a, for example...

// A marker type to represent implementors who borrow by shared reference
pub struct Ref<T: ?Sized>(PhantomData<T>);

impl<'a, T, F> LazyClosureBase<'a, &'a Ref<T>> for F
where
    F: Send + Sync + 'static + FnOnce(&'a T),
    T: ?Sized,
    &'a T: SystemData<'a>,
{
    type DataBase = &'a T;
}

...and everything that worked before continues to work, and borrowing implementations with a non-'static T also work now.

fn non_static<'a>() {
    lazy_exec(|_: Read<'_, &'a str>| {});
}
1 Like

It's a bit tricky to understand the advanced usage of combining HRTB and GAT. According to your method, I can indeed use Read<'a>, but in specs there are items like (Read<'a, T> ReadStorage<'a, T>). In the current implementation, it actually requires enum implementations, which is quite cumbersome. I think it's better to revert to using LazyUpdate directly. Anyway, your answer has given me a deeper understanding of how to solve these types of problems, thank you very much.

1 Like

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.