Asynchronous callback and explicit lifetimes problem

This code works:

use std::future::Future;
pub trait Callback<State> {
    type Output: Future<Output = ()>;

    fn call(&self, state: State) -> Self::Output;
}

impl<State, F, Fut> Callback<State> for F
where
    F: Fn(State) -> Fut,
    Fut: Future<Output = ()>,
{
    type Output = Fut;

    fn call(&self, state: State) -> Self::Output {
        self(state)
    }
}

pub async fn aaa<State, Cb>(state: &mut State, callback: &Cb)
where
    for<'state> Cb: Callback<&'state mut State>,
{
}

pub async fn bbb<State, Cb>(state: &mut State) {
    struct InnerState<'state, State> {
        state: &'state mut State,
        x: i32,
    }
    
    async fn callback<'a, State>(state: &mut InnerState<'a, State>) {}
    
    aaa(&mut InnerState { state, x: 42 }, &callback).await;
}

This does not:

use std::future::Future;
pub trait Callback<'state, State> {
    type Output: Future<Output = ()>;

    fn call(&self, state: &'state mut State) -> Self::Output;
}

impl<'state, State: 'state, F, Fut> Callback<'state, State> for F
where
    F: Fn(&'state mut State) -> Fut,
    Fut: Future<Output = ()>,
{
    type Output = Fut;

    fn call(&self, state: &'state mut State) -> Self::Output {
        self(state)
    }
}

pub async fn aaa<State, Cb>(state: &mut State, callback: &Cb)
where
    for<'state> Cb: Callback<'state, State>,
{
}

pub async fn bbb<State: 'static, Cb>(state: &mut State) {
    struct InnerState<'state, State> {
        state: &'state mut State,
        x: i32,
    }
    
    async fn callback<'a, State>(state: &mut InnerState<'a, State>) {}
    
    aaa(&mut InnerState { state, x: 42 }, &callback).await;
}

with error:

error[E0521]: borrowed data escapes outside of function
  --> src/lib.rs:34:5
   |
26 | pub async fn bbb<State: 'static, Cb>(state: &mut State) {
   |                                      -----  - let's call the lifetime of this reference `'1`
   |                                      |
   |                                      `state` is a reference that is only valid in the function body
...
34 |     aaa(&mut InnerState { state, x: 42 }, &callback).await;
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   |     |
   |     `state` escapes the function body here
   |     argument requires that `'1` must outlive `'static`

Am I adding the explicit lifetimes wrongly or some magic happens in the compiler making code without lifetimes somewhat "superior" (compiling)?

Lifetimes that have explicit bounds in implementations...

//           vvvvvvvvvvvvv
impl<'state, State: 'state, F, Fut> Callback<'state, State> for F
where
    F: Fn(&'state mut State) -> Fut,
    Fut: Future<Output = ()>,

...usually results in HRTBs like for<'state> Cb: Callback<'state, State> to fail or otherwise be problematic. For example, as I understand it, the compiler ends up trying to prove

for<'state> State: 'state,

which is only true if State: 'static, which is presumably why you added that bound elsewhere.

What you really want is

for<'state where State: 'state> Cb: Callback<'state, State>

But we don't have those directly yet. We kinda-sorta have them when the well-formedness of a type implies that bound, and the type shows up in the HRTB somewhere. Here's a longer article about that.


One of the tricks covered in the article is to put a type with the desired bound into your trait as a default parameter.

pub trait Callback<'state, State, Guard = &'state State> {

Then you can get rid of the explicit bound in your implementation and rely on the implied bound from the elided Guard parameter.

//           vvvvv removed `: 'state`
impl<'state, State, F, Fut> Callback<'state, State> for F
where
    F: Fn(&'state mut State) -> Fut,
    Fut: Future<Output = ()>,

That's enough for the example to compile.[1]


The presence of &'state mut State is providing the implied State: 'state bound in this case.


  1. You can also ditch the State: 'static bound on bbb. ↩ī¸Ž

2 Likes

This clarifies what the issue is about and provides workarounds. Thanks a lot!

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.