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