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:
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).
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.
You are right.
I didn't expect this to be so tricky
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: