How to use an asynchronous function as a parameter (in enum)?

Hello,

I need your help :smiling_face_with_tear: , how can I use an asynchronous function as a parameter in an enum?

pub enum ActionCallback {
    Tick(fn(&Instance, Tick)),
    ConnectionLost(fn(&Instance)),
    AgentInfo(fn(&Instance, AgentInfo) -> impl Future<XXX>), //<---- how to use an asynchronous function as a parameter 
}

ActionCallBack::AgentInfo(agent_info_callback);

async fn agent_info_callback(instance: &Instance, agent_info: AgentInfo) {
    println!("receive : {:?}", agent_info);
    instance.send_ping().await;
}

fn(&Instance, AgentInfo) -> Box<dyn Future<Output = ()>>
but it gives me this error :

32 |             &ActionCallBack::AgentInfo(agent_info_callback),
   |              ------------------------- ^^^^^^^^^^^^^^^^^^^ expected fn pointer, found fn item
   |              |
   |              arguments to this enum variant are incorrect
   |
   = note: expected fn pointer `for<'a> fn(&'a Instance, AgentInfo) -> Box<(dyn Future<Output = ()> + 'static)>`
                 found fn item `for<'a> fn(&'a Instance, AgentInfo) -> impl Future<Output = ()> {agent_info_callback}`
note: tuple variant defined here

Many thanks in advance :pray:

For now your best bet is probably to type erase the return type, something like

fn(&Instance, AgentInfo) -> Pin<Box<Future<Output = Something> + ' _>>

Assuming the future captures the &'_ Instance, you need trait gymnastics with rough edges or other not-yet-existing features to avoid the type erasure.

2 Likes

Thanks for your suggestion, I've tried but it still doesn't work,

pub enum ActionCallBack {
    AgentInfo(fn(&MetaInstance, AgentInfo) -> Pin<Box<dyn Future<Output = ()> + '_>>),
}
42 |             &ActionCallBack::AgentInfo(agent_info_callback),
   |              --------------------- ^^^ expected fn pointer, found fn item
   |              |
   |              arguments to this enum variant are incorrect
   |
   = note: expected fn pointer `for<'a> fn(&'a Instance, AgentInfo) -> Pin<Box<(dyn Future<Output = ()> + 'a)>>`
                 found fn item `fn(Arc<MetaInstance>, AgentInfo) -> impl Future<Output = ()> {agent_info_callback}`

I think I have no choice. I'll have to use Tokio with spawn() , block_on(), spawn_blocking() to simulate asynchronous behavior, even if it's not clean at all and I'll have to change all the code logic.

I dream of the day when Rust manages 100% async/await :smiling_face_with_tear: I'm in a blocking situation..

I don’t know about the &Instance vs Arc<MetaInstance> difference, but for the return type, you’ll have to adapt the async fn with a little closure boxing its return value.

use core::future::Future;
use core::pin::Pin;

struct Instance;
impl Instance {
    async fn send_ping(&self) {}
}
struct Tick;
#[derive(Debug)]
struct AgentInfo;

enum ActionCallback {
    Tick(fn(&Instance, Tick)),
    ConnectionLost(fn(&Instance)),
    AgentInfo(fn(&Instance, AgentInfo) -> Pin<Box<dyn Future<Output = ()> + '_>>),
}

async fn agent_info_callback(instance: &Instance, agent_info: AgentInfo) {
    println!("receive : {:?}", agent_info);
    instance.send_ping().await;
}

fn main() {
    let _ = ActionCallback::AgentInfo(|x, y| Box::pin(agent_info_callback(x, y)));
}

There’s also convenient abbreviations for this type in the futures crate. Or more minimally (futures reexports from it) in futures-util. There’s also a nice boxed method for converting the future into the pinned box.

Note that the default BoxFuture and .boxed() also add a + Send bound to the trait object which is often what you want, anyways.

use futures::future::BoxFuture;
use futures::FutureExt;

struct Instance;
impl Instance {
    async fn send_ping(&self) {}
}
struct Tick;
#[derive(Debug)]
struct AgentInfo;

enum ActionCallback {
    Tick(fn(&Instance, Tick)),
    ConnectionLost(fn(&Instance)),
    AgentInfo(fn(&Instance, AgentInfo) -> BoxFuture<'_, ()>),
}

async fn agent_info_callback(instance: &Instance, agent_info: AgentInfo) {
    println!("receive : {:?}", agent_info);
    instance.send_ping().await;
}

fn main() {
    let _ = ActionCallback::AgentInfo(|x, y| agent_info_callback(x, y).boxed());
}
2 Likes

I just forgot to rename MetaInstance to Instance, I did that to make the code more readable and shorter.
for the Arc (in Arc<MetaInstance>) I added it to be able to use Instance with tokio::spawn calls, I replaced all the "&self" in the code with "self: Arc".

thanks for this solution, I'll try it with the closures. :pray:

You can also spawn futures that call async fn where the arguments aren’t Arc<…>s yet. So if the function itself doesn’t need it, there’s no need to force the caller to always produce an Arc. On the other hand, if all you’re ever going to use it for is for tokio::spawn situations, maybe that’s not a disadvantage.

If you have a async fn foo(&self) and want to spawn it, as you probably noticed tokio::spawn(x.foo()) doesn’t work directly, giving something like `x` does not live long enough. Assuming x is an Arc<Self> value, the way to fix it at the call site is also by wrapping it, with an async block in this instance, as in tokio::spawn(async move { x.foo().await }). This however then also works if x is just an owned Self value, in case you don’t need it shared, so there’s some additional flexibility, and even with spawn situations you don’t always need the Arc then.

1 Like

I used Arc to avoid the error as in the "test2" example :

async fn test1(instance: Arc<MetaInstance>, agent_info: AgentInfo) {
    println!("receive : {:?}", agent_info);

    TOKIO_RUNTIME.spawn(async move {
        instance.send_ping().await;
    });
}

async fn test2(instance: &MetaInstance, agent_info: AgentInfo) {
    println!("receive : {:?}", agent_info);

    TOKIO_RUNTIME.spawn(async move {
        instance.send_ping().await;
    });
}
async fn test2(instance: &MetaInstance, agent_info: AgentInfo) {
   |                  --------  - let's call the lifetime of this reference `'1`
   |                  |
   |                  `instance` is a reference that is only valid in the function body
...
92 | /     TOKIO_RUNTIME.spawn(async move {
93 | |         instance.send_ping().await;
94 | |     });
   | |      ^
   | |      |
   | |______`instance` escapes the function body here
   |        argument requires that `'1` must outlive `'static`

Sure; on the outside, in the code that wraps the spawning operations, you’ll still need the Arc (or some other way of owned or shared owned access, without lifetimes attached). Maybe I misinterpreted your statement then, I (seemingly incorrectly) assumed the function in the AgentInfo enum was something being spawned, not something that spawns other tasks.

The problem with the spawn-based solution is that I'll have to create a wrapper for each function,
callback -> sync fn -> spawn -> async fn

and if I need a blocking result, I must use block_on(), like this :

lazy_static! {
    static ref TOKIO_RUNTIME: tokio::runtime::Runtime = tokio::runtime::Builder::new_multi_thread()
        .enable_all()
        .build()
        .unwrap();
}

fn test3(instance: Arc<MetaInstance>, agent_info: AgentInfo) {
    println!("receive : {:?}", agent_info);

    TOKIO_RUNTIME.block_on(async move { instance.send_ping().await });
}

but it gives this error at runtime :

Cannot start a runtime from within a runtime. This happens because a function (like `block_on`) attempted to block the current thread while the thread is being used to drive asynchronous tasks.
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

but can be arranged using spawn_blocking :

fn test4(instance: Arc<MetaInstance>, agent_info: AgentInfo) {
    println!("receive : {:?}", agent_info);

    TOKIO_RUNTIME.spawn_blocking(|| {
        TOKIO_RUNTIME.block_on(async move { instance.send_ping().await });
    });

}

Maybe we’re not communicating properly; I was merely pointing out in code like this

use std::sync::Arc;
use std::time::Duration;

struct Foo(i32);

impl Foo {
    async fn foo(&self) {
        println!("{}", self.0);
        tokio::time::sleep(Duration::from_secs(2)).await;
        println!("welcome back!");
    }
}

#[tokio::main]
async fn main() {
    let x = Arc::new(Foo(123));
    let another_x = Arc::clone(&x);
    // let handle = tokio::spawn(x.foo()); // doesn't work
    let handle = tokio::spawn(async move { x.foo().await });
    handle.await.unwrap();
    println!("still having x: {}", another_x.0);
}

that the tokio::spawn(x.foo()); not working can be solved either by rewriting it to async fn foo(self: Arc<Self>) or by using tokio::spawn(async move { x.foo().await }), and explaining the benefits of the latter. Maybe that’s clear to you anyways, I was pointing it out in case it wasn’t :wink:

I’m not sure what problems you’re explaining here.

My remark on that was also supposed to be unrelated to the question of putting an async fn into a struct/enum; I don’t believe that spawning is any sort of desirable solution to hide the future.

I understood what you explained above, I just gave you all the possibilities I used to try and solve the problem.

and at the same time I leave feedback for other people if one day they are in the same situation as me

You're absolutely right, but it's the best I can do at the moment to avoid having to make changes to the whole code.

Ah, alright, so there are remaining problems? Maybe you can present them in a more complete code example / mock-up?

1 Like

I found a solution to simulate await in a sync context without blocking the main thread (block_in_place):

tokio::task::block_in_place(move || {
        Handle::current().block_on(async move { instance.send_ping().await })
});

Thank you @steffahn for the time you gave me :pray:

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.