diff --git a/Cargo.toml b/Cargo.toml index ea277ef6..9003a9bc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -72,6 +72,10 @@ targets = [ name = "coroutine" harness = false +[[bench]] +name = "fiber" +harness = false + # We want debug symbols on release binaries by default. We can always strip them # out later if necessary. [profile.release] diff --git a/benches/fiber.rs b/benches/fiber.rs new file mode 100644 index 00000000..d6394d86 --- /dev/null +++ b/benches/fiber.rs @@ -0,0 +1,75 @@ +use std::convert::identity; + +use corosensei::stack::DefaultStack; +use corosensei::{fiber, fiber_with_stack, Fiber}; +use criterion::measurement::Measurement; +use criterion::{black_box, criterion_group, criterion_main, Criterion}; + +fn fiber_switch(name: &str, c: &mut Criterion) { + struct IdFiber(Fiber<(IdFiber, usize)>); + + let mut identity = IdFiber(fiber::().switch(|fiber| { + let (mut yielder, mut input): (IdFiber, usize) = fiber.switch(identity); + loop { + (yielder, input) = yielder.0.switch(move |fib| (IdFiber(fib), input)) + } + })); + + c.bench_function(name, |b| { + b.iter(|| { + identity.0.switch_in_place( + |yielder| (IdFiber(yielder), black_box(0usize)), + |(f, _)| (f.0, ()), + ); + }) + }); +} + +fn fiber_call(name: &str, c: &mut Criterion) { + // Don't count time spent allocating a stack. + let mut stack = Some(DefaultStack::default()); + + c.bench_function(name, move |b| { + b.iter(|| { + let identity = + fiber_with_stack(move |input, stack| (stack, input), stack.take().unwrap()); + stack = Some(identity.switch(|fib| (fib, black_box(0usize))).0) + }) + }); +} + +fn fiber_switch_time(c: &mut Criterion) { + fiber_switch("fiber_switch_time", c); +} +fn fiber_call_time(c: &mut Criterion) { + fiber_call("fiber_call_time", c); +} + +criterion_group!( + name = time; + config = Criterion::default(); + targets = fiber_switch_time, fiber_call_time +); + +cfg_if::cfg_if! { + if #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] { + use criterion_cycles_per_byte::CyclesPerByte; + + fn fiber_switch_cycles(c: &mut Criterion) { + fiber_switch("fiber_switch_cycles", c); + } + fn fiber_call_cycles(c: &mut Criterion) { + fiber_call("fiber_call_cycles", c); + } + + criterion_group!( + name = cycles; + config = Criterion::default().with_measurement(CyclesPerByte); + targets = fiber_switch_cycles, fiber_call_cycles + ); + + criterion_main!(cycles, time); + } else { + criterion_main!(time); + } +} diff --git a/examples/fiber.rs b/examples/fiber.rs new file mode 100644 index 00000000..34945f18 --- /dev/null +++ b/examples/fiber.rs @@ -0,0 +1,60 @@ +// This example is basically a reworked `example/basic.rs` + +use std::convert::identity; + +use corosensei::{fiber, Fiber}; + +struct Arg { + exec: Fiber>, + i: i32, +} + +struct Yield { + exec: Fiber, + i: i32, +} + +impl Arg { + fn new(exec: Fiber>, input: i32) -> Self { + Self { exec, i: input } + } +} + +fn main() { + println!("[main] creating fiber"); + + let exec = fiber(); + + let mut exec = exec.switch(|exec| { + let Arg { mut exec, i } = exec.switch(identity); + println!("[fiber] fiber started with input {}", i); + for mut i in 0..5 { + println!("[fiber] yielding {}", i); + Arg { exec, i } = exec.switch(move |exec| Some(Yield { exec, i })); + println!("[fiber] got {} from parent", i) + } + println!("[fiber] exiting fiber"); + (exec, None) + }); + + let mut counter = 100; + loop { + println!("[main] resuming fiber with argument {}", counter); + match exec.switch(move |exec| Arg::new(exec, counter)) { + Some(Yield { exec: new, i }) => { + exec = new; + println!("[main] got {:?} from fiber", i) + } + None => break, + } + + counter += 1; + } + + println!("[main] exiting"); +} + +#[test] +fn fiber() { + main() +} diff --git a/src/arch/aarch64.rs b/src/arch/aarch64.rs index 7a8662ee..c2e03144 100644 --- a/src/arch/aarch64.rs +++ b/src/arch/aarch64.rs @@ -71,12 +71,13 @@ //! ``` use core::arch::{asm, global_asm}; +use core::{ffi, ptr}; use super::{allocate_obj_on_stack, push}; use crate::stack::{Stack, StackPointer}; use crate::unwind::{ asm_may_unwind_root, asm_may_unwind_yield, cfi_reset_args_size_root, cfi_reset_args_size_yield, - InitialFunc, StackCallFunc, TrapHandler, + InitialFunc, StackCallFunc, SwitchFiberFunc, TrapHandler, }; use crate::util::EncodedValue; @@ -185,6 +186,55 @@ extern "C" { fn stack_call_trampoline(arg: *mut u8, sp: StackPointer, f: StackCallFunc); } +#[inline] +pub unsafe fn fiber_init_stack(stack_base: StackPointer) -> StackPointer { + unsafe extern "C" fn entry( + sp: StackPointer, + arg: EncodedValue, + // There could be no output buffer on a fresh fiber. + // Assuming first `f` function never returns on this fiber. + _obj: *mut ffi::c_void, + f: SwitchFiberFunc, + ) { + f(sp, arg, ptr::null_mut()) + } + + let mut sp = stack_base.get(); + + // Put the entry function pointer + push(&mut sp, Some(entry as StackWord)); + + StackPointer::new_unchecked(sp) +} + +#[inline] +pub unsafe fn fiber_switch( + stack_ptr: StackPointer, + mut arg: EncodedValue, + ret: *mut ffi::c_void, + mut f: SwitchFiberFunc, +) { + let mut sp = stack_ptr.get(); + asm!( + "stp x19, x29, [sp, #-16]!", + "adr lr, 2f", + "str lr, [sp, #-8]!", + "ldr lr, [x0], #8", + "mov x9, sp", + "mov sp, x0", + "mov x0, x9", + "ret", + "2:", + "ldp x19, x29, [sp], #16", + inlateout("x0") sp, inlateout("x1") arg, inlateout("x3") f, + lateout("x20") _, lateout("x21") _, lateout("x22") _, lateout("x23") _, + lateout("x24") _, lateout("x25") _, lateout("x26") _, lateout("x27") _, + lateout("x28") _, + clobber_abi("C"), + ); + f(StackPointer::new_unchecked(sp), arg, ret) +} + #[inline] pub unsafe fn init_stack(stack: &impl Stack, func: InitialFunc, obj: T) -> StackPointer { let mut sp = stack.base().get(); diff --git a/src/arch/riscv.rs b/src/arch/riscv.rs index 153937a0..e35c1533 100644 --- a/src/arch/riscv.rs +++ b/src/arch/riscv.rs @@ -71,12 +71,13 @@ //! ``` use core::arch::{asm, global_asm}; +use core::{ffi, ptr}; use super::{allocate_obj_on_stack, push}; use crate::stack::{Stack, StackPointer}; use crate::unwind::{ asm_may_unwind_root, asm_may_unwind_yield, cfi_reset_args_size_root, cfi_reset_args_size_yield, - InitialFunc, StackCallFunc, TrapHandler, + InitialFunc, StackCallFunc, SwitchFiberFunc, TrapHandler, }; use crate::util::EncodedValue; @@ -255,6 +256,63 @@ extern "C" { fn stack_call_trampoline(arg: *mut u8, sp: StackPointer, f: StackCallFunc); } +#[inline] +pub unsafe fn fiber_init_stack(stack_base: StackPointer) -> StackPointer { + unsafe extern "C" fn entry( + sp: StackPointer, + arg: EncodedValue, + // There could be no output buffer on a fresh fiber. + // Assuming first `f` function never returns on this fiber. + _obj: *mut ffi::c_void, + f: SwitchFiberFunc, + ) { + f(sp, arg, ptr::null_mut()) + } + + let mut sp = stack_base.get(); + + // Put the entry function pointer + push(&mut sp, Some(entry as StackWord)); + + StackPointer::new_unchecked(sp) +} + +#[inline] +pub unsafe fn fiber_switch( + stack_ptr: StackPointer, + mut arg: EncodedValue, + ret: *mut ffi::c_void, + mut f: SwitchFiberFunc, +) { + let mut sp = stack_ptr.get(); + asm!( + addi!("sp", "sp", -3), + s!("s0", 2, "sp"), + s!("s1", 1, "sp"), + "lla ra, 2f", + s!("ra", 0, "sp"), + l!("ra", 0, "a0"), + "mv t1, sp", + addi!("sp", "a0", 1), + "mv a0, t1", + "ret", + "2:", + l!("s1", 0, "sp"), + l!("s0", 1, "sp"), + addi!("sp", "sp", 2), + + inlateout("a0") sp, inlateout("a1") arg, inlateout("a3") f, + lateout("s2") _, lateout("s3") _, lateout("s4") _, lateout("s5") _, + lateout("s6") _, lateout("s7") _, lateout("s8") _, lateout("s9") _, + lateout("s10") _, lateout("s11") _, + lateout("fs0") _, lateout("fs1") _, lateout("fs2") _, lateout("fs3") _, + lateout("fs4") _, lateout("fs5") _, lateout("fs6") _, lateout("fs7") _, + lateout("fs8") _, lateout("fs9") _, lateout("fs10") _, lateout("fs11") _, + clobber_abi("C"), + ); + f(StackPointer::new_unchecked(sp), arg, ret) +} + #[inline] pub unsafe fn init_stack(stack: &impl Stack, func: InitialFunc, obj: T) -> StackPointer { let mut sp = stack.base().get(); diff --git a/src/arch/x86_64.rs b/src/arch/x86_64.rs index b84a7c5c..67ff37f7 100644 --- a/src/arch/x86_64.rs +++ b/src/arch/x86_64.rs @@ -99,12 +99,13 @@ //! ``` use core::arch::{asm, global_asm}; +use core::{ffi, ptr}; use super::{allocate_obj_on_stack, push}; use crate::stack::{Stack, StackPointer}; use crate::unwind::{ asm_may_unwind_root, asm_may_unwind_yield, cfi_reset_args_size_root, cfi_reset_args_size_yield, - InitialFunc, StackCallFunc, TrapHandler, + InitialFunc, StackCallFunc, SwitchFiberFunc, TrapHandler, }; use crate::util::EncodedValue; @@ -283,6 +284,61 @@ extern "C" { fn stack_call_trampoline(arg: *mut u8, sp: StackPointer, f: StackCallFunc); } +#[inline] +pub unsafe fn fiber_init_stack(stack_base: StackPointer) -> StackPointer { + unsafe extern "sysv64" fn entry( + sp: StackPointer, + arg: EncodedValue, + // There could be no output buffer on a fresh fiber. + // Assuming first `f` function never returns on this fiber. + _obj: *mut ffi::c_void, + f: SwitchFiberFunc, + ) { + f(sp, arg, ptr::null_mut()) + } + + let mut sp = stack_base.get(); + + // Zero initialize return pointer + push(&mut sp, Some(0)); + // Put the entry function pointer + push(&mut sp, Some(entry as StackWord)); + + StackPointer::new_unchecked(sp) +} + +#[inline] +pub unsafe fn fiber_switch( + stack_ptr: StackPointer, + mut arg: EncodedValue, + ret: *mut ffi::c_void, + mut f: SwitchFiberFunc, +) { + let mut sp = stack_ptr.get(); + // TODO: edit the comment below + // The function `fiber_switch` upon its call saves `ret: rdx` output pointer argument and + // callee-saved registers (defined by to the AMD64 System-V ABI) to the stack. So it switches + // stacks, restores callee-saved registers and an output pointer argument, then finally it jumps + // to the input function pointer in rcx. + asm!( + "lea rax, [rip + 2f]", + "push rbx", + "push rbp", + "push rax", + "mov rax, rsp", + "mov rsp, rdi", + "mov rdi, rax", + "ret", + "2:", + "pop rbp", + "pop rbx", + inout("rdi") sp, inout("rsi") arg, inout("rcx") f, + lateout("r12") _, lateout("r13") _, lateout("r14") _, lateout("r15") _, + clobber_abi("sysv64"), + ); + f(StackPointer::new_unchecked(sp), arg, ret) +} + /// Sets up the initial state on a stack so that the given function is /// executed on the first switch to this stack. /// diff --git a/src/fiber.rs b/src/fiber.rs new file mode 100644 index 00000000..09a0dc4c --- /dev/null +++ b/src/fiber.rs @@ -0,0 +1,226 @@ +use core::{convert::Infallible, ffi, marker::PhantomData, mem, ptr}; + +use crate::{ + arch, + stack::{self, DefaultStack, StackPointer}, + util::{decode_val, encode_val, EncodedValue}, +}; + +struct SwitchPayload { + function: F, +} + +/// A suspended execution +/// +/// This type represents a lightweight thread of execution or a [fiber] in its suspended state. +/// Every fiber logically owns a [program stack] it uses during the execution. +/// However dropping a `Fiber` object simply leaks. +/// The only way to free its resources such as its program stack is for call of the closure it was initialized with to return. +/// As such any running execution context can be converted into a `Fiber` (see [`Fiber::switch`]), even if its the [main function]. +/// +/// These restrictions might seem limiting due to this design trying to be minimalistic so that any reasonable functionallity can be added after using safe Rust. +/// +/// [fiber]: https://en.wikipedia.org/wiki/Fiber_(computer_science) +/// [program stack]: https://en.wikipedia.org/wiki/Call_stack +/// [main function]: https://en.wikipedia.org/wiki/Entry_point +pub struct Fiber { + sp: StackPointer, + _arg: PhantomData Yield)>, + _thread_unsafe: PhantomData<*mut ()>, + _unwind_unsafe: PhantomData<&'static mut ()>, +} + +/// Create a new `Fiber`. +/// +/// Create a fiber with default-initialized [`DefaultStack`]. +/// After switching to it, it immediately switches back after [`intermediate`](`Fiber::switch`) returns another fiber to switch to. +/// `Return` type can be used to send objects between fibers. +/// +/// To specify a custom stack and to recover it after fiber's destruction use [`fiber_with_stack`]. +/// +/// # Unwinding +/// +/// In case fiber catches a panic, it aborts current execution (using double-panic). +pub fn fiber() -> Fiber<(Fiber, Return)> +where + Return: 'static, +{ + fiber_with_stack( + |arg, stack| { + drop(stack); + arg + }, + DefaultStack::default(), + ) +} + +/// Create a new `Fiber` with custom `stack`. +/// +/// Similar to [`fiber`] except for the explicit stack control. +/// After switching to it, it immediately switches back after [`intermediate`](`Fiber::switch`) returns another fiber to switch to. +/// Then `after_exit` is called, from which you can recover and save used stack by storing it into the `Return` object. +/// `Arg` and `Return` types can be used to send objects between fibers, including the used stack. +/// +/// If you don't care about managing stacks, see [`fiber`]. +/// +/// # Unwinding +/// +/// In case fiber catches a panic, it aborts current execution (using double-panic). +pub fn fiber_with_stack( + after_exit: F, + stack: Stack, +) -> Fiber<(Fiber, Arg)> +where + F: FnOnce(Arg, Stack) -> Return + 'static, + Arg: 'static, + Return: 'static, + Stack: stack::Stack + 'static, +{ + // SAFETY: We never return from the intermediate function and do not reference any of the stack + // variables from outside. + let exec = unsafe { fiber_unchecked(stack.base()) }; + // Never return from this as this is the "main" function of any `Fiber` + exec.switch(|execution| { + // Double panic is good enough to prevent UB from happening. It would either abort + // in case unwinding is enabled, halt, or stuck in a loop forever, which is fine + // because `Fiber`s are `!Send`. + let _guard = scopeguard::guard((), |()| { + panic!("fiber panicked, aborting..."); + }); + + let (execution, arg): (Fiber<_>, _) = execution.switch(core::convert::identity); + execution.switch(|_| after_exit(arg, stack)) + }) +} + +/// Unsafely create a `Fiber`. +/// +/// A stronger version of [`fiber`] and [`fiber_with_stack`] constructors. +/// After switching to it, you cannot return from the [`intermediate`](`Fiber::switch`) closure. +/// The only way to get rid of this fiber is to switch to another fiber and drop this one. +/// +/// # Safety +/// +/// The `stack_base` must be the stack's base pointer, which you can get from `Stack::base`. +/// Used stack can be deallocated or reused only if you ensure fiber's stack variables aren't +/// referenced from somewhere else. +pub unsafe fn fiber_unchecked(stack_base: StackPointer) -> Fiber { + let sp = unsafe { arch::fiber_init_stack(stack_base) }; + Fiber:: { + sp, + _arg: PhantomData, + _thread_unsafe: PhantomData, + _unwind_unsafe: PhantomData, + } +} + +impl Fiber { + /// Context switch + /// + /// Switch or jump to a different `Fiber`. Suspends the current execution, then resumes `self` fiber and calls the `intermediate` closure, while passing previous execution as another `Fiber`. + /// The `YieldBack` generic type argument + /// + /// The `intermediate` closure in intended to convert previous fiber into the custom `Arg` type, allowing the use of caller's generic types during it. + /// Call to the `intermediate` closure happens on the `self` fiber instead of within the current execution, just because closure's `Fiber` argument must already represent this execution in the suspended state. + /// + /// # Unwind + /// + /// If call to `f` ever panics or unwinds by other means, then the process is aborted. + pub fn switch(self, intermediate: F) -> YieldBack + where + F: FnOnce(Fiber) -> Arg + 'static, + Arg: 'static, + { + unsafe extern "C-unwind" fn switcheroo( + sp: StackPointer, + arg: EncodedValue, + ret: *mut ffi::c_void, + ) where + F: FnOnce(Fiber) -> Arg + 'static, + { + let SwitchPayload { function } = decode_val::>(arg); + let execution = Fiber { + sp, + _arg: PhantomData, + _thread_unsafe: PhantomData, + _unwind_unsafe: PhantomData, + }; + ret.cast::().write(function(execution)); + } + + let mut output = mem::MaybeUninit::::uninit(); + let Fiber { sp, .. } = self; + let mut payload = mem::ManuallyDrop::new(SwitchPayload { + function: intermediate, + }); + unsafe { + let arg = encode_val(&mut payload); + arch::fiber_switch( + sp, + arg, + output.as_mut_ptr().cast::(), + switcheroo::, + ); + output.assume_init() + } + } + + /// In-place context switch + /// + /// See [`Fiber::switch`] for more information. + /// + /// Exploits exclusivity of mutable reference to context switch to a pointed-to fiber. + /// Such detail basically disallows a fiber to be simply destroyed, + /// as such [`Fiber::switch`] could be a better alternative in most cases. + /// + /// # Unwind + /// + /// In case a call of the `convert` closure unwinds, abort is triggered. + pub fn switch_in_place( + &mut self, + intermediate: F, + convert: G, + ) -> Output + where + F: FnOnce(Fiber) -> Arg + 'static, + G: FnOnce(YieldBack) -> (Fiber, Output), + Arg: 'static, + { + let this = ptr::addr_of_mut!(*self); + // We ensure the `self` fiber won't be invalid outside of this method, + // even in case the `convert` closure unwinds. + let guard = scopeguard::guard((), |()| { + panic!("convert closure panicked, aborting..."); + }); + let fiber = unsafe { ptr::read(this) }; + + let yield_ = fiber.switch(intermediate); + let (fiber, output) = convert(yield_); + + unsafe { ptr::write(this, fiber) }; + mem::forget(guard); + output + } + + /// Converts [`Fiber`] into a raw stack pointer. Use [`Fiber::from_raw`] to convert it back. + pub fn into_raw(self) -> StackPointer { + self.sp + } + + /// Converts raw stack pointer returned from [`Fiber::into_raw`] back to a [`Fiber`]. + /// + /// # Safety + /// + /// The `sp` argument must come from [`Fiber::into_raw`] call. + /// Recovered [`Fiber`] must have same specified type parameter as before. + pub unsafe fn from_raw(sp: StackPointer) -> Self { + Fiber { + sp, + _arg: PhantomData, + _thread_unsafe: PhantomData, + _unwind_unsafe: PhantomData, + } + } +} + +// TODO: BorrowingFiber diff --git a/src/lib.rs b/src/lib.rs index c1e9364a..0798ebaf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -243,11 +243,13 @@ mod unwind; mod arch; mod coroutine; +mod fiber; pub mod stack; pub mod trap; mod util; pub use coroutine::*; +pub use fiber::*; #[cfg(test)] mod tests; diff --git a/src/tests/fiber.rs b/src/tests/fiber.rs new file mode 100644 index 00000000..5c904a36 --- /dev/null +++ b/src/tests/fiber.rs @@ -0,0 +1,185 @@ +extern crate alloc; +extern crate std; + +use std::boxed::Box; +use std::cell::Cell; +use std::convert::{identity, Infallible}; +use std::panic::{catch_unwind, resume_unwind}; +use std::rc::Rc; +use std::string::ToString; +use std::{println, ptr}; + +use crate::{fiber, Fiber}; +use Fiber as F; + +#[test] +fn smoke() { + let hit = Rc::new(Cell::new(false)); + let hit2 = hit.clone(); + let fiber = fiber().switch(move |fiber| { + let parent = fiber.switch(identity); + hit2.set(true); + (parent, ()) + }); + assert!(!hit.get()); + () = fiber.switch(identity); + assert!(hit.get()); +} + +#[test] +fn suspend_and_resume() { + let hit = Rc::new(Cell::new(false)); + let hit2 = hit.clone(); + let fiber = fiber().switch(move |parent: F>>>>>>| { + let parent = parent.switch(identity); + let parent = parent.switch(identity); + hit2.set(true); + let parent = parent.switch(identity); + (parent, ()) + }); + assert!(!hit.get()); + let fiber = fiber.switch(identity); + assert!(!hit.get()); + let fiber = fiber.switch(identity); + assert!(hit.get()); + () = fiber.switch(identity); + assert!(hit.get()); +} + +// Linked backtraces are not supported on x86 Windows. +#[cfg_attr(all(windows, target_arch = "x86"), ignore)] +#[test] +fn backtrace_traces_to_host() { + #[inline(never)] // try to get this to show up in backtraces + fn look_for_me() { + run_test(); + } + fn contains_host() -> bool { + let trace = backtrace::Backtrace::new(); + println!("{:?}", trace); + trace + .frames() + .iter() + .flat_map(|f| f.symbols()) + .filter_map(|s| Some(s.name()?.to_string())) + .any(|s| s.contains("look_for_me")) + } + + fn run_test() { + assert!(contains_host()); + let fiber = fiber().switch(move |parent: F>>>>>>| { + let parent = parent.switch(identity); + assert!(!contains_host()); + let parent = parent.switch(identity); + let parent = parent.switch(|f| { + assert!(contains_host()); + f.switch(identity) + }); + let parent = parent.switch(identity); + assert!(!contains_host()); + (parent, ()) + }); + let fiber = fiber.switch(identity); + let fiber = fiber.switch(identity); + () = fiber.switch(identity); + } + + look_for_me(); +} + +#[test] +fn suspend_and_resume_values() { + let fiber = fiber().switch(move |fiber| { + let (parent, first): (Fiber<_>, _) = fiber.switch(identity); + assert_eq!(first, 2.0); + let (parent, second) = parent.switch(|f| (f, 4)); + assert_eq!(second, 3.0); + (parent, "hello".to_string()) + }); + let (fiber, second) = fiber.switch(|f| (f, 2.0)); + assert_eq!(second, 4); + let third = fiber.switch(|f| (f, 3.0)); + assert_eq!(third, "hello"); +} + +#[test] +fn stateful() { + enum Yield { + Continue(Fiber>, i32), + Stop, + } + + #[allow(dead_code)] + #[repr(align(128))] + #[allow(dead_code)] + struct Aligned(u8); + let state = [41, 42, 43, 44, 45]; + let aligned = Aligned(100); + let mut fiber = fiber().switch(move |fiber| { + let mut parent: Fiber = fiber.switch(identity); + assert_eq!(&aligned as *const _ as usize % 128, 0); + for i in state { + parent = parent.switch(move |f| Yield::Continue(f, i)); + } + (parent, Yield::Stop) + }); + for i in state { + match fiber.switch(identity) { + Yield::Continue(f, j) => { + fiber = f; + assert_eq!(j, i); + } + Yield::Stop => panic!("stopped too early"), + } + } + assert!(matches!(fiber.switch(identity), Yield::Stop)); +} + +// The Windows stack starts out small with only one page comitted. Check that it +// gets properly grown by the kernel as needed. +#[test] +fn stack_growth() { + let fiber = fiber().switch(|fiber| { + let parent = fiber.switch(identity); + + fn recurse(i: u32, p: &mut [u8; 10000]) { + unsafe { + // Ensure the stack allocation isn't optimized away. + ptr::read_volatile(&p); + } + if i > 0 { + recurse(i - 1, &mut [0; 10000]); + } + } + + // Use ~500KB of stack. + recurse(50, &mut [0; 10000]); + (parent, ()) + }); + fiber.switch(identity); +} + +#[test] +#[should_panic = "oh no"] +fn panic_from_switch() { + fiber::<()>().switch(|f| f.switch(|_| panic!("oh no"))) +} + +#[test] +fn unwind_through_switch() { + struct Unique(Fiber<(Fiber<()>, ())>); + unsafe impl Send for Unique {} + unsafe impl Sync for Unique {} + + let payload = catch_unwind(|| { + let _: Infallible = + fiber::<()>().switch(|f| f.switch(|f| resume_unwind(Box::new(Unique(f))))); + }) + .unwrap_err(); + + let fiber = payload.downcast::().unwrap(); + // cleanup + () = fiber.0.switch(|f| (f, ())); +} + +// TODO: panic_in_sythetic_fiber test diff --git a/src/tests/mod.rs b/src/tests/mod.rs index 8889aae2..d85f7ddb 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -1,2 +1,3 @@ mod coroutine; +mod fiber; mod on_stack; diff --git a/src/unwind.rs b/src/unwind.rs index e40e16f5..a4238d70 100644 --- a/src/unwind.rs +++ b/src/unwind.rs @@ -15,6 +15,8 @@ #![allow(unused_macros)] +use core::ffi; + use crate::stack::StackPointer; use crate::util::EncodedValue; @@ -340,6 +342,9 @@ cfg_if::cfg_if! { } } +pub type SwitchFiberFunc = + unsafe extern "C-unwind" fn(sp: StackPointer, arg: EncodedValue, obj: *mut ffi::c_void); + #[allow(unused_imports)] pub(crate) use { asm_may_unwind_root, asm_may_unwind_yield, cfi_reset_args_size, cfi_reset_args_size_root,