Async Fundamentals Stage 1 Explainer

This document describes Stage 1 of the "Async Fundamentals" plans. Our eventual goal is to make it so that, in short, wherever you write fn, you can also write async fn and have things work as transparently as possible. This includes in traits, even special traits like Drop.

Now that I've got you all excited, let me bring you back down to earth: That is our goal, but Stage 1 does not achieve it. However, we do believe that Stage 1 does enable async functions in traits in such a way that everything is possible, though it may not be easy.

The hope is that by having a stage 1 where stable Rust exposes the core functionality needed for async traits, we can get more data about how async traits will be used in practice. That can guide us as we try to resolve some of the (rather sticky) design questions that block making things more ergonomic. =)

Review: how async fn works

When you write an async function in Rust:


#![allow(unused)]
fn main() {
async fn test(x: &u32) -> u32 {
    *x
}
}

This is actually shorthand for a function that returns an impl Future and which captures all of its arguments. The body of this function is an async move block, which simply creates and returns a future:


#![allow(unused)]
fn main() {
fn test<'x>(x: &'x u32) -> impl Future<Output = u32> + 'x {
    async move {
        *x
    }
}
}

The await operation can be performed on any value that implements Future. It causes the async fn to execute the future's "poll" function to see if it's value is ready. If not, then it suspends the current frame until it is re-invoked.

Async fn in traits

Writing an async function in a trait desugars in just the same way as an async function elsewhere. Hence this trait:


#![allow(unused)]
fn main() {
trait HttpRequestProvider {
    async fn request(&mut self, request: Request) -> Response;
}
}

becomes:


#![allow(unused)]
fn main() {
trait HttpRequestProvider {
    fn request<'a>(&'a mut self, request: Request) -> impl Future<Output = Response> + 'a;
}
}

On stable Rust today, impl Trait is not permitted in "return position" within a trait, but we plan to allow it. It will desugar to a function that returns an associated type with the same name as the method itself:


#![allow(unused)]
fn main() {
trait HttpRequestProvider {
    type request<'a>: Future<Output = Response> + 'a;
    fn request<'a>(&'a mut self, request: Request) -> Self::request<'a>;
}
}

Calling t.request(...) thus returns a value of type T::request<'a>. We will reference this request variable later.

Using async fn in traits

When you have async fn in a trait, you can use it with generic types in the usual way. For example, you could write a function that uses the above trait like so:


#![allow(unused)]
fn main() {
async fn process_request(
    mut provider: impl HttpRequestProvider,
    request: Request,
) {
    let response = provider.request(request).await;
    ...
}
}

Naturally you could also write the above example using explicit generics as well (just as with any impl trait):


#![allow(unused)]
fn main() {
async fn process_request<P>(
    mut provider: P,
    request: Request,
) where
    P: HttpRequestProvider,
{
    let response = provider.request(request).await;
    ...
}
}

Naming the future that is returned

Like any function that returns an impl Trait, the return type from an async fn in a trait is anonymous. However, there are times when it can be very useful to be able to talk about it. One particular place where this comes up is when spawning tasks. Consider a function that invokes process_request (above) many times in parallel:


#![allow(unused)]
fn main() {
fn process_all_requests<P>(
    provider: P,
    requests: Vec<RequestData>,
) where
    P: HttpRequestProvider + Clone + Send,
{
    let handles =
        requests
            .into_iter()
            .map(|r| {
                let provider = provider.clone();
                tokio::spawn(async move {
                    process_request(provider, r).await;
                })
            })
            .collect();
    join_all(handles).await;
}
}

As is, compiling this function gives the following error, even though P is marked as Send:

error[E0277]: the future returned by `HttpRequestProvider::request` (invoked on `P`) cannot be sent between threads safely
   --> src/lib.rs:21:17
    |
21  |                 tokio::spawn(async move {
    |                 ^^^^^^^^^^^^ `<P as HttpRequestProvider>::request` cannot be sent between threads safely
    |
note: future is not `Send` as it awaits another future which is not `Send`
   --> src/lib.rs:35:5
    |
35  |     let response = provider.request(request).await?;
    |                    ^^^^^^^^^^^^^^^^^^^^^^^^^ this future is not `Send`
note: required by a bound in `tokio::spawn`
   --> /playground/.cargo/registry/src/github.com-1ecc6299db9ec823/tokio-1.13.0/src/task/spawn.rs:127:21
    |
127 |         T: Future + Send + 'static,
    |                     ^^^^ required by this bound in `tokio::spawn`

The problem here is that, while P: Send, the future returned by request is not necessarily Send (it could, for example, create Rc state and store it in local variables). To fix this, one can add the bound on the P::request type:


#![allow(unused)]
fn main() {
fn process_all_requests<P>(
    provider: P,
    requests: Vec<RequestData>,
)
where
    P: HttpRequestProvider + Clone + Send,
    for<'a> P::request<'a>: Send,
{
    let handles =
        requests
            .into_iter()
            .map(|r| {
                let provider = provider.clone();
                your_runtime::spawn(async move {
                    process_request(provider, r).await;
                })
            })
            .collect();
    your_runtime::join_all(handles).await
}
}

The new bound is here:


#![allow(unused)]
fn main() {
    for<'a> P::request<'a>: Send,
}

The "higher-ranked" requirement says that "no matter what lifetime provider has, the result is Send". Specifying these higher-ranked lifetimes can be a bit cumbersome, and sometimes the GATs accumulate quite a large number of parameters. Therefore, the compiler also supports a shorthand form; if you leave off the GAT parameters, the for<'a> is assumed:


#![allow(unused)]
fn main() {
where
    P: HttpRequestProvider + Clone + Send,
    P::request: Send,
}

In fact, using a nightly feature, we can write this more compactly:


#![allow(unused)]
fn main() {
where
    P: HttpRequestProvider<request: Send> + Clone + Send,
}

When would send bounds be required?

One thing we are not sure of is how often send bounds would be required in practice. The main point where such bounds are required are for generic async functions that will be "spawned". For example, the process_request function itself didn't require any kind of bounds on request:


#![allow(unused)]
fn main() {
async fn process_request(
    mut provider: impl HttpRequestProvider,
    request: Request,
) {
    let response = provider.request(request).await;
    ...
}
}

This is because nothing in this function was required to be Send. The problems only arise when you have a call to spawn or some other function that imposes a Send bound -- and even then, there may be no issue. For example, invoking process_request on a known type doesn't require any sort of where clauses:


#![allow(unused)]
fn main() {
your_runtime::spawn(async move {
    process_request(MyProvider::new(), some_request);
})
}

This works because the compiler is able to see that process_request is being called with a MyProvider, and hence it can determine exactly what future process_request will call when it invokes provider.request, and it can see that this future is Send.

The problems only arise when you invoke spawn on a value of a generic type, like P in our example above. In that case, the compiler doesn't know exactly what future will run, so it needs some bounds on P::request.

Caveat: Async fn in trait are not dyn safe

There is one key limitation in stage 1: traits that contain async fn are not dyn safe. Instead, support for dynamic dispatch is provided via a procedural macro called #[dyner]. In future stages, we expect to support dynamic dispatch "natively", but we still need more experimentation and feedback on the requirements to find the best design.

The #[dyner] macro works as follows. You attach it to a trait:


#![allow(unused)]
fn main() {
#[dyner]
trait HttpRequestProvider {
    async fn request(&mut self, request: Request) -> Response;
}
}

It will generate, alongside the trait, a type DynHttpRequestProvider (Dyn + the name of the trait, in general):


#![allow(unused)]
fn main() {
// Your trait, unchanged:
trait HttpRequestProvider { .. }

// A struct to represent a trait object:
struct DynHttpRequestProvider<'me> { .. }

impl<'me> HttpRequestProvider for DynHttpRequestProvider<'me>
{ .. }
}

This type is a replacement for Box<dyn HttpRequestProvider>. You can create an instance of it by writing DynHttpRequestProvider::from(x), where x is of some type that implements HttpRequestProvider. The DynHttpRequestProvider implements HttpRequestProvider but it forwards each method through using dynamic dispatch.

Example usage

Suppose we had a Context type that was generic over an HttpRequestProvider:


#![allow(unused)]
fn main() {
struct Context<T: HttpRequestProvider> {
   provider: T,
}

async fn process_request(
    provider: impl HttpRequestProvider,
    request: Request,
) -> anyhow::Result<()> {
    let cx = Context {
        provider,
    };
    // ...
}
}

Now suppose that we wanted to remove the T type parameter, so that Context included a trait object. You could do this with dyner by altering the code as follows:


#![allow(unused)]
fn main() {
struct Context<'me> {
   provider: DynHttpRequestProvider<'me>,
}

async fn process_request(
    provider: impl HttpRequestProvider,
    request: Request,
) -> anyhow::Result<()> {
    let cx = Context {
        provider: DynHttpRequestProvider::new(provider),
    };
    // ...
}
}

You might be surprised to see the 'me parameter -- this is needed because we don't whether the provider given to us includes borrowed data. If we knew that it had no references, we might also write:


#![allow(unused)]
fn main() {
struct Context {
   provider: DynHttpRequestProvider<'static>,
}

async fn process_request(
    provider: impl HttpRequestProvider + 'static,
    request: Request,
) -> anyhow::Result<()> {
    let cx = Context {
        provider: DynHttpRequestProvider::from(provider),
    };
    // ...
}
}

Dyner with references

Dyner currently requires an allocator. The DynHttpRequestProvider::new method, for example, allocates a Box internally, and invoking an async function allocates a box to store the resulting future (the #[async_trait] crate does the same; the difference with this approach is that you only use the boxing when you are using dynamic dispatch).

Sometimes though we would like to construct our dynamic objects without using Box. This might be because we only have a &T access or simply because we don't need to allocate and would prefer to avoid the runtime overhead. In cases like that, you can use the from_ref and from_mut methods. from_ref takes a &impl Trait and gives you a Ref<DynTrait>; Ref is a special type that ensures you only use &self methods. from_mut works the same way but for &mut Trait types. Since the provider type never escapes process_request, we could rework our previous example to use mutable references instead of boxing:


#![allow(unused)]
fn main() {
struct Context<'me> {
   provider: &'me mut DynHttpRequestProvider<'me>,
}

async fn process_request(
    mut provider: impl HttpRequestProvider,
    request: Request,
) -> anyhow::Result<()> {
    let cx = Context {
        provider: DynHttpRequestProvider::from_mut(&mut provider),
    };
    // ...
}
}

The rest of the code would work just the same... you can still invoke methods in the usual way (e.g., cx.provider.request(r).await).

Dyner and no-std

Because dyner requires an allocator, it is not currently suitable for no-std settings. We are exploring alternatives here: it's not clear if there is a suitable general purpose mechanism that could be used in settings like that. But part of the beauty of the dyner approach is that, in principle, there might be "dyner-like" crates that can be used in a no-std environment.

Dyner all the things

Dyner is meant to be a "general purpose" replacement for dyn Trait. It expands the scope of what kinds of traits are dyn safe to include traits that use impl Trait in argument- and return-position. For example, the following trait is not dyn safe, but it is dyner safe:


#![allow(unused)]
fn main() {
#[dyner]
trait Print {
    fn print(&self, screen: &mut impl Screen);
    //                           ^^^^^^^^^^^
    //                    impl trait is not dyn safe;
    //                    it is dyner-safe if the trait
    //                    is also procssed with dyner
}

#[dyner]
trait Screen {
    fn output(&mut self, x: usize, y: usize, c: char);
}
}

Dyner for external traits

For dyner to work well, all the traits that you reference from your dyner trait must be processed with dyner. But sometimes those traits may be out of your control! What can you do then? To support this, dyner permits you to apply dyner to an external trait definition. For example, support you had this trait, referencing the Clear trait from cc_traits:


#![allow(unused)]
fn main() {
#[dyner]
trait MyOp {
    fn apply_op(x: &mut impl cc_traits::Clear);
}
}

Applying dyner to MyOp will yield an error that "the type cc_traits::DynClear is not found". This is because the code that dyner generates expects that, for each Foo trait, there will be a DynFoo type available at the same location, and in this case there is not. You can fix this by using the dyner::external_trait! macro, but unfortunately doing so requires that you copy and paste the trait definition:


#![allow(unused)]
fn main() {
mod ext {
    dyner::external_trait! {
        pub trait cc_traits::Clear {
    	    fn clear(&mut self);
        }
    }
}
}

The dyner::external_trait! macro will generate two things:

  • A re-export of cc_traits::Clear
  • a type DynClear

Now we can rewrite our MyOp trait to reference Clear from this new location and everything works:


#![allow(unused)]
fn main() {
mod ext { ... }

#[dyner]
trait MyOp {
    fn apply_op(x: &mut impl ext::Clear);
    //                       ^^^ Changed this.
}
}

The ext::Clear path is just a re-exported version of cc_traits::Clear, so there is no change there, but the type ext::DynClear is well-defined.

Dyner for things in the stdlib

The dyner crate already includes dyner-ified versions of things from the stdlib. However, to take advantage of them, you have to import the traits from dyner::std. For example, instead of referencing std::iter::Iterator, try dyner::std::iter::Iterator:


#![allow(unused)]
fn main() {
use dyner::std::iter::{Iterator, DynIterator};
//  ^^^^^^^                      ^^^^^^^^^^^

#[dyner]
trait WidgetStream {
    fn request(&mut self, r: impl Iterator<Item = String>);
    //                            ^^^^^^^^ the macro will look for DynIterator
}
}

You can use use dyner::std::prelude::* to get all the same traits as are present in the std prelude, along with their Dyn equivalents.

Feedback requested

We'd love to hear what you think. Nothing here is set in stone, quite far from it!

Some questions we are specifically interested in getting answers to:

  • What about this document was confusing to you? We want you to understand, of course, but we also want to improve the way we explain things.
  • How often do you think you would use static vs dynamic dispatch in traits?
  • How are you managing async fn in trait today? Do you think the dyner crate would work for you?
  • How often do you think you use functions that require Send. Do you anticipate any problems from the bound scheme described above?