Rust's async-await is executor-agnostic and runs entirely in userspace. It is just syntax-sugar for Futures as state machines, where "await points" are your states.
An executor (I think this is what you meant by runtime) is nothing special and doesn't need to be tied to OS features at all. You can poll and run futures in a single thread. It's just something that holds and runs futures to completion.
Not very different from an OS scheduler, except it is cooperative instead of preemptive. It's a drop in the ocean of kernel complexities.
I find it interesting that this fulfills some of Dennis Ritchie's goals for what became the STREAMS framework for byte-oriented I/O:
> I decided, with regret, that each processing module could not act as an independent process with its own call record. The numbers seemed against it: on large systems it is necessary to allow for as many as 1000 queues, and I saw no good way to run this many processes without consuming inordinate amounts of storage. As a result, stream server procedures are not allowed to block awaiting data, but instead must return after saving necessary status information explicitly. The contortions required in the code are seldom serious in practice, but the beauty of the scheme would increase if servers could be written as a simple read-write loop in the true coroutine style.
The power of that framework was exactly that it didn't need independent processes. It avoided considerable overhead that way. The cost was that you had to write coroutines by hand, and at a certain point that becomes very difficult to code. With a language that facilitates stackless coroutines, you can get much of the strengths of an architecture like STREAMS while not having to write contorted code.
Ok, I see. I spent a lot of time with .Net VMs, where you cannot simply separate await from the heavy machinery that runs it. I now understand that in a kernel context, you don't need a complex runtime like Tokio. But you still need a way to wake the executor up when hardware does something (like a disk interrupt); but this indeed is not a runtime dependency.
EDIT: just found this source which explains in detail how it works: https://os.phil-opp.com/async-await/
There’s got to be some complexity within the executor implementation though I imagine as I believe you have to suspend and resume execution of the calling thread which can be non-trivial.
Yeah, for example embassy-rs is an RTOS that uses rust async on tiny microcontrollers. You can hook task execution up to a main loop and interrupts pretty easily. (And RTIC is another, more radically simple version which also uses async but just runs everything in interrupt handlers and uses the interrupt priority and nesting capability of most micros to do the scheduling)