Async callbacks in Rust

Hi there! I am pretty new to async Rust and still struggle with parts of it.
I want to store async callbacks and then execute the later in a detached manner.
My current type for storing them looks like this:

pub(crate) type AsyncCallback<I> = dyn Fn(I) -> Pin<Box<dyn Future<Output = ()>>> + 'static;

#[derive(Clone)]
pub(crate) struct OptionalCallback<I> {
    inner: Arc<Option<Box<AsyncCallback<I>>>>,
}

As mentioned above I am looking for a way to execute them in a detached manner, so I don't want to await the saved future, as the result type of the callback is (), I am also not interested in the result of the Fn. Is this possible without having a reference to a runtime that spawns the future? And is this even a proper way of saving the futures?

Thanks a lot!

Hi :wave:

Yeah, you're on the right track; just know that Arc<…Box< can be flattened down to a simple Arc, as in, you can use Arc to wrap a dyn trait.

Moreover, if you wish to spawn the tasks and have the spin in the background, as in, in a background thread, you'll need a Send bound as well.

Since Pin<Box<dyn 'static + Send + Future<Output = …>>> is a mouthful, you have the BoxFuture<'static, …> handy alias in the ::futures crate:

+ use ::futures::future::{BoxFuture, FutureExt}; // FutureExt provides a `.boxed()` adapter on futures to pin-box-dyn them up.

- pub(crate) type AsyncCallback<I> = dyn Fn(I) -> Pin<Box<dyn Future<Output = ()>>> + 'static;
+ pub(crate) type DynAsyncCallback<I> = dyn 'static + Send + Sync + Fn(I) -> BoxFuture<'static, ()>;
  …

-     inner: Arc<Option<Box<AsyncCallback<I>>>>,
+     inner: Option<Arc<DynAsyncCallback<I>>,
  • I've taken the liberty to add that Dyn to the alias name, since I find it more readable to have the outer dyns be as visible as possible. You could even go the extra mile and use a "trait alias" pattern to have it be even more readable:

    trait_alias! {
        trait AsyncCallback<I> = 'static + Send + Sync + Fn(I) -> BoxFuture<'static, ()>;
    }
    
    …
    
        inner: Option<Arc<dyn AsyncCallback<I>>,
    

    where trait_alias! would basically do (feel free to copy-paste this hand-rolled code):

    pub
    trait AsyncCallback<I>
    where
        Self : 'static + Send + Sync + Fn(I) -> BoxFuture<'static, ()>,
    {}
    impl<I, Self_ : ?Sized> AsyncCallback<I> for Self_
    where
        Self : 'static + Send + Sync + Fn(I) -> BoxFuture<'static, ()>,
    {}
    

You could try to have a helper runtime in, for instance, a lazy_static! (or a OnceCell if you are not fond of macros), that would be in charge of spinning the spawned futures to completion.

Basically the question boils down to how do you want to handle shutdown / shutting down the runtime; the "global" / static approach may not be great for proper shutdown, but it's nice to get things started, and tackle polishing the shutdown later on.

2 Likes

Great!! Thanks a lot, that did the job :wink: Instead of a lazy_static! I introduced a Context type (I am writing a library and more complex static resources might introduce problems). Am I right that it is possible to spawn futures with multiple different runtimes? The user might use a different one for scheduling the other futures. Do I risk a performance hit with introducing a global one just for callbacks?

Well, with too many runtimes you may end up having too many threads spawned; since Futures are supposed to never block / very quickly yield, it's okay for a single runtime to be used to spawn and thus drive many different tasks; it's mainly that by "hiding" a runtime into a global it's rather the drop / shutdown of the runtime which will no longer really be under the caller's control.

Anyways, if you are writing a lib then there may be extra subtleties involved, I'll thus defer to more async-savy people to fill in the relevant details

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.