Store async functions with pointer arguments in a struct

How do i store an async function pointer with pointer arguments?

i can store a function with owned arguments like so:

use std::{collections::HashMap, future::Future, pin::Pin, sync::Arc};

struct Param {}

type Res = Result<(), ()>;

type Handler = Arc<dyn FnOnce(Param) -> Pin<Box<dyn Future<Output = Res>>>>;

#[derive(Default)]
struct Container {
    map: HashMap<String, Handler>,
}

impl Container {
    pub fn insert<F, Fut>(&mut self, f: F)
    where
        F: FnOnce(Param) -> Fut + 'static,
        Fut: Future<Output = Res> + 'static,
    {
        self.map.insert("demo".into(), Arc::new(|p| Box::pin(f(p))));
    }
}

fn main() {
    let mut c = Container::default();
    c.insert(demo_handler);
}

async fn demo_handler(_: Param) -> Res {
    println!("ciao dall'italia");
    Ok(())
}

but if i try to change the handler signature to accept &Param like so:

type Handler = Arc<dyn FnOnce(&Param) -> Pin<Box<dyn Future<Output = Res>>>>;

i get a mismatch type error on c.insert(demo_handler);

the error:

error[E0308]: mismatched types
  --> src/proto.rs:26:5
   |
26 |     c.insert(demo_handler);
   |     ^^^^^^^^^^^^^^^^^^^^^^ one type is more general than the other
   |
   = note: expected opaque type `impl for<'a> Future<Output = Result<(), ()>>`
              found opaque type `impl Future<Output = Result<(), ()>>`
   = help: consider `await`ing on both `Future`s
   = note: distinct uses of `impl Trait` result in different opaque types
note: the lifetime requirement is introduced here
  --> src/proto.rs:17:30
   |
17 |         F: FnOnce(&Param) -> Fut + 'static,
   |                              ^^^

i tried to introduce lifetimes but i can't get to the bottom of it

The problem is that when you write fn insert<F, Fut>(... you are claiming that there is a single Fut type per handler, which contradicts the idea that Fut can borrow things passed to F (which requires a generic type with a lifetime parameter).

(Another problem is that you can’t actually call a FnOnce held in an Arc, so I’ll continue assuming Fn.)

Therefore, you need to write insert in a way which doesn’t have a Fut parameter. Theoretically, the best solution is the recently-introduced AsyncFn trait, but you currently can’t express a type-erased version of that which returns dyn Future, so it can’t be used in type Handler and (due to a subtle difference in capturing ability) can't be used in install either. Thus, you need a helper trait like the ones provided by async_fn_traits. This version of your program compiles:

use std::{collections::HashMap, future::Future, pin::Pin, sync::Arc};

struct Param {}

type Res = Result<(), ()>;

// The use of 'a here expresses the borrowing relationship you want to allow
type Handler = Arc<dyn for<'a> Fn(&'a Param) -> Pin<Box<dyn Future<Output = Res> + 'a>> + 'static>;

#[derive(Default)]
struct Container {
    map: HashMap<String, Handler>,
}

impl Container {
    pub fn insert<F>(&mut self, f: F)
    where
        // AsyncFn1 lets you write this bound without mentioning "Fut"
        F: for<'a> async_fn_traits::AsyncFn1<&'a Param, Output = Res> + 'static,
    {
        self.map
            .insert("demo".into(), Arc::new(move |p: &Param| Box::pin(f(p))));
    }
}

fn main() {
    let mut c = Container::default();
    c.insert(demo_handler);
}

async fn demo_handler(_: &Param) -> Res {
    println!("ciao dall'italia");
    Ok(())
}
2 Likes