Compiler support: requires rustc 1.65+
genoise
implements generators (a weaker, special case of coroutines) for stable Rust.
Instead of using #![feature(generators, generator_trait)]
and the yield
keyword, "extra-unstable"
features in the Rust compiler, async
/await
syntax is used.
Common use cases are:
- Defining iterators with self-referential states without writing
unsafe
code. - Building state machines like it’s imperative code and leaving the compiler do the rest.
A generator control the flow of three types of data:
- Yield type: Each time a generator suspends execution, a value is handed to the caller.
- Resume type: Each time a generator is resumed, a value is passed in by the caller.
- Output type: When a generator completes, one final value is returned.
Here is an example taking advantage of this:
use genoise::local::{Gn, Co};
use genoise::GnState;
async fn my_generator<'a>(mut co: Co<'_, usize, bool>, input: &'a str) -> &'a str {
let mut trimmed = input;
while co.suspend(trimmed.len()).await {
trimmed = &trimmed[..trimmed.len() - 1];
}
trimmed
}
let argument = "1234567890";
let mut generator = Gn::new(|co| my_generator(co, argument));
// A generator does nothing when created, you need to `.start()` it first
assert!(!generator.started());
assert!(matches!(generator.start(), GnState::Suspended(10)));
// Once started, you can pass in data and resume the execution using `.resume(…)`
assert!(generator.started());
assert!(matches!(generator.resume(true), GnState::Suspended(9)));
assert!(matches!(generator.resume(true), GnState::Suspended(8)));
assert!(matches!(generator.resume(false), GnState::Completed("12345678")));
- Low maintenance:
genoise
is a zero-dependency crate. There is no need to release a new version ofgenoise
solely for transitive dependencies. - Lightweight:
genoise
consists of only a few hundred lines of code and does not rely on procedural macros. - Doesn’t attempt to use reserved keywords: there are no
yield_
orr#yield
in its API. - Concise and simple: there is more example and test code than actual library code.
You can read and grok its source code in just a few minutes.
The most challenging part is
GeneratorFlavor
, which relies on GATs (Generic Associated Types). - Supports continuation arguments and completion values.
- Provides allocation-free generators at user’s option.
- Genericity over the
GeneratorFlavor
: Write once, use everywhere. - No standard library:
genoise
is a no-std crate, and thealloc
feature can be disabled. - Not a concurrency framework or async runtime:
genoise
does not aim to replacetokio
orsmol
, and it does not contain platform-specific code.
- You prefer an API closer to the actual generators available on Rust nightly.
- You are writing performance-sensitive code, and need to use the generator in a tight loop.
genoise
has not been bencharked and will probably slow down your program. Outside of a tight loop the cost is likely negligible.
[local::StackGn ] |
[local::Gn ] |
[sync::StackGn ] |
[sync::Gn ] |
|
---|---|---|---|---|
Allocations per instance | 0 | 2 | 0 | 2 |
Can be returned | No | Yes | No | Yes |
Thread-safe (Sync + Send ) |
No | No | Yes | yes |
"local" here is used like in thread-"local".
Constructing a heap-flavored generator requires two allocations:
- A memory slot to share the yield and resume values
- A memory slot for the
Future
-based state machine
Stack-flavored generators are relying on "local pinning" for the underlying
Future
, and the memory slots for the yield and resume values are standard &T
references
pointing elsewhere, most likely to a local memory region.
As such, in most cases these generators can’t be returned from functions.
However, it’s generally not a problem to transfer the ownership as long as the new owner does
not outlive the memory slots.
TODO: expand on this
Safety blocks are properly documented.
- noop RawWaker
- SyncRefCell (~= kind of spinlock but without spinning)
TODO: elaborate this section
use genoise::local::{StackGn, StackCo, let_gen};
use genoise::GnState;
async fn my_generator<'a>(mut co: StackCo<'_, usize, bool>, input: &'a str) -> &'a str {
let mut trimmed = input;
while co.suspend(trimmed.len()).await {
trimmed = &trimmed[..trimmed.len() - 1];
}
trimmed
}
let argument = "1234567890";
let_gen!(generator, |co| { my_generator(co, argument) }); // <- let_gen! helper macro
assert!(matches!(generator.start(), GnState::Suspended(10)));
assert!(matches!(generator.resume(true), GnState::Suspended(9)));
assert!(matches!(generator.resume(false), GnState::Completed("123456789")));
TODO: elaborate this section
use genoise::{local, sync};
use genoise::{GnState, Co, GeneratorFlavor};
async fn my_generator<'a, F>(mut co: Co<'_, usize, bool, F>, input: &'a str) -> &'a str
where
F: GeneratorFlavor,
{
let mut trimmed = input;
while co.suspend(trimmed.len()).await {
trimmed = &trimmed[..trimmed.len() - 1];
}
trimmed
}
let argument = "1234567890";
{
// Local stack-flavored
local::let_gen!(generator, |co| { my_generator(co, argument) });
assert!(matches!(generator.start(), GnState::Suspended(10)));
assert!(matches!(generator.resume(false), GnState::Completed("1234567890")));
}
{
// Local heap-flavored
let mut generator = local::Gn::new(|co| my_generator(co, argument));
assert!(matches!(generator.start(), GnState::Suspended(10)));
assert!(matches!(generator.resume(false), GnState::Completed("1234567890")));
}
{
// Thread-safe stack-flavored
sync::let_gen!(generator, |co| { my_generator(co, argument) });
std::thread::scope(|s| {
s.spawn(|| {
assert!(matches!(generator.start(), GnState::Suspended(10)));
assert!(matches!(generator.resume(false), GnState::Completed("1234567890")));
});
});
}
{
// Thread-safe heap-flavored
let mut generator = sync::Gn::new(|co| my_generator(co, argument));
let handle = std::thread::spawn(move || {
assert!(matches!(generator.start(), GnState::Suspended(10)));
assert!(matches!(generator.resume(false), GnState::Completed("1234567890")));
});
handle.join().unwrap();
}
A generator which does not take any value when resumed nor returns any value on completion is
also an Iterator
:
use genoise::local::{Gn, Co};
async fn fibonacci(mut co: Co<'_, u32, ()>) {
let mut a = 0;
co.suspend(a).await;
let mut b = 1;
co.suspend(b).await;
while b < 200 {
core::mem::swap(&mut a, &mut b);
b += a;
co.suspend(b).await;
}
}
let generator = Gn::new(fibonacci);
let fibonacci_sequence: Vec<u32> = generator.collect();
assert_eq!(
fibonacci_sequence,
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233]
);
Note that calling size_hint
on a generator will always return
(0, None)
since there is no way to know how many items will be yielded by the generator.
Some generators may never terminate at all (it is advised to not call
collect
on these).
- genawaiter: Stackless generators on stable Rust
- next_gen: Safe generators on stable Rust
- generator: Stackfull Generator Library in Rust
- remit: Rust generators implemented through async/await syntax
- gen-z: Macro-free stream construction through asynchronous generators via an awaitable sender
- corosensei: A fast and safe implementation of stackful coroutines
- may: Rust Stackful Coroutine Library
- mco: Rust Coroutine Library like go
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this crate by you shall be licensed as above, without any additional terms or conditions.