You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Tracking issue for RFC 2033: Experimentally add coroutines to Rust #43122
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.
I'm using this branch for stream-heavy data processing. By streams I mean iterators with blocking FS calls. Because Generator is missing an Iterator or IntoIterator implementation, you must call your own wrapper. Zoxc kindly provided an example, but it's quite unergonomic. Consider:
Python:
defmy_iter(iter):
forvalueiniter:
yieldvalue
Rust with generators:
fnmy_iter<A,I:Iterator<Item=A>>(iter:I) -> implIterator<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 Generator→Iterator.
Reacted by Jan Hohenheim, Gurinder Singh, O01eg, ~, Matthew, cybai (Haku), Techcable, Ian Douglas Scott, Jaro Fietz, Jesse Wright and 85 more
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 co_await keyword somewhere (or co_yield which is just syntactic sugar for co_await, or co_return). Any occurrence of co_await in the body marks a suspension point where control is returned to the caller.
The object passed to co_await should provide three methods. The first one tells the state machine whether the suspension should be skipped and the coroutine immediately resumed (kind of a degenerate case). The second method is executed before returning control to the caller; it is meant to be used for chaining asynchronous tasks, handling recursive calls, etc. The third method is executed once the coroutine is resumed, e.g. to construct the value returned by the co_await expression. When implementing most generators, these three methods would have trivial bodies, respectively { return false; }, {}, and {}.
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 co_yield or co_return (how yielded values are passed back to the caller is completely controlled by the code).
Reacted by Ayose C., pravic, leehyong, Matt Ickstadt, Benjamin Saunders, Alkis Evlogimenos, Jan Hohenheim, Maxim Ryazanov, Ty Overby, lijinpei and 7 more
Then at the yield point, the generator obviously contains a live Foo<...> for OIBIT and borrow purposes. It also contains a semi-empty Box<(Foo<...>, (), Bar<...>)>, and we have to decide whether we should have that mean that it is to be treated like it contains a Box, just the Foo<...>, or something else.
Reacted by Jan Hohenheim, Daniel, Silvio Clécio and Js2xxx
I might be missing something in the RFC, but based on the definition of resume in the Generator struct, and the given examples, it looks like these generators don't have two way communication. Ideally this language construct would allow us to yield values out and resume values into the generator.
Here's an example of implementing the async/await pattern using coroutines in ES6. The generator yields Promises and the coroutine resumes the generator with the unwrapped value of a Promise each time the Promise completes. There is no way this pattern could have been implemented without the two-way communication.
Rust has a problem here because what's the type of resume? In the ES6 example, the generator always yields out some kind of Promise and is always resumed with the unwrapped value of the Promise. However the contained type changes on each line. In other words, first it yields a Promise<X> and is resumed with an X, and then it yields a Promise<Y> and is resumed with a Y. I can imagine various ways of declaring that this generator first yields a Wrapper<X> and then a Wrapper<Y>, and expects to be resumed with an X and then a Y, but I can't imagine how the compiler will prove that this is what happens when the code runs.
TL;DR: yield value is the less interesting half. It has the potential to be a much more ergonomic way to build an Iterator, but nothing more.
let resumedValue = yield value; is the fun half. It's what turns on the unique flow control possibilities of coroutines.
(Here are some more very interesting ideas for how to use two-way coroutines.)
Reacted by Nelo Mitranim, Jimliang, runiq, Alkis Evlogimenos, Jared Burce, Jon Gjengset, Luna Borowska, ynimous, Igor Żuk, Jeremy W. Sherman and 49 more
Reacted by scottmcm, Igor Żuk, Zakarum, Jason Priest, rbingham, garro95, Jay Sistar, OvermindDL1, Jeswin, Florian Jacob and 16 more
Then at the yield point, the generator obviously contains a live Foo<...> for OIBIT and borrow purposes. It also contains a semi-empty Box<(Foo<...>, (), Bar<...>)>, and we have to decide whether we should have that mean that it is to be treated like it contains a Box, just the Foo<...>, or something else.
I don't know what you mean by "OIBIT". But at the yield point, you do not have a Box<(Foo<...>, (), Bar<...>)> yet. You have a <Box<(Foo<...>, (), Bar<...>)> as Boxed>::Place and a Foo<...> that would need to be dropped if the generator were dropped before resuming.
Looking at the API, it doesn't seem very ergonomic/idiomatic that you have to check if resume returns Yielded or Complete every single iteration. What makes the most sense is two methods:
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 Generator does not exhaust it, stopping it entirely. This makes sense if we view the generator as a channel: resume requests a value from the channel, await_done waits until the channel is closed and returns a final state, and drop simply closes the channel.
Reacted by Jan Hohenheim, Ellen Emilia Anna Zscheile, SasakiSaki, Moja and Alexander Novotny
Reacted by Toni Dietze, Vincent Esche, Kolsky, zohnannor, Christof Sprenger, Victor-Nicolae Savu and Maurits van Riezen
Reacted by Jòan, oxalica, zohnannor and Victor-Nicolae Savu
Has there been any progress regarding the generator -> iterator conversion? If not, is there any active discussion about it somewhere? It would be useful to link it. @Nemikolh and @uHOOCCOOHu, I'm curious about why you disagree with @clarcharr's suggestion. Care to share your thoughts?
I was looking at the current Generator-API and immediately felt uneasy when I read
If Complete is returned then the generator has completely finished with the value provided. It is invalid for the generator to be resumed again.
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:
This function may panic if it is called after the Complete variant has been returned previously. While generator literals in the language are guaranteed to panic on resuming after Complete, this is not guaranteed for all implementations of the Generator trait.
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 resume, which would not be necessary with the above idea.
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.
Reacted by Nelo Mitranim, ash lea, Nemo157, Tobias Denkinger, ActuallyaDeviloper, Jimmy Cuadra, Artyom Pavlov, Squirrel, Peter Leonov, Ashkan Kiani and 21 more
Reacted by Nelo Mitranim, ash lea, Jimmy Cuadra, adiba, Peter Leonov, Jay Sistar, Leonardo Vencovsky, Sergey Shandar, Jakub Beránek, Silvio Clécio and 4 more
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 GeneratorState::Yielded variant rather than the current lifetime scoped Pin reference.
error[E0282]: type annotations needed
--> src/lib.rs:9:36
|
9 | assert_coroutine(#[coroutine] |x| {
| ^
10 | x.count_ones();
| - type must be known at this point
|
help: consider giving this closure parameter an explicit type
|
9 | assert_coroutine(#[coroutine] |x: /* Type */| {
| ++++++++++++
For more information about this error, try `rustc --explain E0282`.
error: could not compile `playground` (lib) due to 1 previous error
I have been programming in Rust for 5 years. I am making an experimental deterministic runtime with Rust. Coroutines/generators are instrumental to my engine. How would one get involved in the creation of the language or attend a design meeting?
Best regards, Sol Midnight
@solmidnight - I'd recommend reaching out in the t-lang Zulip channel. There are some other places to go from there, but that's a good starting point.
@discreaminant2809 - What do you think the type of x should be in that context?
The signature: impl Coroutine<i32, Yield = ()> x is at the resume argument position, which is i32 in this case (the resume argument type is the only generic so the one without = is it).
Reacted by Joseph, Kevin Stephen, Pavel Atanasov and Horu
I'm glad I actually read through the comments, because I now understand the contention over the interface. I would like to further complicate the discussion by proposing yet another distinct trait, but first I want to propose something entirely different.
Construction of Coroutines from unsafe Primitives
My understanding is that coroutines require specific compiler support, which is why they can't be polyfilled in user code. From the eRFC, that compiler support appears to specifically consist of the construction of a state machine which closes over variables and function logic.
I see much debate over the appropriate high-level trait to wrap this in, and the various use cases described in this thread seem to present directly conflicting constraints. But to my understanding, the wrapper trait does not require specific compiler support. So I'm curious if we could avoid blocking on that decision by exposing a(n unsafe) lower-level interface to the compiler-specific part.
Analogy to RawWaker{,VTable}
In particular, I'm struck by the way async and the core::future::Future trait are progressively built up from lower-level primitives, and in particular by the RawWaker object, which directly exposes a type-erased vtable of unsafe function calls (see RawWakerVTable::new().
I'm surprised that the coroutine implementation has only the single high-level ops::Coroutine trait, without any such lower-level unsafe interface that encapsulates the state machine and closures that only the compiler can build. The MIR phase's CoroutineArgs type appears to describe exactly that state machine:
/// Number of variants to reserve in coroutine state. Corresponds to
/// `UNRESUMED` (beginning of a coroutine) and `RETURNED`/`POISONED`
/// (end of a coroutine) states.
constRESERVED_VARIANTS:usize = 3;
constUNRESUMED_NAME:&'staticstr = "Unresumed";
constRETURNED_NAME:&'staticstr = "Returned";
constPOISONED_NAME:&'staticstr = "Panicked";
Proposal: Explicit State Machine with unsafe Function Pointers
I understand that this rustc internal MIR struct cannot just be translated to user code, especially because it maps state machine variants to types, which is a form of dependent typing that Rust cannot currently express in user code. Is it at all possible to adapt the approach of RawWakerVTable, which only works with type-erased function pointers?
It strikes me that the coroutine state machine is a kind of inverse of the RawWakerVTable, in that the coroutine state machine providesunsafe untyped function pointers for the user to invoke. Since we can't yet agree on a single type signature for inputs and outputs, could we instead just return an untyped argument, and let the user do an unsafe cast to the correct type?
I understand that there remains an intricacy about how to handle sized arguments and return values in this model, which I don't yet know how to resolve. RawWakerVTable can force its vtable to operate upon pointers--perhaps the coroutine state machine can require the user to provide Pinned pointers for resume, yield, and return values?
On the generator side, the value you get out of an invocation of yield(_) would be a value that lives until the next yield. For example:
let resumed_value = yield(1);// <-- Legal to use `resumed_value` herelet resumed_value2 = yield(2);// <-- Illegal to use `resumed_value` here, the second `yield` invalidates the first resume value
This seems of questionable value, except for the fact that this design allows you to repeatedly pass some sort of long-lived state/context into a generator for it to operate upon.
I have encountered this pattern repeatedly in the context of buffered streaming, where the user provides an allocated memory region, and the stream process populates it with a series of outputs, stopping when it runs out of space. While the Read trait largely achieves this for byte streams, it fails to represent streams of delimited messages. My current example is getdents() (see rust-lang/libc#4522), which accepts a buffer from the user, and writes a series of directory entries.
I am currently adapting this chunked streaming process for directory entries from getdents() to conform to the Iterator interface, so that it can be used to implement fs::read_dir() in the stdlib. One issue that arises from ReadDir and DirEntry having no lifetime parameters is that they can live past the Iterator::next() invocation that produced them. As per the linked comment:
Being able to communicate with a generator using some extra context where said context has a lifetime bound to the caller of resume makes generators immensely more useful for many applications, particularly those with deferred execution patterns that need to do some useful work on the context between invocations of resume. The only ways to do this today require a lot of unsafe (which can't be hidden from the user) and have extremely questionable soundness.
Because the buffer provided to getdents() has its own lifetime distinct from the directory stream as well as individual DirEntrys, it needs to deploy a workaround, in case the user doesn't drop all the DirEntrys associated with a getdents() chunk before the ReadDir iterator needs to call getdents() again. In this specific case, the getdents() buffer can be cloned, allowing the remaining DirEntrys to continue using the previous chunk without blocking forward progress. But in general, representing the lifetime constraints of the streaming process itself as distinct from the buffer it uses to store its output avoids these kinds of workarounds and enables much more generic library APIs.
Case Studies: Pipelined Processing
One particular use case that demonstrates the value of giving the stream buffer its own lifetime is pipelined processing of stream chunks. In the case of getdents(), chunks of directory entries can be processed across a thread pool, instead of interspersing directory traversal with e.g. regex filtering logic in the same OS thread. This is particularly important for recursive directory traversal such as in the ignore crate, where lower levels of the directory hierarchy cannot be traversed until their parent directory is scanned. Parallel processing of getdents() chunks weakens the serial data dependency inherent in tree traversals.
For another example, I am currently reworking my parallel zip file extraction technique for the third time to compare performance using in-memory queues over OS pipes (zip-rs/zip2#236). Each zip file entry is copied into a worker thread's buffer with pread(), and the decompressed contents are then written out to yet another buffer from another worker thread, and finally written to the output file from yet another worker thread. OS pipes currently perform most of the buffering, and also provide backpressure, but that buffering is kind of mysterious and induces control flow that's not directly visible in my code. For this third and hopefully final rewrite, I'm planning to introduce a coroutine-like trait that looks very much like the proposal below, so that I can express buffered pipelines with lifetimes, and produce backpressure that I can control, measure, and optimize in my own code instead of leaving it up to the OS.
Proposal: More Lifetimes Means More Betterer
The proposed extension is very small, but specifically allows for buffered streaming use cases in which the yielded value contains a reference to the provided buffer argument:
Note too the lifetime relation from self to Return, which enables moving data out of the coroutine when it completes. The lifetime parameters seem excessive, but they can be easily ignored when not needed:
Reacted by Jiahao XU, Kevin Stephen, 1.11e-1f64, Tristan Guichaoua, Ellen Emilia Anna Zscheile, marziply, Bennet Bleßmann, Dmitry Rusakov, zroug, Andrei Cravtov and 23 more
@cosmicexplorer those are some reasonable considerations; it may be worth bringing them up on Zulip (#t-lang) since much more design work happens there than in GitHub issues.
Reacted by Sohang Chopra, Kevin Stephen, Danny McClanahan, cjw and Eyal Kalderon
Activity
alexcrichton commentedon Jul 9, 2017
cc #43076, an initial implementation
mitranim commentedon Jul 25, 2017
Copied from #43076:
I'm using this branch for stream-heavy data processing. By streams I mean iterators with blocking FS calls. Because
Generatoris missing anIteratororIntoIteratorimplementation, you must call your own wrapper. Zoxc kindly provided an example, but it's quite unergonomic. Consider:Python:
Rust with generators:
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
Generator→Iterator.silene commentedon Aug 21, 2017
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
co_awaitkeyword somewhere (orco_yieldwhich is just syntactic sugar forco_await, orco_return). Any occurrence ofco_awaitin the body marks a suspension point where control is returned to the caller.The object passed to
co_awaitshould provide three methods. The first one tells the state machine whether the suspension should be skipped and the coroutine immediately resumed (kind of a degenerate case). The second method is executed before returning control to the caller; it is meant to be used for chaining asynchronous tasks, handling recursive calls, etc. The third method is executed once the coroutine is resumed, e.g. to construct the value returned by theco_awaitexpression. When implementing most generators, these three methods would have trivial bodies, respectively{ return false; },{}, and{}.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
co_yieldorco_return(how yielded values are passed back to the caller is completely controlled by the code).arielb1 commentedon Sep 20, 2017
One subtle point that came up is how we handle the partially-empty boxes created inside of
boxstatements with respect to OIBITs/borrows.For example, if we have something like:
Then at the yield point, the generator obviously contains a live
Foo<...>for OIBIT and borrow purposes. It also contains a semi-emptyBox<(Foo<...>, (), Bar<...>)>, and we have to decide whether we should have that mean that it is to be treated like it contains aBox, just theFoo<...>, or something else.masonk commentedon Jan 7, 2018
I might be missing something in the RFC, but based on the definition of
resumein the Generator struct, and the given examples, it looks like these generators don't have two way communication. Ideally this language construct would allow us toyieldvalues out andresumevalues into the generator.Here's an example of implementing the async/await pattern using coroutines in ES6. The generator yields
Promises and the coroutine resumes the generator with the unwrapped value of aPromiseeach time thePromisecompletes. There is no way this pattern could have been implemented without the two-way communication.Rust has a problem here because what's the type of
resume? In the ES6 example, the generator always yields out some kind ofPromiseand is always resumed with the unwrapped value of thePromise. However the contained type changes on each line. In other words, first it yields aPromise<X>and is resumed with anX, and then it yields aPromise<Y>and is resumed with aY. I can imagine various ways of declaring that this generator first yields aWrapper<X>and then aWrapper<Y>, and expects to be resumed with anXand then aY, but I can't imagine how the compiler will prove that this is what happens when the code runs.TL;DR:
yield valueis the less interesting half. It has the potential to be a much more ergonomic way to build an Iterator, but nothing more.let resumedValue = yield value;is the fun half. It's what turns on the unique flow control possibilities of coroutines.(Here are some more very interesting ideas for how to use two-way coroutines.)
mikeyhew commentedon Jan 19, 2018
@arielb1
I don't know what you mean by "OIBIT". But at the yield point, you do not have a
Box<(Foo<...>, (), Bar<...>)>yet. You have a<Box<(Foo<...>, (), Bar<...>)> as Boxed>::Placeand aFoo<...>that would need to be dropped if the generator were dropped before resuming.clarfonthey commentedon Feb 11, 2018
Looking at the API, it doesn't seem very ergonomic/idiomatic that you have to check if
resumereturnsYieldedorCompleteevery single iteration. What makes the most sense is two methods: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
Generatordoes not exhaust it, stopping it entirely. This makes sense if we view the generator as a channel:resumerequests a value from the channel,await_donewaits until the channel is closed and returns a final state, anddropsimply closes the channel.janhohenheim commentedon Feb 28, 2018
Has there been any progress regarding the
generator -> iteratorconversion? If not, is there any active discussion about it somewhere? It would be useful to link it.@Nemikolh and @uHOOCCOOHu, I'm curious about why you disagree with @clarcharr's suggestion. Care to share your thoughts?
phaux commentedon Mar 2, 2018
https://internals.rust-lang.org/t/pre-rfc-generator-integration-with-for-loops/6625
Flupp commentedon Mar 28, 2018
I was looking at the current
Generator-API and immediately felt uneasy when I readInstead 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:
(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
resume, which would not be necessary with the above idea.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.
Nemo157 commentedon Mar 28, 2018
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
GeneratorState::Yieldedvariant rather than the current lifetime scopedPinreference.185 remaining items
Rollup merge of rust-lang#138435 - eholk:prefix-yield, r=oli-obk
Rollup merge of rust-lang#138435 - eholk:prefix-yield, r=oli-obk
Rollup merge of rust-lang#138435 - eholk:prefix-yield, r=oli-obk
Unrolled build for rust-lang#138435
Rollup merge of rust-lang#138435 - eholk:prefix-yield, r=oli-obk
Rollup merge of rust-lang#138435 - eholk:prefix-yield, r=oli-obk
discreaminant2809 commentedon Apr 15, 2025
There are multiple tracking issues of it so idk where it should appropriately be.
The compiler not being able to infer type in this case is quite wild.
eholk commentedon Apr 15, 2025
@discreaminant2809 - What do you think the type of
xshould be in that context?eholk commentedon Apr 15, 2025
@solmidnight - I'd recommend reaching out in the t-lang Zulip channel. There are some other places to go from there, but that's a good starting point.
discreaminant2809 commentedon Apr 15, 2025
The signature:
impl Coroutine<i32, Yield = ()>xis at the resume argument position, which isi32in this case (the resume argument type is the only generic so the one without=is it).DanielFrantes commentedon Jun 29, 2025
@discreaminant2809 Hello anything that needs to be tested?
cosmicexplorer commentedon Dec 4, 2025
I'm glad I actually read through the comments, because I now understand the contention over the interface. I would like to further complicate the discussion by proposing yet another distinct trait, but first I want to propose something entirely different.
Construction of Coroutines from
unsafePrimitivesMy understanding is that coroutines require specific compiler support, which is why they can't be polyfilled in user code. From the eRFC, that compiler support appears to specifically consist of the construction of a state machine which closes over variables and function logic.
I see much debate over the appropriate high-level trait to wrap this in, and the various use cases described in this thread seem to present directly conflicting constraints. But to my understanding, the wrapper trait does not require specific compiler support. So I'm curious if we could avoid blocking on that decision by exposing a(n
unsafe) lower-level interface to the compiler-specific part.Analogy to
RawWaker{,VTable}In particular, I'm struck by the way
asyncand thecore::future::Futuretrait are progressively built up from lower-level primitives, and in particular by theRawWakerobject, which directly exposes a type-erased vtable ofunsafefunction calls (seeRawWakerVTable::new().I'm surprised that the coroutine implementation has only the single high-level
ops::Coroutinetrait, without any such lower-levelunsafeinterface that encapsulates the state machine and closures that only the compiler can build. The MIR phase'sCoroutineArgstype appears to describe exactly that state machine:rust/compiler/rustc_middle/src/ty/sty.rs
Lines 61 to 76 in 29e035e
Proposal: Explicit State Machine with
unsafeFunction PointersI understand that this rustc internal MIR struct cannot just be translated to user code, especially because it maps state machine variants to types, which is a form of dependent typing that Rust cannot currently express in user code. Is it at all possible to adapt the approach of
RawWakerVTable, which only works with type-erased function pointers?It strikes me that the coroutine state machine is a kind of inverse of the
RawWakerVTable, in that the coroutine state machine providesunsafeuntyped function pointers for the user to invoke. Since we can't yet agree on a single type signature for inputs and outputs, could we instead just return an untyped argument, and let the user do an unsafe cast to the correct type?The 2020 RFC implemented by @jonas-schievink as per #43122 (comment) describes the MIR transformation in a way that is very close to this: https://github.com/semtexzv/rfcs/blob/master/text/0000-unified_coroutines.md#addendum-samples Instead of a generic type-safe trait, could we produce a concrete struct that operates upon pointers?
I understand that there remains an intricacy about how to handle sized arguments and return values in this model, which I don't yet know how to resolve.
RawWakerVTablecan force its vtable to operate upon pointers--perhaps the coroutine state machine can require the user to providePinned pointers for resume, yield, and return values?cosmicexplorer commentedon Dec 4, 2025
Separately, I want to note yet another use case for coroutines that motivates yet another way to arrange type bounds in the
Coroutinetrait (sorry!).Use Case: Buffered Streaming
This extends @zesterer's proposed interface at #43122 (comment):
I have encountered this pattern repeatedly in the context of buffered streaming, where the user provides an allocated memory region, and the stream process populates it with a series of outputs, stopping when it runs out of space. While the
Readtrait largely achieves this for byte streams, it fails to represent streams of delimited messages. My current example isgetdents()(see rust-lang/libc#4522), which accepts a buffer from the user, and writes a series of directory entries.I am currently adapting this chunked streaming process for directory entries from
getdents()to conform to theIteratorinterface, so that it can be used to implementfs::read_dir()in the stdlib. One issue that arises fromReadDirandDirEntryhaving no lifetime parameters is that they can live past theIterator::next()invocation that produced them. As per the linked comment:Because the buffer provided to
getdents()has its own lifetime distinct from the directory stream as well as individualDirEntrys, it needs to deploy a workaround, in case the user doesn't drop all theDirEntrys associated with agetdents()chunk before theReadDiriterator needs to callgetdents()again. In this specific case, thegetdents()buffer can be cloned, allowing the remainingDirEntrys to continue using the previous chunk without blocking forward progress. But in general, representing the lifetime constraints of the streaming process itself as distinct from the buffer it uses to store its output avoids these kinds of workarounds and enables much more generic library APIs.Case Studies: Pipelined Processing
One particular use case that demonstrates the value of giving the stream buffer its own lifetime is pipelined processing of stream chunks. In the case of
getdents(), chunks of directory entries can be processed across a thread pool, instead of interspersing directory traversal with e.g. regex filtering logic in the same OS thread. This is particularly important for recursive directory traversal such as in theignorecrate, where lower levels of the directory hierarchy cannot be traversed until their parent directory is scanned. Parallel processing ofgetdents()chunks weakens the serial data dependency inherent in tree traversals.For another example, I am currently reworking my parallel zip file extraction technique for the third time to compare performance using in-memory queues over OS pipes (zip-rs/zip2#236). Each zip file entry is copied into a worker thread's buffer with
pread(), and the decompressed contents are then written out to yet another buffer from another worker thread, and finally written to the output file from yet another worker thread. OS pipes currently perform most of the buffering, and also provide backpressure, but that buffering is kind of mysterious and induces control flow that's not directly visible in my code. For this third and hopefully final rewrite, I'm planning to introduce a coroutine-like trait that looks very much like the proposal below, so that I can express buffered pipelines with lifetimes, and produce backpressure that I can control, measure, and optimize in my own code instead of leaving it up to the OS.Proposal: More Lifetimes Means More Betterer
The proposed extension is very small, but specifically allows for buffered streaming use cases in which the yielded value contains a reference to the provided buffer argument:
Note too the lifetime relation from
selftoReturn, which enables moving data out of the coroutine when it completes. The lifetime parameters seem excessive, but they can be easily ignored when not needed:tgross35 commentedon Jan 3, 2026
@cosmicexplorer those are some reasonable considerations; it may be worth bringing them up on Zulip (#t-lang) since much more design work happens there than in GitHub issues.
cosmicexplorer commentedon Jan 5, 2026
Thanks so much!