-
Notifications
You must be signed in to change notification settings - Fork 12.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Tracking issue for RFC 2033: Experimentally add coroutines to Rust #43122
Comments
cc #43076, an initial implementation |
Copied from #43076: I'm using this branch for stream-heavy data processing. By streams I mean iterators with blocking FS calls. Because Python: def my_iter(iter):
for value in iter:
yield value Rust with generators: fn my_iter<A, I: Iterator<Item=A>>(iter: I) -> impl Iterator<Item=A> {
gen_to_iter(move || {
for value in iter {
yield value;
}
})
} Two extra steps: inner closure + wrapper, and, worse, you have to write the wrapper yourself. We should be able to do better. TL:DR: There should be a built-in solution for |
I was a bit surprised that, during the RFC discussion, links to the C++ world seemed to reference documents dating back from 2015. There have been some progress since then. The latest draft TS for coroutines in C++ is n4680. I guess the content of that draft TS will be discussed again when the complete RFC for Rust's coroutines is worded, so here are some of the salient points. First, it envisions coroutines in a way similar to what this experimental RFC proposes, that is, they are stackless state machines. A function is a coroutine if and only if its body contains the The object passed to Various customization mechanisms are also provided. They tell how to construct the object received by the caller, how to allocate the local variables of the state machine, what to do at the start of the coroutine (e.g. immediately suspend), what to do at the end, what do to in case of an unhandled exception, what to do with the value passed to |
One subtle point that came up is how we handle the partially-empty boxes created inside of For example, if we have something like: fn foo(...) -> Foo<...> {}
fn bar(...) -> Bar<...> {}
box (foo(...), yield, bar(...)) Then at the yield point, the generator obviously contains a live |
I might be missing something in the RFC, but based on the definition of Here's an example of implementing the async/await pattern using coroutines in ES6. The generator yields Rust has a problem here because what's the type of TL;DR:
(Here are some more very interesting ideas for how to use two-way coroutines.) |
I don't know what you mean by "OIBIT". But at the yield point, you do not have a |
Looking at the API, it doesn't seem very ergonomic/idiomatic that you have to check if
Note that this would technically require adding an additional state to closure-based generators which holds the return value, instead of immediately returning it. This would make futures and iterators more ergonomic, though. I also think that explicitly clarifying that dropping a |
Has there been any progress regarding the |
https://internals.rust-lang.org/t/pre-rfc-generator-integration-with-for-loops/6625 |
I was looking at the current
Instead of relying on the programmer to not resume after completion, I would strongly prefer if this was ensured by the compiler. This is easily possible by using slightly different types: pub enum GeneratorState<S, Y, R> {
Yielded(S, Y),
Complete(R),
}
pub trait Generator where Self: std::marker::Sized {
type Yield;
type Return;
fn resume(self) -> GeneratorState<Self, Self::Yield, Self::Return>;
} (see this rust playground for a small usage example) The current API documentation also states:
So you might not immediately notice a resume-after-completion at runtime even when it actually occurs. A panic on resume-after-completion needs additional checks to be performed by In fact, the same idea was already brought up in a different context, however, the focus of this discussion was not on type safety. I assume there are good reasons for the current API. Nevertheless I think it is worth (re)considering the above idea to prevent resume-after-completion. This protects the programmer from a class of mistakes similar to use-after-free, which is already successfully prevented by rust. |
I too would have preferred a similar construction for the compile time safety. Unfortunately, that construction doesn't work with immovable generators, once they have been resumed they can't ever be passed by value. I can't think of a way to encode that constraint in a similar way for pinned references, it seems you need some kind of affine reference that you can pass in and recieve back in the |
A |
Note that |
Question regarding the current experimental implementation: Can the yield and return types of generators (move-like syntax) be annotated? I would like to do the following: use std::hash::Hash;
// Somehow add annotations so that `generator` implements
// `Generator<Yield = Box<Hash>, Return = ()>`.
// As of now, `Box<i32>` gets deduced for the Yield type.
let mut generator = || {
yield Box::new(123i32);
yield Box::new("hello");
}; |
I was hopeful that fn foo() -> impl Generator<Yield = Box<Debug + 'static>> {
|| {
yield Box::new(123i32);
yield Box::new("hello");
}
} it seems the associated types of the return value aren't used to infer the types for the (Note that One terrible way to do this is to place an unreachable yield at the start of the generator declaring its yield and return types, e.g.: let mut generator = || {
if false { yield { return () } as Box<Debug> };
yield Box::new(123i32);
yield Box::new("hello");
}; EDIT: The more I look at |
Yeah, I was hoping as well impl Trait would do the trick, but couldn't get it to work either. Your I guess the only way is to introduce more syntax to annotate the types? |
Will the |
I assumed that it would be changed, and I happened to be looking at the Pin RFC just now and noticed that it agrees, but it is blocked on object safety of arbitrary self types (which is currently an open RFC):
The third point has actually happened already, but it doesn't help much since that required making |
Implement `gen` blocks in the 2024 edition Coroutines tracking issue rust-lang/rust#43122 `gen` block tracking issue rust-lang/rust#117078 This PR implements `gen` blocks that implement `Iterator`. Most of the logic with `async` blocks is shared, and thus I renamed various types that were referring to `async` specifically. An example usage of `gen` blocks is ```rust fn foo() -> impl Iterator<Item = i32> { gen { yield 42; for i in 5..18 { if i.is_even() { continue } yield i * 2; } } } ``` The limitations (to be resolved) of the implementation are listed in the tracking issue
EDIT: It's unnecessary for the resume type to be a GAT, as long as you can use |
I started experimenting with coroutines and it looks like they implement lifetimes incorrectly? https://play.rust-lang.org/?version=nightly&mode=debug&edition=2021&gist=a7a1af3f2e2259aaa7df29350763e27f I would expect it to |
One thing I'd like to see implemented is a way to strongly type yield and return types together. enum Yield {
Foo,
Bar,
}
enum Resume {
FooResponse,
BarResponse,
}
type BadState<C> = CoroutineState<C, Yield, Resume>;
|
We recently did some exploratory work to find out if Rust could support our streaming architecture - and hit several of the roadblocks mentioned in this thread. Thus, I am here to toss in our two cents. At a very high level generators, coroutines and streams should be somewhat complimentary concepts, regardless of language. Especially I mean that a calling scope loops/iterates/traverses until end of some incoming output, seamlessly forwarding underlying yields. foo() Scope
Ultimately the yield of some_processed_content and some_optional_content should end up becoming the yield of upstream functions when the developer says so. One could think of the total call stack as a tree and each yield as a node on such a tree. Then, resume/pause/etc are simply a form of iterating cursor, keeping up with what has been traversed so far. Cheers |
I made a simple test of coroutines for my use case, but there seems to be a problem with lifetimes of values passed into Isn't borrow supposed to end after the call? |
@antonsmetanin it's even worse: https://play.rust-lang.org/?version=nightly&mode=debug&edition=2021&gist=491f8437083e51ec5c0bf3aedf7e5d04; the values passed into are used through all generator invocations. |
There's an existing issue about giving coroutines the ability to borrow the argument to |
Today I had a go at using coroutines to write a high-performance VM that supports reentrant code (effect handlers and the like). The idea is to produce a chain of TCO-ed dynamic calls like threaded code, except with each instruction being a little coroutine instead of just a regular closure. It's so close to working very well and producing near-optimal code (something I was very shocked at), but unfortunately the codegen is being nuked from high by the panic-if-resumed-after-return check, resulting in extra branches and stack spilling. I know that this is way outside the bounds of what most people are probably going to be using coroutines for, but I'm very much wishing for an unsafe version of |
It's sad we can't In Python it's easy to define a "yielding" generator function that actually returns an iterator when called: def yield1to3():
yield 1
yield 2
yield 3 In Rust, #[coroutine] || {
yield 1;
yield 2;
yield 3;
} is not a closure, it's the coroutine (generator) itself, but it can't be used as an iterator without explicit conversion. |
@Sunshine40 You'd want actual generators: #![feature(gen_blocks)]
fn main() {
for v in gen { yield 1; yield 2 } {
println!("{v}");
}
} |
@zesterer for a while I was thinking of writing an article about this for |
Fascinated by the conversation at large but especially between @zesterer and @Kixunil here That said; I wonder if it could be handled by the mere behavior of i.e., is it actually required that Coroutine panic on resume if trying to run after return just yields I am mixing logic from different languages here so, again, perhaps this makes no sense to however Coroutines are being implemented. Glad to see the progress so far from userland though. thx |
That would change the return type, now all of them would be injected with an
It's only possible when the coroutine is pure (no I/O), and the parameters for it are either only used by immutable reference, or copyable/clonable. And it has performance implications: These parameters have to be saved somewhere, and copying/cloning isn't free.
IMO it is possible that rust has a FusedCoroutine, similar to |
@NobodyXu ah yes that makes perfect sense of course. I suppose the hope was that the Rust internals had some magic type wrangling to hide what my mind wants to think of as "forwarding null". I see that would not be the case. Per resetting, that does make sense as well. Does that then imply that, when pure, @zesterer 's UnsafeCoroutine could be both trivial and safe? Per upstream handling of safety, very nice to hear that. |
I think adding a new unsafe method is definitely doable and achievable, though ideally compiler should optimize out the panic landing code. |
The reason for There are tricks to deal with the unsafety though. The code is generated by the compiler, so the implementation doesn't need @NobodyXu the compiler can't optimize it across dynamic dispatch but a special trait can. I'm not suggesting that the cost of dynamic dispatch is small enough for this to matter, just stating the fact. |
I wish
That's true, adding an Or maybe using a |
@NobodyXu note that I've since realized that Also I've found a way to return a zero-sized token instead of
The performance cost is the same - it's one branch and one byte in memory. Less panics probably means smaller binary but hardly anyone cares about it. |
…joboet Add `#[must_use]` attribute to `Coroutine` trait [Coroutines tracking issue](rust-lang#43122) Like closures (`FnOnce`, `AsyncFn`, etc.), coroutines are lazy and do nothing unless called (resumed). Closure traits like `FnOnce` have `#[must_use = "closures are lazy and do nothing unless called"]` to catch likely bugs for users of APIs that produce them. This PR adds such a `#[must_use]` attribute to `trait Coroutine`.
…joboet Add `#[must_use]` attribute to `Coroutine` trait [Coroutines tracking issue](rust-lang#43122) Like closures (`FnOnce`, `AsyncFn`, etc.), coroutines are lazy and do nothing unless called (resumed). Closure traits like `FnOnce` have `#[must_use = "closures are lazy and do nothing unless called"]` to catch likely bugs for users of APIs that produce them. This PR adds such a `#[must_use]` attribute to `trait Coroutine`.
Rollup merge of rust-lang#129034 - henryksloan:coroutine-must-use, r=joboet Add `#[must_use]` attribute to `Coroutine` trait [Coroutines tracking issue](rust-lang#43122) Like closures (`FnOnce`, `AsyncFn`, etc.), coroutines are lazy and do nothing unless called (resumed). Closure traits like `FnOnce` have `#[must_use = "closures are lazy and do nothing unless called"]` to catch likely bugs for users of APIs that produce them. This PR adds such a `#[must_use]` attribute to `trait Coroutine`.
This comment has been minimized.
This comment has been minimized.
Hi @jonatino, Your example program seemingly does not exhibit undefined behavior as written, since the implementation of the one instance of the coroutine carefully re-assigns I do think coroutines can be useful in game development, in similar contexts but with a different implementation. That said, I would suggest that you learn the full capabilities of the language as it exists today, as there are other mechanisms that you can use to accomplish the same goal in a different way without requiring the stabilization of any features. |
RFC.
This is an experimental RFC, which means that we have enough confidence in the overall direction that we're willing to land an early implementation to gain experience. However, a complete RFC will be required before any stabilization.
This issue tracks the initial implementation.
related issues
The text was updated successfully, but these errors were encountered: