#[hook]
Expand description
A macro that transforms async
functions (and closures) into plain functions, whose return
type is a boxed Future
.
A macro that transforms async
functions (and closures) into plain functions, whose return
type is a boxed Future
.
§Transformation
The macro transforms an async
function, which may look like this:
async fn foo(n: i32) -> i32 {
n + 4
}
into this (some details omitted):
use std::future::Future;
use std::pin::Pin;
fn foo(n: i32) -> Pin<Box<dyn Future<Output = i32>>> {
Box::pin(async move { n + 4 })
}
This transformation also applies to closures, which are converted more simply. For instance, this closure:
async move |x: i32| x * 2 + 4
is changed to:
|x: i32| Box::pin(async move { x * 2 + 4 })
§How references are handled
When a function contains references, their lifetimes are constrained to the returned
Future
. If the above foo
function had &i32
as a parameter, the transformation would be
instead this:
use std::future::Future;
use std::pin::Pin;
fn foo<'fut>(n: &'fut i32) -> Pin<Box<dyn Future<Output = i32> + 'fut>> {
Box::pin(async move { *n + 4 })
}
Explicitly specifying lifetimes (in the parameters or in the return type) or complex usage of
lifetimes (e.g. 'a: 'b
) is not supported.
§Necessity for the macro
The macro performs the transformation to permit the library to store and invoke the functions.
Functions marked with the async
keyword will wrap their return type with the Future
trait, which a state-machine generated by the compiler for the function will implement. This
complicates matters for the library, as Future
is a trait. Depending on a type that
implements a trait is done with two methods in Rust:
- static dispatch - generics
- dynamic dispatch - trait objects
First method is infeasible for the library. The library will contain a plethora of diferent
optional events that will be stored in a structure. And due to the nature of generics,
generic types can only resolve to a single concrete type. If events had a generic type for
their function’s return type, the library would be unable to store the events, as only a single
Future
type from one of the commands would get resolved, preventing other events from
being stored. This issue only presents itself when there’s a need to store different event
functions between different nodes.
Second method involves heap allocations, but is the only working solution. If a trait is
object-safe (which Future
is), the compiler can generate a table of function pointers
(a vtable) that correspond to certain implementations of the trait. This allows to decide which
implementation to use at runtime. Thus, we can use the interface for the Future
trait, and
avoid depending on the underlying value (such as its size). To opt-in to dynamic dispatch,
trait objects must be used with a pointer, like references (&
and &mut
) or Box
. The
latter is what’s used by the macro, as the ownership of the value (the state-machine) must be
given to the caller, the library in this case.
The macro exists to retain the normal syntax of async
functions (and closures), while
granting the user the ability to pass those functions to the library events.
§Notes
If applying the macro on an async
closure, you will need to enable the async_closure
feature. Inputs to procedural macro attributes must be valid Rust code, and async
closures
are not stable yet.