Async/Await VI: 6 weeks of great progress

It’s hard to believe its been almost 6 weeks since the last post I made about async/await in Rust. So much has happened that these last several weeks have flown by. We’ve made exceptionally good progress on solving the problem laid out in the first post of this series, and I want to document it all for everyone.

Future and the pinning API

Last month I wrote an RFC called “Standard library API for immovable types”. The goal of this RFC was to create an API in the standard library for a pointer to a type that would never be moved again, iterating off of the ideas in a previous post in this series. It went through several rounds of design during the RFC process, but a PR has merged which adds three new items to the standard library. The entire API is built around the concept of “pinning.”

A new marker trait called Unpin has been added to std. Like Send and Sync, it is an auto trait: it doesn’t define any methods, and types implement Unpin by default. A type that implements Unpin is safe to move out of the new Pin reference type. Unpin is not implemented for self-referential generators, making it unsafe to move them out of the Pin reference.

This means that if you have a Pin reference to a self-referential generator, you are guaranteed that it cannot be moved. But if you have a Pin reference to anything else, which can be moved, you can basically treat Pin like its just an &mut reference.

In addition to the Pin type and Unpin trait, a type called PinBox has been added. As Pin is to &mut, PinBox is to Box - that way you can have a fully owned self-referential generator that you can move around. Ultimately, a stack of async functions will be ideally be put into a single PinBox, achieving the dream of a single, perfectly sized heap allocation for your async IO “task” stack.

Ultimately, the Future API will change to take self by Pin:

trait Future {
    type Item;
    type Error;
    fn poll(self: Pin<Self>, ctx: &mut task::Context) -> Poll<Self::Item, Self::Error>;
}

This can’t be done until we have custom self types, another RFC I wrote this month. For now, in futures 0.2, we are using a set of shims to make the #[async] macro work.

Impact on async/await code

What all of this means is that the problem of references in async functions is essentially solved. You will be able to write async functions just as if they were normal Rust code, with references and everything. Because this is more ergonomic and requires less reference counting and heap allocations than the alternative, once it becomes possible to do this on stable, I think this will be the most common way to write async code in Rust. The main exception will be concurrency primitives, which will be implemented as reusable library components (just like blocking concurrency primitives are).

In 0.2, you will not be able to call combinators on async functions without first pining them in the heap. This is a limitation because of the shims I mentioned above, before #[async] is stable, you will be able to use combinators directly on the future returned by an async function without allocating it in the heap. There are a few combinators - most importantly select - which currently depend on the ability to move the future around as they’re polling it; these will need to be revisited to find alternative APIs that work well with !Unpin data in the next few months.

Impact on hand-rolled futures

However, because we’re changing the signature of poll, you might fairly be worried that this will have a negative impact on hand-rolled futures. Fortunately, the design of this API is such that the impact should be extremely minimal.

If the hand-rolled future you’re writing is entirely concrete & has no generic parameters - like an implementation of a networking protocol, for example - you are guaranteed that all of your data is Unpin. This means you can, with minimal boilerplate, convert your Pin into an &mut and vice versa.

If the hand-rolled future is a combinator or a kind of middleware, and has a generic F: Future parameter, you will run into a slight issue, because you don’t know that the future you are applied to implements Unpin. One solution is to bound your future F: Future + Unpin, and say that this combinator cannot be applied to a self-referential future. This gives you the same situation as a concrete generator.

But if you do want to be able to take a self-referential inner future, transforming a Pin<Combinator<F>> to Pin<F> is a single line of easy to audit unsafe code:

fn poll(self: Pin<Self>, ctx: &mut task::Context) -> Poll<T, E> {
    let inner: Pin<F> = unsafe { Pin::map(&mut self, |this| &mut this.inner) };
    inner.poll(ctx)
}

As long as the function you pass to map just does a field access like this, and doesn’t move out of the argument it receives, this code is safe. @cramertj plans to write a derive, which generates a safe function that does a pin to pin field access for you so that you don’t even have to write this one line of unsafe code.

Conclusion

The situation is looking extremely good. From a design perspective, we seem to have solved the problem of self-references in async functions. We’ve gotten exactly the code we want, without unsafety, and will only the addition of a small set of library items to the standard library. That means its time to look past the problem of self-referential async functions to the larger picture.

We want async functions on stable; what exactly does that look like, both in terms of feature set and syntax? And what is the most expedient path to achieving that goal? I’ll begin addressing those questions when I continue this series again in April.