From a3e1400cdda317508c670580e748f7217beb219d Mon Sep 17 00:00:00 2001 From: Amanieu d'Antras Date: Thu, 18 Sep 2025 01:29:30 +0100 Subject: [PATCH 1/3] Rework `ScopedCoroutine` by using a scope in addition to `Pin` Fixes #59 --- benches/coroutine.rs | 12 ++--- src/scoped.rs | 125 ++++++++++++++++++++++++++++++++++--------- 2 files changed, 104 insertions(+), 33 deletions(-) diff --git a/benches/coroutine.rs b/benches/coroutine.rs index 1321aa5c..9bc27af7 100644 --- a/benches/coroutine.rs +++ b/benches/coroutine.rs @@ -1,5 +1,3 @@ -use core::pin::pin; - use corosensei::stack::DefaultStack; use corosensei::{on_stack, Coroutine, ScopedCoroutine}; use criterion::measurement::Measurement; @@ -25,12 +23,12 @@ fn coroutine_call(name: &str, c: &mut Criterion) { c.bench_function(name, move |b| { b.iter(|| { - let identity = pin!(ScopedCoroutine::::with_stack( + ScopedCoroutine::::with_stack( &mut stack, - |_yielder, input| input - )); - identity.resume(black_box(0usize)); - }) + |_yielder, input| input, + |identity| identity.resume(black_box(0usize)), + ); + }); }); } diff --git a/src/scoped.rs b/src/scoped.rs index c672c9cb..114e126f 100644 --- a/src/scoped.rs +++ b/src/scoped.rs @@ -1,9 +1,10 @@ +use crate::stack; #[cfg(feature = "default-stack")] use crate::stack::DefaultStack; -use crate::Yielder; -use crate::{stack, trap::CoroutineTrapHandler}; -use crate::{Coroutine, CoroutineResult}; -use core::pin::Pin; +use crate::trap::CoroutineTrapHandler; +use crate::{Coroutine, CoroutineResult, Yielder}; +use core::marker::PhantomPinned; +use core::pin::{pin, Pin}; /// Variant of [`Coroutine`] which allows the use of non-`'static` lifetimes. /// @@ -12,14 +13,46 @@ use core::pin::Pin; /// - Lifetimes in `Input`, `Yield` and `Return`, /// - Borrowed values in the coroutine body closure. /// -/// However this requires that the coroutine itself be pinned to ensure that it -/// is properly dropped before any of these lifetimes end. The easiest way to -/// do so it to use the [`pin!`] macro when constructing the coroutine +/// To ensure safety, this requires that the coroutine only be accessed within +/// a scope, at the end of which it is dropped. Within the scope, a pinned +/// reference to a `ScopedCoroutine` is provided, which mirrors the API of +/// `Coroutine`. /// -/// [`pin!`]: core::pin::pin +/// # Example +/// +/// ``` +/// use corosensei::{ScopedCoroutine, Yielder}; +/// +/// let mut counter = 0; +/// let body = |yielder: &Yielder, input| { +/// if input == 0 { +/// return; +/// } +/// counter += input; +/// loop { +/// let input = yielder.suspend(()); +/// if input == 0 { +/// break; +/// } +/// counter += input; +/// } +/// }; +/// ScopedCoroutine::new( +/// body, +/// |mut coroutine| { +/// coroutine.as_mut().resume(1); +/// coroutine.as_mut().resume(2); +/// coroutine.as_mut().resume(3); +/// coroutine.as_mut().resume(4); +/// coroutine.as_mut().resume(5); +/// } +/// ); +/// assert_eq!(counter, 15); +/// ``` #[cfg(not(feature = "default-stack"))] pub struct ScopedCoroutine { inner: Coroutine, + _marker: PhantomPinned, } /// Variant of [`Coroutine`] which allows the use of non-`'static` lifetimes. @@ -29,46 +62,86 @@ pub struct ScopedCoroutine { /// - Lifetimes in `Input`, `Yield` and `Return`, /// - Borrowed values in the coroutine body closure. /// -/// However this requires that the coroutine itself be pinned to ensure that it -/// is properly dropped before any of these lifetimes end. The easiest way to -/// do so it to use the [`pin!`] macro when constructing the coroutine +/// To ensure safety, this requires that the coroutine only be accessed within +/// a scope, at the end of which it is dropped. Within the scope, a pinned +/// reference to a `ScopedCoroutine` is provided, which mirrors the API of +/// `Coroutine`. +/// +/// # Example +/// +/// ``` +/// use corosensei::{ScopedCoroutine, Yielder}; /// -/// [`pin!`]: core::pin::pin +/// let mut counter = 0; +/// let body = |yielder: &Yielder, input| { +/// if input == 0 { +/// return; +/// } +/// counter += input; +/// loop { +/// let input = yielder.suspend(()); +/// if input == 0 { +/// break; +/// } +/// counter += input; +/// } +/// }; +/// ScopedCoroutine::new( +/// body, +/// |mut coroutine| { +/// coroutine.as_mut().resume(1); +/// coroutine.as_mut().resume(2); +/// coroutine.as_mut().resume(3); +/// coroutine.as_mut().resume(4); +/// coroutine.as_mut().resume(5); +/// } +/// ); +/// assert_eq!(counter, 15); +/// ``` #[cfg(feature = "default-stack")] pub struct ScopedCoroutine { inner: Coroutine, + _marker: PhantomPinned, } #[cfg(feature = "default-stack")] impl ScopedCoroutine { - /// Creates a new coroutine which will execute `func` on a new stack. + /// Creates a new coroutine which will execute `func` on the given stack. + /// + /// This function creates a coroutine which, when resumed, will execute + /// `body` to completion. When desired the `func` can suspend itself via + /// [`Yielder::suspend`]. /// - /// This function returns a `ScopedCoroutine` which, when resumed, will - /// execute `func` to completion. When desired the `func` can suspend - /// itself via `Yielder::suspend`. - pub fn new(f: F) -> Self + /// The coroutine is passed as a `Pin<&mut ScopedCoroutine>` to `scope` and + /// dropped at the end of the scope. + pub fn new(body: F, scope: S) -> T where F: FnOnce(&Yielder, Input) -> Return, + S: FnOnce(Pin<&mut Self>) -> T, { - Self::with_stack(Default::default(), f) + Self::with_stack(Default::default(), body, scope) } } impl ScopedCoroutine { /// Creates a new coroutine which will execute `func` on the given stack. /// - /// This function returns a coroutine which, when resumed, will execute - /// `func` to completion. When desired the `func` can suspend itself via + /// This function creates a coroutine which, when resumed, will execute + /// `body` to completion. When desired the `func` can suspend itself via /// [`Yielder::suspend`]. - pub fn with_stack(stack: Stack, f: F) -> Self + /// + /// The coroutine is passed as a `Pin<&mut ScopedCoroutine>` to `scope` and + /// dropped at the end of the scope. + pub fn with_stack(stack: Stack, body: F, scope: S) -> T where F: FnOnce(&Yielder, Input) -> Return, + S: FnOnce(Pin<&mut Self>) -> T, { - unsafe { - Self { - inner: Coroutine::with_stack_unchecked(stack, f), - } - } + let coroutine = pin!(Self { + inner: unsafe { Coroutine::with_stack_unchecked(stack, body) }, + _marker: PhantomPinned, + }); + scope(coroutine) } /// Resumes execution of this coroutine. From 997c85612da69c4faddcbd57a0d8ae6142e9276d Mon Sep 17 00:00:00 2001 From: Amanieu d'Antras Date: Sat, 20 Sep 2025 23:24:13 +0100 Subject: [PATCH 2/3] Rework the API for scoped coroutines --- benches/coroutine.rs | 4 +- src/coroutine.rs | 13 ++-- src/lib.rs | 2 +- src/scoped.rs | 143 ++++++++++++++++++++++++------------------- 4 files changed, 91 insertions(+), 71 deletions(-) diff --git a/benches/coroutine.rs b/benches/coroutine.rs index 9bc27af7..1c7d4c8b 100644 --- a/benches/coroutine.rs +++ b/benches/coroutine.rs @@ -23,11 +23,11 @@ fn coroutine_call(name: &str, c: &mut Criterion) { c.bench_function(name, move |b| { b.iter(|| { - ScopedCoroutine::::with_stack( + let coroutine = ScopedCoroutine::::with_stack( &mut stack, |_yielder, input| input, - |identity| identity.resume(black_box(0usize)), ); + coroutine.scope(|mut identity| identity.resume(black_box(0usize))); }); }); } diff --git a/src/coroutine.rs b/src/coroutine.rs index 570e08e5..193b8823 100644 --- a/src/coroutine.rs +++ b/src/coroutine.rs @@ -159,7 +159,7 @@ impl Coroutine { /// This function returns a `Coroutine` which, when resumed, will execute /// `func` to completion. When desired the `func` can suspend itself via /// `Yielder::suspend`. - pub fn new(f: F) -> Self + pub fn new(func: F) -> Self where F: FnOnce(&Yielder, Input) -> Return, F: 'static, @@ -167,7 +167,7 @@ impl Coroutine { Yield: 'static, Return: 'static, { - Self::with_stack(Default::default(), f) + Self::with_stack(Default::default(), func) } } @@ -177,7 +177,7 @@ impl Coroutine(stack: Stack, f: F) -> Self + pub fn with_stack(stack: Stack, func: F) -> Self where F: FnOnce(&Yielder, Input) -> Return, F: 'static, @@ -186,12 +186,12 @@ impl Coroutine(stack: Stack, f: F) -> Self + pub(crate) unsafe fn with_stack_unchecked(stack: Stack, func: F) -> Self where F: FnOnce(&Yielder, Input) -> Return, { @@ -255,7 +255,8 @@ impl Coroutine, f); + let stack_ptr = + arch::init_stack(&stack, coroutine_func::, func); let sanitizer_fiber = stack.sanitizer_fiber(); Self { stack, diff --git a/src/lib.rs b/src/lib.rs index 2a3f81af..89991460 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -250,7 +250,7 @@ pub mod trap; mod util; pub use coroutine::{on_stack, Coroutine, CoroutineResult, Yielder}; -pub use scoped::ScopedCoroutine; +pub use scoped::{ScopedCoroutine, ScopedCoroutineRef}; #[cfg(test)] mod tests; diff --git a/src/scoped.rs b/src/scoped.rs index 114e126f..5eb4084d 100644 --- a/src/scoped.rs +++ b/src/scoped.rs @@ -3,8 +3,6 @@ use crate::stack; use crate::stack::DefaultStack; use crate::trap::CoroutineTrapHandler; use crate::{Coroutine, CoroutineResult, Yielder}; -use core::marker::PhantomPinned; -use core::pin::{pin, Pin}; /// Variant of [`Coroutine`] which allows the use of non-`'static` lifetimes. /// @@ -14,8 +12,8 @@ use core::pin::{pin, Pin}; /// - Borrowed values in the coroutine body closure. /// /// To ensure safety, this requires that the coroutine only be accessed within -/// a scope, at the end of which it is dropped. Within the scope, a pinned -/// reference to a `ScopedCoroutine` is provided, which mirrors the API of +/// a scope, at the end of which it is dropped. Within the scope, a +/// [`ScopedCoroutineRef`] is provided, which mirrors the API of /// `Coroutine`. /// /// # Example @@ -24,7 +22,7 @@ use core::pin::{pin, Pin}; /// use corosensei::{ScopedCoroutine, Yielder}; /// /// let mut counter = 0; -/// let body = |yielder: &Yielder, input| { +/// let coroutine = ScopedCoroutine::new(|yielder: &Yielder, input| { /// if input == 0 { /// return; /// } @@ -36,23 +34,19 @@ use core::pin::{pin, Pin}; /// } /// counter += input; /// } -/// }; -/// ScopedCoroutine::new( -/// body, -/// |mut coroutine| { -/// coroutine.as_mut().resume(1); -/// coroutine.as_mut().resume(2); -/// coroutine.as_mut().resume(3); -/// coroutine.as_mut().resume(4); -/// coroutine.as_mut().resume(5); -/// } -/// ); +/// }); +/// coroutine.scope(|mut coroutine| { +/// coroutine.as_mut().resume(1); +/// coroutine.as_mut().resume(2); +/// coroutine.as_mut().resume(3); +/// coroutine.as_mut().resume(4); +/// coroutine.as_mut().resume(5); +/// }); /// assert_eq!(counter, 15); /// ``` #[cfg(not(feature = "default-stack"))] pub struct ScopedCoroutine { inner: Coroutine, - _marker: PhantomPinned, } /// Variant of [`Coroutine`] which allows the use of non-`'static` lifetimes. @@ -63,17 +57,17 @@ pub struct ScopedCoroutine { /// - Borrowed values in the coroutine body closure. /// /// To ensure safety, this requires that the coroutine only be accessed within -/// a scope, at the end of which it is dropped. Within the scope, a pinned -/// reference to a `ScopedCoroutine` is provided, which mirrors the API of +/// a scope, at the end of which it is dropped. Within the scope, a +/// [`ScopedCoroutineRef`] is provided, which mirrors the API of /// `Coroutine`. /// /// # Example /// /// ``` -/// use corosensei::{ScopedCoroutine, Yielder}; +/// use corosensei::ScopedCoroutine; /// /// let mut counter = 0; -/// let body = |yielder: &Yielder, input| { +/// let coroutine = ScopedCoroutine::new(|yielder, input| { /// if input == 0 { /// return; /// } @@ -85,23 +79,19 @@ pub struct ScopedCoroutine { /// } /// counter += input; /// } -/// }; -/// ScopedCoroutine::new( -/// body, -/// |mut coroutine| { -/// coroutine.as_mut().resume(1); -/// coroutine.as_mut().resume(2); -/// coroutine.as_mut().resume(3); -/// coroutine.as_mut().resume(4); -/// coroutine.as_mut().resume(5); -/// } -/// ); +/// }); +/// coroutine.scope(|mut coroutine| { +/// coroutine.as_mut().resume(1); +/// coroutine.as_mut().resume(2); +/// coroutine.as_mut().resume(3); +/// coroutine.as_mut().resume(4); +/// coroutine.as_mut().resume(5); +/// }); /// assert_eq!(counter, 15); /// ``` #[cfg(feature = "default-stack")] pub struct ScopedCoroutine { inner: Coroutine, - _marker: PhantomPinned, } #[cfg(feature = "default-stack")] @@ -111,15 +101,11 @@ impl ScopedCoroutine { /// This function creates a coroutine which, when resumed, will execute /// `body` to completion. When desired the `func` can suspend itself via /// [`Yielder::suspend`]. - /// - /// The coroutine is passed as a `Pin<&mut ScopedCoroutine>` to `scope` and - /// dropped at the end of the scope. - pub fn new(body: F, scope: S) -> T + pub fn new(func: F) -> Self where F: FnOnce(&Yielder, Input) -> Return, - S: FnOnce(Pin<&mut Self>) -> T, { - Self::with_stack(Default::default(), body, scope) + Self::with_stack(Default::default(), func) } } @@ -129,19 +115,59 @@ impl ScopedCoroutine` to `scope` and - /// dropped at the end of the scope. - pub fn with_stack(stack: Stack, body: F, scope: S) -> T + pub fn with_stack(stack: Stack, func: F) -> Self where F: FnOnce(&Yielder, Input) -> Return, - S: FnOnce(Pin<&mut Self>) -> T, { - let coroutine = pin!(Self { - inner: unsafe { Coroutine::with_stack_unchecked(stack, body) }, - _marker: PhantomPinned, - }); - scope(coroutine) + Self { + inner: unsafe { Coroutine::with_stack_unchecked(stack, func) }, + } + } + + /// Extracts the stack from a coroutine that has finished executing. + /// + /// This allows the stack to be re-used for another coroutine. + pub fn into_stack(self) -> Stack { + self.inner.into_stack() + } + + /// Creates a scope in which the coroutine can be executed. + /// + /// This ensure that any lifetimes used by the coroutine do not escape the + /// scope and that the coroutine is dropped at the end of the scope. + pub fn scope(mut self, f: F) -> T + where + F: FnOnce(ScopedCoroutineRef<'_, Input, Yield, Return, Stack>) -> T, + { + let coroutine = ScopedCoroutineRef { + inner: &mut self.inner, + }; + f(coroutine) + } +} + +/// Reference to a coroutine within a scope created by +/// [`ScopedCoroutine::scope`]. +/// +/// Note that, unlike normal mutable references, Rust will not automatically +/// re-borrow this type so you may need to use the +/// [`ScopedCoroutineRef::as_mut`] method when invoking methods that take a +/// mutable reference. +pub struct ScopedCoroutineRef<'a, Input, Yield, Return, Stack: stack::Stack> { + inner: &'a mut Coroutine, +} + +impl + ScopedCoroutineRef<'_, Input, Yield, Return, Stack> +{ + /// Reborrows the `ScopedCoroutineRef`. + /// + /// This is useful when the reference needs to be used multiple times. + /// + /// This is necessary before Rust only supports automatic reborrowing for + /// plain mutable references. + pub fn as_mut(&mut self) -> ScopedCoroutineRef<'_, Input, Yield, Return, Stack> { + ScopedCoroutineRef { inner: self.inner } } /// Resumes execution of this coroutine. @@ -161,8 +187,8 @@ impl ScopedCoroutine, val: Input) -> CoroutineResult { - unsafe { self.get_unchecked_mut().inner.resume(val) } + pub fn resume(&mut self, val: Input) -> CoroutineResult { + self.inner.resume(val) } /// Returns whether this coroutine has been resumed at least once. @@ -188,8 +214,8 @@ impl ScopedCoroutine) { - unsafe { self.get_unchecked_mut().inner.force_reset() } + pub unsafe fn force_reset(&mut self) { + self.inner.force_reset() } /// Unwinds the coroutine stack, dropping any live objects that are @@ -210,15 +236,8 @@ impl ScopedCoroutine) { - unsafe { self.get_unchecked_mut().inner.force_unwind() } - } - - /// Extracts the stack from a coroutine that has finished executing. - /// - /// This allows the stack to be re-used for another coroutine. - pub fn into_stack(self) -> Stack { - self.inner.into_stack() + pub fn force_unwind(&mut self) { + self.inner.force_unwind() } /// Returns a [`CoroutineTrapHandler`] which can be used to handle traps that From 10485c9c7114b8cbc4a7f88a3c88351ff423410d Mon Sep 17 00:00:00 2001 From: Amanieu d'Antras Date: Mon, 22 Sep 2025 21:02:07 +0100 Subject: [PATCH 3/3] Ensure borrows in the coroutine body outlive the `ScopedCoroutine` --- src/scoped.rs | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/src/scoped.rs b/src/scoped.rs index 5eb4084d..e8bff512 100644 --- a/src/scoped.rs +++ b/src/scoped.rs @@ -1,3 +1,5 @@ +use core::marker::PhantomData; + use crate::stack; #[cfg(feature = "default-stack")] use crate::stack::DefaultStack; @@ -45,8 +47,10 @@ use crate::{Coroutine, CoroutineResult, Yielder}; /// assert_eq!(counter, 15); /// ``` #[cfg(not(feature = "default-stack"))] -pub struct ScopedCoroutine { +pub struct ScopedCoroutine<'a, Input, Yield, Return, Stack: stack::Stack> { inner: Coroutine, + // Ensure any borrows in the coroutine body outlive the ScopedCoroutine. + marker: PhantomData<&'a mut ()>, } /// Variant of [`Coroutine`] which allows the use of non-`'static` lifetimes. @@ -90,18 +94,20 @@ pub struct ScopedCoroutine { /// assert_eq!(counter, 15); /// ``` #[cfg(feature = "default-stack")] -pub struct ScopedCoroutine { +pub struct ScopedCoroutine<'a, Input, Yield, Return, Stack: stack::Stack = DefaultStack> { inner: Coroutine, + // Ensure any borrows in the coroutine body outlive the ScopedCoroutine. + marker: PhantomData<&'a mut ()>, } #[cfg(feature = "default-stack")] -impl ScopedCoroutine { +impl<'a, Input, Yield, Return> ScopedCoroutine<'a, Input, Yield, Return, DefaultStack> { /// Creates a new coroutine which will execute `func` on the given stack. /// /// This function creates a coroutine which, when resumed, will execute /// `body` to completion. When desired the `func` can suspend itself via /// [`Yielder::suspend`]. - pub fn new(func: F) -> Self + pub fn new(func: F) -> Self where F: FnOnce(&Yielder, Input) -> Return, { @@ -109,18 +115,21 @@ impl ScopedCoroutine { } } -impl ScopedCoroutine { +impl<'a, Input, Yield, Return, Stack: stack::Stack> + ScopedCoroutine<'a, Input, Yield, Return, Stack> +{ /// Creates a new coroutine which will execute `func` on the given stack. /// /// This function creates a coroutine which, when resumed, will execute /// `body` to completion. When desired the `func` can suspend itself via /// [`Yielder::suspend`]. - pub fn with_stack(stack: Stack, func: F) -> Self + pub fn with_stack(stack: Stack, func: F) -> Self where F: FnOnce(&Yielder, Input) -> Return, { Self { inner: unsafe { Coroutine::with_stack_unchecked(stack, func) }, + marker: PhantomData, } }