Async Callback Struct
The Structure
mod structure {
use ::futures::future::BoxFuture;
type Cb<E> = Box<dyn Fn(E) -> BoxFuture<'static, ()>>;
pub struct Callback<E> {
pub(super) func: Cb<E>,
}
}
First I create a Cb<E> type alias using BoxFuture from the futures
crate. Before explaining this Cb<E> more first it helps to make sure
we understand exactly what this BoxFuture actually is. It turns out
that it is also a type alias:
type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
This BoxFuture can be read as a dynamically allocated future on the
stack, it must be be pinned to prevent references from moving, and
has a reported lifetime. With this understanding we can now look back
at the Cb<E> type. This is a dynamically allocated function pointer
which receives some owned type E and returns a BoxFuture.
The 'static lifetime is assigned to the BoxFuture because it owns it's
own data and therefore can live the entire life of the application. The
type of () simply means the future returns no output.
The Implementation
mod impls {
use super::Callback;
impl<E> Callback<E> {
pub fn new<F, Ret>(func: F) -> Self
where
F: Fn(E) -> Ret + 'static,
Ret: Future<Output = ()> + Send + 'static,
{
Self {
func: Box::new(move |e| Box::pin(func(e))),
}
}
pub fn call(&self, e: E) -> impl Future<Output = ()> {
(self.func)(e)
}
}
}
This Callback<E> can seem a little scary; however, I think after we
break down the code it will seem more approachable. This new function
has two declared types of F and Ret. The F type must be a function
which returns a type Ret and the function's lifetime must be 'static.
The Ret which the function returns has to implement a Future that
has no output, is Send safe, and has a 'static lifetime.
The Ret type of this signature is very important, it is what allows
us to create our final callback. This future return is exactly what
can be boxed into our Cb<E> from before. Inside of the method body
a lambda is boxed up to create the BoxFuture when called. With that
we now have a callback ready.
The Test
mod test {
use super::Callback;
use mockall::predicate::*;
use mockall::*;
#[cfg_attr(test, mockall::automock)]
trait Foo {
async fn say(&self, string: &str);
}
#[tokio::test]
async fn example() {
let mut foo = MockFoo::new();
foo.expect_say()
.with(predicate::eq("hello"))
.returning(|_| ());
foo.expect_say()
.with(predicate::eq("goodbye"))
.returning(|_| ());
let cb = Callback::new(|foo: MockFoo| async move {
foo.say("hello").await;
foo.say("goodbye").await;
});
cb.call(foo).await;
}
}