Reference parameter to async fn does not live long enough

Hi, I'm struggling with the lifetimes of arguments to async functions.

As illustrated below, I would like to pass a reference to a closure that runs asynchronously. The caller f1, itself being async function, is supposed to wait until it gets the "return" of the closure so I assume the reference argument s to the closure callback will live as long as the lifetime of f1.

The question is.. why doesn't it compile? And how may I fix this?
Any advice will be much appreciated.

struct S {}

pub async fn f1<'a, F, Fut>(&self, callback: F) -> ()
where
    F: Fn(&'a S) -> Fut,
    Fut: Future<Output = bool>,
{
        let s = S{}
        callback(&s).await; // "s does not live long enough..." compiler yells
       // do something else...
}
    
// somewhere else
obj.f1(|s: &S| async move {
     println!("{...}", s);
}

Okay, so you can get a bit further like this:

struct S {}

pub async fn f1<F, Fut>(&self, callback: F) -> ()
where
    F: for<'a> Fn(&'a S) -> Fut,
    Fut: Future<Output = bool>,
{
        let s = S{}
        callback(&s).await;
}

However, even with this approach, the returned future type must be the same type no matter which lifetime 'a is. This is a problem because &'a T and &'b T are different types when the lifetimes are different. So you can do this:

// This works.
obj.f1(|s: &S| {
    let value = use_s_in_non_async_way(s);
    async move {
        println!("{}", value);
    }
});

but not this:

// This doesn't work.
obj.f1(|s: &S| {
    async move {
        let value = use_s_in_async_way(s).await;
        println!("{}", value);
    }
});

(here value must be an owned value, and can't borrow from s)

The only way to allow use of s inside the closure is to change your code to use a boxed future.

use futures::future::BoxFuture;

struct S {}

pub async fn f1<F>(&self, callback: F) -> ()
where
    F: for<'a> Fn(&'a S) -> BoxFuture<'s, bool>,
{
        let s = S{}
        callback(&s).await;
}
    
// This is OK!
obj.f1(|s: &S| {
    Box::pin(async move {
        let value = use_s_in_async_way(s).await;
        println!("{}", value);
    })
});
4 Likes

Wow, I would've never been able to imagine using something like Box::pin explicitly in the way you described. Such a new realm of the world, Rust, for me, still.

I ended up tweaking my codebase a little so that instead of passing reference parameters into a function, I instantiate objects within the function itself. (It had to do with how I would spawn multiple threads and tossing the data back and forth) Nonetheless, I really appreciate the thorough walk-through of your explanation and will surely employ this technique next time I face a similar issue.

One thing I wish though is if there's any plan for the language design team to seamlessly integrate this quirky "async-programming" aspect into the grammar. Returning Box::pin(async move {...}) seems quite "un-elegant" to my view.

There is a plan, revolving around async closures, which would be written as async move |…| { … } rather than |…| async move { … }. I don't think it will be happening very soon though, since there are some technicalities to be sorted out beforehand.

Note that you also have the .boxed() adaptor from FutureExt's trait (next to the BoxFuture alias Alice showcased) to write async move { … }.boxed() which has the advantage not to require a line diff around the async move, but just to chain something afterwards.

That being said, when ergonomics are involved, you can always try to improve those through macros, if you so wish:

macro_rules! dyn_async {( $($contents:tt)* ) => (
    ::futures::future::FutureExt::boxed(async move {
        $($contents)*
    })
)}

That way you could write:

obj.f1(|s: &S| dyn_async! {
    let value = ….await;
    …
})
1 Like

I don't think it will be happening very soon though, since there are some technicalities to be sorted out beforehand.

Glad it is being thought over at least.

macro_rules! dyn_async {( $($contents:tt)* ) => (
    ::futures::future::FutureExt::boxed(async move {
        $($contents)*
    })
)}

This meta-programming approach does look clever. I'll keep this gimmick in my book.

Much thanks!

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.