How to store async function pointers?

How do you store async function pointers? I got this far:

use futures::future::{Future, BoxFuture};

struct Holder {
    func: Box<dyn Fn() -> BoxFuture<'static, u32>>
}

impl Holder {
    fn new<F>(f: fn() -> F) -> Holder where F: Future<Output = u32> + Send + 'static {
        Holder {
            func: Box::new(|| Box::pin(f())),
        }
    }

    async fn run(&self) {
        (self.func)().await;
    }
}

async fn test_func() -> u32 { return 42; }
async fn test_func2() -> u32 { return 33; }

#[tokio::main]
async fn main() {
    let mut v: Vec<Holder> = Vec::new();
    v.push(Holder::new(test_func));
    v.push(Holder::new(test_func2));
}

This gives me lifetime warnings:

error[E0597]: `f` does not live long enough

I'm quite new to rust. Storing a regular function pointer in comparison is trivial. Why has it been made so complicated?

You need to move it into the closure:

func: Box::new(move || Box::pin(f())),

This has nothing to do with regular vs async function pointers.

Thank you. This clears it up. It works now :slightly_smiling_face:

No I meant why can't I simply do:

struct Holder {
    func: async fn() -> u32
}

impl Holder {
    fn new(f: async fn() -> u32) -> Holder {
        Holder {
            func: f
        }
    }

    async fn run(&self) {
        (self.func)().await;
    }
}

but instead have to resort to all this complicated trait specification, boxing, pinning, lifetime speficiation and closures. Seems so weird.

It's because every asynchronous function has a different return type. You must use boxing if you want to combine them into a single return type, and boxing requires memory allocation. Rust does not hide memory allocations behind syntax sugar. This is partly because Rust supports devices without memory allocations, but it also one of the fundamental ideas behind the language.

Async/await is not magic, unfortunately. You have to pick a position on the trade offs.

2 Likes

When you write this:

async fn test_func() -> u32 { return 42; }
async fn test_func2() -> u32 { return 33; }

it compiles to something like this:

fn test_func() -> TestFuncFutureThatReturns42 { TestFuncFutureThatReturns42::new() }
fn test_func2() -> TestFunc2FutureThatReturns33 { TestFunc2FutureThatReturns33::new() }

So you get two different functions returning two different, incompatible types.

For completeness, here's my final working code. I also added a Send trait bound to the function return value so that it can be placed into a (lazy) static:

use futures::future::{Future, BoxFuture};
use tokio::sync::Mutex;

#[macro_use] extern crate lazy_static;

struct AsyncFnPtr<R> {
    func: Box<dyn Fn() -> BoxFuture<'static, R> + Send + 'static>
}

impl <R> AsyncFnPtr<R> {
    fn new<F>(f: fn() -> F) -> AsyncFnPtr<F::Output> where F: Future<Output = R> + Send + 'static {
        AsyncFnPtr {
            func: Box::new(move || Box::pin(f())),
        }
    }
    async fn run(&self) -> R { (self.func)().await }
}

async fn test_func() -> u32 { println!("test_func()"); return 42; }
async fn test_func2() -> u32 { println!("test_func2()"); return 33; }

lazy_static! {
    static ref GLOBAL : Mutex<AsyncFnPtr<u32>> = Mutex::new(AsyncFnPtr::new(test_func));
}

#[tokio::main]
async fn main() {
    let mut v: Vec<AsyncFnPtr<_>> = Vec::new();
    v.push(AsyncFnPtr::new(test_func));
    v.push(AsyncFnPtr::new(test_func2));
    for e in v { e.run().await; }
    GLOBAL.lock().await.run().await;
}

It compiles and produces the expected output. To be honest, I sprinkled "Send" and "'static" randomly into places until it started compiling to get here. Still much to learn.

1 Like