corosensei

repository·master·Indexed 18 days ago

https://github.com/amanieu/corosensei

A high-performance Rust crate providing a fast and safe implementation of stackful coroutines. It enables context switching between different stacks, allowing functions to pause and resume while yielding and receiving data. The library supports bidirectional data flow, panic propagation with linked backtraces, and safe cleanup on drop. It can be used in #![no_std] environments and supports multiple architectures including x86_64, AArch64, and RISC-V.

Tokens
6K
Snippets
16
Records
24
Agent score
62%

What's inside corosensei

  1. What is corosensei and how do coroutines work?

    master

    corosensei provides a safe and efficient abstraction for context switching between different stacks using coroutines.

    A coroutine is a function that can be paused and resumed, yielding values to the caller. It can suspend itself from any point in its call stack. This allows for bidirectional data flow: you can receive yielded values from a coroutine and pass data back into the coroutine each time it is resumed.

    use corosensei::{Coroutine, CoroutineResult};
    
    fn main() {
        let mut coroutine = Coroutine::new(|yielder, input| {
            // ... coroutine logic ...
            let input = yielder.suspend(i);
            // ... 
        });
    
        loop {
            match coroutine.resume(counter) {
                CoroutineResult::Yield(i) => println!("got {:?}", i),
                CoroutineResult::Return(()) => break,
            }
        }
    }
  2. How cleanup on drop works

    master

    If a coroutine is dropped while it is suspended, its stack will be safely unwound using the same mechanism as panics. This ensures that all local variables on the stack are properly dropped.

    Warning: If the unwind feature is disabled and a coroutine is dropped while suspended, the program will abort.

    If you need to avoid this automatic unwinding, you can use the unsafe force_reset function to forcibly mark the coroutine as completed.

  3. How panic propagation works in coroutines

    master

    If a panic occurs within a coroutine, the panic will unwind through the coroutine stack and then continue to unwind out of the caller that last resumed it. Once this happens, the coroutine is considered complete and cannot be resumed again. This behavior requires the unwind feature (enabled by default).

    use std::panic::{catch_unwind, AssertUnwindSafe};
    use corosensei::Coroutine;
    
    fn main() {
        let mut coroutine = Coroutine::new(|yielder, ()| {
            yielder.suspend(42);
            panic!("foobar");
        });
    
        // Resuming once to yield
        let _ = coroutine.resume(()).unwrap().as_yield().unwrap();
    
        // Resuming again to trigger panic
        let result = catch_unwind(AssertUnwindSafe(|| coroutine.resume(())));
        // result will contain the panic error
    }
  4. How linked backtraces work

    master
    When the unwind feature is enabled, backtraces taken from within a coroutine will continue into the parent stack from the point where the coroutine was last resumed. This allows you to see the call path from the parent function into the coroutine, which is highly useful for debugging.
  5. Manual `Send` implementation for `Coroutine`

    master

    By default, Coroutine does not implement Send. This is because Rust cannot guarantee that all data currently residing on the coroutine's stack is Send.

    Warning: If you have full control over the code executed by the coroutine and can guarantee that all types on the stack during suspension are Send, it is safe to manually implement Send for your Coroutine instance.

  6. Configure Cargo features for corosensei

    master

    corosensei can be used in #![no_std] environments by disabling all Cargo features.

    Available features:

    • default-stack (Enabled by default): Provides a DefaultStack implementation that uses OS APIs for a guard page. Requires std. If disabled, you must provide a custom type implementing the Stack trait.
    • unwind (Enabled by default): Enables panic unwinding through the coroutine stack to the caller and safe cleanup on drop. Requires std. If disabled, dropping a suspended coroutine will cause an abort.
    • asm-unwind: Uses the asm_unwind nightly feature to allow panics to unwind directly through inline assembly, improving performance. Implies unwind.
    • sanitizer: Adds support for sanitizers like ASAN. Should only be enabled when running with sanitizers enabled to avoid linker errors.
  7. How Coroutine and Yielder work together

    master

    A Coroutine is a managed execution context with its own stack. It is initialized with a closure that defines its behavior.

    1. Initialization: You create a Coroutine using new or with_stack.
    2. Resumption: You call resume(input) on the Coroutine. This transfers control to the closure.
    3. Suspension: Inside the closure, the Yielder object is used to call suspend(yield_val). This pauses the coroutine and returns CoroutineResult::Yield(yield_val) to the caller.
    4. Completion: When the closure returns, resume() returns CoroutineResult::Return(return_val).

    The Yielder acts as the bridge, allowing the coroutine to communicate values back and forth with the caller during its lifecycle.

  8. Use ScopedCoroutine for non-'static lifetimes

    master

    ScopedCoroutine is a variant of Coroutine that allows the use of non-'static lifetimes. This is useful when you need to:

    • Use a borrowed stack.
    • Use lifetimes in Input, Yield, or Return types.
    • Use borrowed values within the coroutine body closure.

    To ensure safety, a ScopedCoroutine must be accessed within a scope using the .scope() method. This ensures that the coroutine is dropped at the end of the scope and that any borrows used by the coroutine do not outlive the scope. Inside the scope, you interact with the coroutine via a ScopedCoroutineRef.

    Note: Because ScopedCoroutineRef is a custom wrapper, you may need to call .as_mut() to reborrow it when invoking methods that require a mutable reference, as Rust does not automatically reborrow this specific type.

    use corosensei::{ScopedCoroutine, Yielder};
    
    let mut counter = 0;
    let coroutine = ScopedCoroutine::new(|yielder: &Yielder<i32, ()>, input| {
        if input == 0 {
            return;
        }
        counter += input;
        loop {
            let input = yielder.suspend(());
            if input == 0 {
                break;
            }
            counter += input;
        }
    });
    
    coroutine.scope(|mut coroutine| {
        // Use .as_mut() to access methods on the underlying 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);
  9. How coroutines work in corosensei

    master

    A coroutine is a function that can be paused and resumed, yielding values to the caller. It can suspend itself from any point in its call stack. Corosensei allows you to pass data into a coroutine each time it is resumed and receive yielded values from it.

    When a coroutine is resumed, it runs until it calls yielder.suspend(value). At that point, control returns to the caller with a CoroutineResult::Yield(value). When the coroutine function finishes, it returns CoroutineResult::Return(value).

    use corosensei::{Coroutine, CoroutineResult};
    
    fn main() {
        let mut coroutine = Coroutine::new(|yielder, input| {
            // 'input' is the value passed via .resume()
            // 'yielder.suspend(val)' yields 'val' to the caller
            let input: i32 = yielder.suspend(42);
        });
    
        // Resuming with an initial value
        match coroutine.resume(100) {
            CoroutineResult::Yield(i) => println!("Got {} from coroutine", i),
            CoroutineResult::Return(()) => println!("Coroutine finished"),
        }
    }
  10. Handle panics in coroutines

    master

    If a panic occurs inside a coroutine, it will unwind through the coroutine stack and then continue to unwind out of the caller that last resumed it. Once a panic occurs, the coroutine is considered complete and cannot be resumed again.

    You can use std::panic::catch_unwind to catch panics originating from a coroutine to prevent the entire application from crashing.

    use std::panic::{catch_unwind, AssertUnwindSafe};
    use corosensei::Coroutine;
    
    let mut coroutine = Coroutine::new(|_yielder, ()| {
        panic!("error inside coroutine");
    });
    
    let result = catch_unwind(AssertUnwindSafe(|| coroutine.resume(())));
    if result.is_err() {
        println!("Caught panic from coroutine");
    }
  11. Basic usage of Coroutine

    master

    To use corosensei, create a new coroutine using Coroutine::new. The closure passed to new receives a yielder and an input value. Inside the closure, call yielder.suspend(value) to pause execution and yield a value to the caller. To resume the coroutine, call coroutine.resume(value) on the coroutine instance. The result of resume is a CoroutineResult, which can be either Yield(value) or Return(value).

    use corosensei::{Coroutine, CoroutineResult};
    
    fn main() {
        println!("[main] creating coroutine");
    
        let mut coroutine = Coroutine::new(|yielder, input| {
            println!("[coroutine] coroutine started with input {}", input);
            for i in 0..5 {
                println!("[coroutine] yielding {}", i);
                let input = yielder.suspend(i);
                println!("[coroutine] got {} from parent", input)
            }
            println!("[coroutine] exiting coroutine");
        });
    
        let mut counter = 100;
        loop {
            println!("[main] resuming coroutine with argument {}", counter);
            match coroutine.resume(counter) {
                CoroutineResult::Yield(i) => println!("[main] got {:?} from coroutine", i),
                CoroutineResult::Return(()) => break,
            }
    
            counter += 1;
        }
    
        println!("[main] exiting");
    }
  12. Supported target architectures

    master

    corosensei supports various architectures across different platforms. Note that support varies by architecture and OS.

    ArchELF (Linux, BSD, etc)Darwin (macOS, iOS, etc)WindowsUEFI (no_std)
    x86_64
    x86⚠️*
    AArch64
    ARM
    RISC-V
    LoongArch64
    PowerPC64
    • On x86 Windows, linked backtraces are not supported.