Cannot infer lifetime of a simple async method

I am not able to understand the why the async version of this simple function does compile:

pub fn call_sync(callback: fn(&str) -> Result<(), ()>) {
}

#[test]
fn should_call_sync() {
    call_sync(|value| {
        let value_ref = value;
        Ok(())
    })
}

pub fn call_async<F: Future<Output = Result<(), ()>>>(callback: fn(&str) -> F) {
}

#[test]
fn should_call_async() {
    call_async(|value| async {
        let value_ref = value;
        Ok(())
    })
}

The compilation fails with this error:

error[E0495]: cannot infer an appropriate lifetime due to conflicting requirements
  --> c3p0/tests/c3p0_test_pg_async.rs:82:30
   |
82 |       call_async(|value| async {
   |  ______________________________^
83 | |         let value_ref = value;
84 | |         Ok(())
85 | |     })
   | |_____^

Any idea?

The type of the callback argument doesn't place any constraint on the lifetime of the &str reference that it takes. This means the caller of this closure could pass in a reference with a shorter lifetime than the future that gets returned. For example:

pub fn call_async<F: Future<Output = Result<(), ()>>>(callback: fn(&str) -> F) {
    let future = {
        let s = String::new();
        callback(&s)
    };
    // `s` is destroyed here but `future` is still alive.
}

The provided closure returns a future that borrows the passed-in reference, which would cause a use-after-free in this case.

Fixing this is tricky. You could require the function to take a reference with a specific lifetime specified by the caller of call_async, but this is probably too restrictive to be useful:

pub fn call_async<'a, F>(callback: fn(&'a str) -> F)
where F: Future<Output = Result<(), ()>>

What you probably really want is to say that callback implements something like for<'a> Fn(&'a str) -> (impl Future<...> + 'a). That is, it returns a future with the same lifetime as the string reference passed in. However, I'm not sure if there's a way to write such a bound for an argument with a fn or Fn type, since impl Future isn't allowed there.

In similar situations, the best I've managed is to use dyn Future, for example:

use futures::future::FutureExt;
use std::future::Future;
use std::pin::Pin;

pub fn call_async(callback: fn(&'_ str) -> Pin<Box<dyn Future<Output = Result<(), ()>> + '_>>)
{
}

#[test]
fn should_call_async() {
    call_async(|value| async move {
        let value_ref = value;
        Ok(())
    }.boxed())
}

@mbrubeck
thanks alot, indeed rewriting it the way you propose, it compiles.
Anyway, it is not clear to me why the Box resolves the issue; in fact, if the compiler can resolve the lifetime with the boxed future, then it should be able to resolve it in any case.

With dyn Future, you can write the lifetime of both the &str and the Future both within the callback type, to tell the compiler they are the same. Without it, there’s no way to give the compiler this info (that I know of).

Then this should be the same:

    pub fn call_async<'a, F>(callback: fn(&'a str) -> F)
        where F: 'a + Future<Output = Result<(), ()>> {

    }

isn't it?

That forbids code like this:

pub fn call_async<'a, F>(callback: fn(&'a str) -> F)
        where F: 'a + Future<Output = Result<(), ()>>
{
    let s = String::new();
    callback(&s);
}

The reason is a bit tricky. Rather than taking a reference with any lifetime, chosen by the caller of the callback, the fn here takes a reference with one specific lifetime, chosen by the caller of call_async. And this lifetime may be longer than the body of call_async.

For example, the caller may pass in a callback that only takes &'static str and returns a 'static future.

Compared to my dyn Future version, this is more flexible for the callers of call_async but more restrictive for the implementor. In some use cases this may be good; in others, not.

1 Like

You are right.
I didn't expect this to be so tricky :sweat:

Anyway, even with your approach, I can't make this to compile:


pub fn call_async_fn_once<T, F>(callback: F)
        where F: FnOnce(&'_ str) -> Pin<Box<dyn Future<Output = Result<T, ()>> + '_>>
{
    let s = String::new();
    callback(&s);
}

#[test]
fn should_call_async() {

        let a_var = "".to_owned();
        let an_external_ref = &a_var;

        call_async_fn_once(move |value| async move {
            let value_ref = value;
            println!("{}", an_external_ref); <-- THIS LINE DOES NOT COMPILE
            Ok(())
        }.boxed());
}

I am experiencing much more difficulties than I expected.
Are all of these difficulties related to the fact that the callback function is not a "real" async function so it does not get all the lifetime optimizations of the async/await syntax?

If yes, I hope one day it will be possible to write something like:

pub fn call_async_fn_once<T>(callback: async fn(&'_ str) -> Result<(), ()>)

and just expect it to work.

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.