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 pin
ing 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.