I find the Rust design very simple: a coroutine is just a state machine, i.e. just a C struct. I find this very easy to reason about.
It does not require memory allocations, does not require a run-time, works on embedded targets, etc.
Also, the compiler generates all the boilerplate (the state machine) for you, which I find makes it very easy to use. And well, the compiler ensures memory safety, thread safety, etc. which is the cherry on top.
I'm not sure that answers my question; C++ also uses a state machine.
Most of the post is concerned with the compiler<->library interface - where Rust uses Generator, GeneratorState, Pin, etc. Is there something fundamentally different about the design here?
Rust's compiler/library interface is still much simpler than C++'s. An `async fn` call gives you an anonymous-typed object (much like a lambda) which implements the `Future` trait, which has a single method `poll`.
`Generator` and `GeneratorState` are not exposed or usable. There are no knobs to turn like C++'s `await_ready` or `early_suspend`/`final_suspend`. There is no implicit heap allocation or corresponding elision optimization, and thus no need to map between `coroutine_handle`s and promise objects.
To be fair, C++'s design is a bit more flexible in that it supports passing data in and out of a coroutine. But even if you look at Rust's (unstable work-in-progress) approach to supporting this, the compiler/library interface is still way simpler. The difference is really not related to how much functionality is stabilized, but how scattered and ad-hoc the C++ interface is.
In Rust, the state machines just implement one trait, Future, which has one method: poll.
For a very long time, async/await were just normal Rust macros; there was no compiler<->library interface.
For a year or so, async/await are proper keywords, which provides nicer syntax, and some optimizations that were hard to do with macros (e.g. better layout optimizations for the state machines).
But that's essentially the whole thing.
Looking at safety, flexibility, performance and simplicity, Rust design picks maximum safety, performance, and simplicity, trading off some flexibility in places where it really isn't necessary:
- you can't move a coroutine while its being polled, which is something you probably shouldn't be doing anyways
- you can't control the layout of the coroutine state for auto-generated corotuines; but you can lay them out manually if you need to, for perf (the compiler just won't help you here)
- you need to manually lay out a coroutine and commit to the layout for using them in ABIs
C++ just picks a different design. 100% safety isn't attainable, maximum flexibility is very important, performance is important, but if you need this you have alternatives (callbacks, etc.). I personally just find the API surface of C++ coroutines (futures, promises, tasks, handles, ...) to just be really big.
Rust's interface at the lowest level is just a single method, which in C++ could roughly look like:
optional<T> poll(*waker_t)
When polled it either returns the final result or that it's pending. If it's pending, it keeps the reference to the waker arg, and uses it to notify when it's ready to be polled again.
This design is very composable, because a Future can trivially forward the poll calls to other futures, and the wakers can be plugged into all kinds of existing callback-based APIs.
async/await is a syntax sugar on top of that that auto-generates the poll methods from async function's bodies (where each poll() call advances state to the next .await point).
There's no built-in runtime in the language that does the polling. You make your own event loop, and that gives you freedom to make it simple, or fancy multithreaded, or deterministic for test environments, etc.
It does not require memory allocations, does not require a run-time, works on embedded targets, etc.
Also, the compiler generates all the boilerplate (the state machine) for you, which I find makes it very easy to use. And well, the compiler ensures memory safety, thread safety, etc. which is the cherry on top.