I’ve been looking into how futures/async/await works in rust, and the usage for TLS seems very inelegant to me. For one, it requires host support for thread-local storage, which makes porting to #![no_std]
more difficult than it should be.
- Feature Name:
gen_resume_args
- Start Date: 2019-05-04
- RFC PR: rust-lang/rfcs#0000
- Rust Issue: rust-lang/rust#0000
Summary
Add arguments to generators. This also means that the yield
keyword will “return” those arguments in a tuple.
Motivation
There are two major motivations here.
The primary reason to add generator resume args is to remove the requirement on TLS for async/await.
Currently, the await!
macro is implemented as follows:
macro_rules! r#await {
($e:expr) => { {
let mut pinned = $e;
loop {
if let $crate::task::Poll::Ready(x) =
$crate::future::poll_with_tls_context(unsafe {
$crate::pin::Pin::new_unchecked(&mut pinned)
})
{
break x;
}
// FIXME(cramertj) prior to stabilizing await, we have to ensure that this
// can't be used to create a generator on stable via `|| await!()`.
yield
}
} }
}
Since it has no direct way of retrieving the Context<'_>
from the resumee’s scope, it must grab it from thread-local storage (TLS). While benchmarks have shown that TLS isn’t particularly detrimental to performance, its usage creates confusing control-flow and may prevent some compiler optimizations.
The other motivation is that generators in other languages generally support returning values from a yield
and to have a complete implementation, rust should as well.
Guide-level explanation
This RFC is largely internal. The only user-exposed change would be that generators support resume args.
It’s important to note that if the Resume
associated type is a tuple, the tuple is unpacked for generator entry point. For example, if Resume
is an empty tuple, the generator takes no resume arguments (the resume
method would still take an empty tuple as the args
parameter however).
An example of using this feature in a simple generator:
// `example_gen` implements `Generator<Yield=i32, Return=i32, Resume=(i32, i32)>`.
let mut example_gen = |a, b| {
let (c, d) = yield a + b;
c + d
};
match Pin::new(&mut example_gen).resume((1, 2)) {
GeneratorState::Yielded(3) => {}
_ => unreachable!(),
}
match Pin::new(&mut example_gen).resume((4, 5)) {
GeneratorState::Complete(9) => {}
_ => unreachable!(),
}
Reference-level explaination
This RFC modifies the Generator
trait slightly to make room for arguments:
pub trait Generator {
/// The type of value this generator yields.
///
/// This associated type corresponds to the `yield` expression and the
/// values which are allowed to be returned each time a generator yields.
/// For example an iterator-as-a-generator would likely have this type as
/// `T`, the type being iterated over.
type Yield;
/// The type of value this generator returns.
///
/// This corresponds to the type returned from a generator either with a
/// `return` statement or implicitly as the last expression of a generator
/// literal. For example futures would use this as `Result<T, E>` as it
/// represents a completed future.
type Return;
/// The type of value that this generator can be resumed with.
///
/// This corresponds to the type that the `yield` keyword evaluates to,
/// as well as to the type of the closure-like arguments to the generator.
type Resume;
/// Resumes the execution of this generator.
///
/// This function will resume execution of the generator or start execution
/// if it hasn't already with the supplied resume arguments. This call will
/// return back into the generator's last suspension point, resuming
/// execution from the latest `yield`. The generator will continue executing
/// until it either yields or returns, at which point this function will return.
///
/// # Return value
///
/// The `GeneratorState` enum returned from this function indicates what
/// state the generator is in upon returning. If the `Yielded` variant is
/// returned then the generator has reached a suspension point and a value
/// has been yielded out. Generators in this state are available for
/// resumption at a later point.
///
/// If `Complete` is returned then the generator has completely finished
/// with the value provided. It is invalid for the generator to be resumed
/// again.
///
/// # Panics
///
/// This function may panic if it is called after the `Complete` variant has
/// been returned previously. While generator literals in the language are
/// guaranteed to panic on resuming after `Complete`, this is not guaranteed
/// for all implementations of the `Generator` trait.
fn resume(self: Pin<&mut Self>, args: Self::Resume) -> GeneratorState<Self::Yield, Self::Return>;
}
Using resume args to replace TLS in async/await
Currently, the poll
method for GenFuture
, which is how a Generator
is converted into a Future
temporarily moves the passed context into TLS so that the await!
macro above can retrieve it.
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
// ...
set_task_context(cx, || match gen.resume() {
// ...
})
}
If generators could have resume args, this method could instead be:
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
// ...
match gen.resume(NonNull::from(cx).cast()) {
// ...
}
}
The Resume
associated type for that example would be NonNull<Context<'static>>
.
Unfortunately, await
would require a little compiler-magic to work: the async
function transform would need to keep track of the most recent Context
returned from yield
. The macro, if it were written in user code, would look something like the following:
macro_rules! r#await {
($e:expr) => { {
let mut pinned = $e;
loop {
let cx = /* get most recent context */;
if let $crate::task::Poll::Ready(x) =
unsafe { $crate::pin::Pin::new_unchecked(&mut pinned).poll(cx.cast().as_mut()) }
{
break x;
}
/* set most recent context */ = yield;
}
} }
}
Drawbacks
- This feature adds more complexity to the
Generator
trait and slightly hurts its usability (by forcing the user to pass an empty tuple to theresume
method even when no resume arguments are used. - There are a few places where compiler-magic is required.
Rationale and alternatives
Rationale
- This feature would allow async/await to “just work” with
#![no_std]
and it may be more favourable towards optimizations.
Alternatives
- Just don’t implement generator resume arguments.
- I believe that the positives of implementing it outweigh the negatives.
Prior art
I remember seeing a post somewhere about generator resume arguments, but I can’t find it anymore unfortunately.
I’d like to expand this section.
Unresolved questions
- Are there any issues with lifetimes or borrowing across yield points that I may not have thought of here?
- Is there a better way of representing resume arguments in the
Generator
trait? - Should the initial arguments to the generator be different from the yield result type?
Future possibilities
I’m not sure, would like some suggestions here.