While we wait for the experimental coroutine API to land, this crate offers a way of writing
ergonomic coroutines on stable Rust today without resorting to proc macros or other code generation
techniques. In order to make this work we need to commit a few crimes by way of abusing the fact
that Rust's async/await syntax de-sugars into a state machine that implements Future. There
are a couple of (documented) foot guns lying around that you need to be wary of but so long as you
are alright with that you can play around with coroutines today.
Please note that the top level API provided by this crate is not 100% compatible with the current API that is proposed on nightly.
The [Coro] struct represents a running coroutine which can be driven externally from by calling
[resume][Coro::resume]. The quickest way to create a Coro is to build one from an async function
with the appropriate signature:
use simple_coro::{Coro, CoroState, Handle};
let mut coro = Coro::from(async |handle: Handle<usize, bool>| {
let say_hello: bool = handle.yield_value(42).await;
if say_hello {
"Hello, World!"
} else {
"Goodbye cruel world!"
}
});
loop {
coro = match coro.resume() {
CoroState::Pending(c, n) => {
assert_eq!(n, 42);
c.send(true)
}
CoroState::Complete(msg) => {
assert_eq!(msg, "Hello, World!");
break;
}
};
}A Coro can be made from any async function that accepts a [Handle] as its single argument, optionally
returning a value as in the example here (we call this a [CoroFn]). The Handle argument serves several
purposes:
- The only way to obtain a
Handleis through the construction of aCoroso you will not be able to call this function directly. The (ab)use ofasync/awaitsyntax to make this all work means that theFuturereturned by this function can not be awaited under a normal async runtime so we need to ensure that it is only called as part of running theCoro. It also can't await arbitrary futures, only those made available as methods on [Handle] and otherCoroFns. - The two generic types for a
Handle<S, R>specify the values that will be Sent and Received from yield points. - Communication between a running
Coroand the code driving it is possible via methods on aHandle.
Once we have a [Coro] we are responsible for running it by calling the [resume][Coro::resume] and
[send][Coro::send] methods: this crate does not provide any form of runtime. resume takes ownership of
a [Ready] coroutine and gives you back a [CoroState] that either contains the suspended coroutine along with
the value it emitted from its next [yield][Handle::yield_value], or the return value of the coroutine if
it has finished. send takes ownership of a [Pending] coroutine and stores a response to the last yield
for the coroutine to receive when it is resumed.
The [run_sync][Coro::run_sync] method on Coro simplifies writing the loop construct in the example
above if you always want to respond to yields synchronously in the same way, and the [unwrap][CoroState::unwrap]
and [unwrap_pending][CoroState::unwrap_pending] methods on CoroState let you avoid having to match if
you know the state you coming back from a particular yield.
If you want to implement a coroutine directly then you can implement either of the [AsCoro] or [IntoCoro] traits, depending on whether you need to make use of state held within the implementor or not.