Attribute Macro hook

Source
#[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:

  1. static dispatch - generics
  2. 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.