minicoro Documentation

repository·main·Indexed 21 days ago

https://github.com/edubart/minicoro

A lightweight, single-file C library providing stackful asymmetric coroutines. Designed for high efficiency and minimal dependencies, it serves as a backend for the Nelua programming language. It features a LIFO storage buffer for data passing between yield and resume, a virtual memory backed allocator for low physical memory footprints, and support for various context switch implementations including WebAssembly via Binaryen asyncify.

Tokens
3.8K
Snippets
8
Records
10
Agent score
26%

What's inside minicoro

  1. Enable the virtual memory backed allocator

    main

    To support thousands of coroutines with a low physical memory footprint, you can enable the virtual memory backed allocator by defining MCO_USE_VMEM_ALLOCATOR at compile time.

    How it works: It reserves virtual memory for the stack (up to ~2MB by default) but only commits physical memory pages (usually 4KB chunks) on demand as the stack grows. This prevents the high physical memory usage associated with large, pre-allocated stacks.

    Trade-offs:

    • Pros: Significantly lower physical memory usage; larger default stack (2040KB).
    • Cons: mco_create() and mco_destroy() may be an order of magnitude slower due to OS page table management.
    • Requirement: Requires an OS with virtual memory support.
    // Define this in your build configuration/compiler flags
    #define MCO_USE_VMEM_ALLOCATOR
  2. How coroutines work in minicoro

    main

    A coroutine is an independent "green" thread of execution that only suspends when explicitly told to via a yield function.

    • Creation: Use mco_create with an mco_desc structure. This returns a handle but does not start execution.
    • Execution: Use mco_resume to start or continue a coroutine. It runs until it either terminates or calls mco_yield.
    • Suspension: Use mco_yield to suspend execution. The mco_resume call that triggered the coroutine will return immediately, even if the yield occurs deep within nested function calls.
    • Resumption: Calling mco_resume on the same handle will pick up execution exactly where it last yielded.
    • Data Association: You can attach persistent data to a coroutine using user_data in the mco_desc and retrieve it via mco_get_user_data.
    mco_desc desc = mco_desc_init(coro_entry, 0);
    mco_coro* co;
    mco_create(&co, &desc);
    mco_resume(co);
    mco_yield(co);
    mco_resume(co);
  3. Install and integrate minicoro

    main

    Minicoro is a single-file C library. To use it in your project, define MINICORO_IMPL in exactly one .c file before including the header. In all other files, you can include the header normally.

    #define MINICORO_IMPL
    #include "minicoro.h"
    #define MINICORO_IMPL
    #include "minicoro.h"
  4. Configure minicoro via compile-time macros

    main

    You can customize the library's behavior by defining specific macros before including the header or via compiler flags.

    MacroDescription
    MCO_APIPublic API qualifier (default: extern)
    MCO_MIN_STACK_SIZEMinimum stack size (default: 32768)
    MCO_DEFAULT_STORAGE_SIZESize of coroutine storage buffer (default: 1024)
    MCO_DEFAULT_STACK_SIZEDefault stack size (default: 57344; ~2040KB if MCO_USE_VMEM_ALLOCATOR is set)
    MCO_ALLOCDefault allocation function (default: calloc)
    MCO_DEALLOCDefault deallocation function (default: free)
    MCO_USE_VMEM_ALLOCATOREnables virtual memory backed allocator
    MCO_NO_DEFAULT_ALLOCATORDisables default MCO_ALLOC/MCO_DEALLOC
    MCO_ZERO_MEMORYZeroes memory of stack when popping storage
    MCO_DEBUGEnables debug mode (logs errors to stdout)
    MCO_NO_DEBUGDisables debug mode
    MCO_NO_MULTITHREADDisables multithread support
    MCO_USE_ASMForce assembly context switch
    MCO_USE_UCONTEXTForce ucontext context switch
    MCO_USE_FIBERSForce fibers context switch
    MCO_USE_ASYNCIFYForce Binaryen asyncify (WebAssembly)
    MCO_USE_VALGRINDFixes memory errors when running with Valgrind
  5. Caveats and Best Practices

    main

    When using minicoro, be aware of the following constraints:

    • C++ Compatibility:
      • Avoid using coroutines with C++ exceptions; behavior is unpredictable.
      • When using RAII (destructors), you must resume the coroutine until it dies to ensure destructors execute.
    • Multithreading:
      • The mco_coro object is not thread-safe. Use a mutex if manipulating it from multiple threads.
      • You must compile with a compiler supporting the thread_local qualifier.
      • Avoid thread_local inside coroutine code: The compiler may cache pointers that become invalid when a coroutine switches threads.
    • WebAssembly:
      • If using Emscripten, you must compile with the flag -s ASYNCIFY=1.
    • Stack Management:
      • Default stack is 56KB. Exceeding this causes undefined behavior (crashes). Use MCO_USE_VMEM_ALLOCATOR to increase this to ~2MB safely.
  6. Minimal coroutine example

    main

    This example demonstrates the full lifecycle: initializing a description, creating a coroutine, resuming it twice (to pass through a yield), and destroying it.

    #define MINICORO_IMPL
    #include "minicoro.h"
    #include <stdio.h>
    #include <assert.h>
    
    void coro_entry(mco_coro* co) {
      printf("coroutine 1\n");
      mco_yield(co);
      printf("coroutine 2\n");
    }
    
    int main() {
      mco_desc desc = mco_desc_init(coro_entry, 0);
      desc.user_data = NULL;
      mco_coro* co;
      mco_result res = mco_create(&co, &desc);
      assert(res == MCO_SUCCESS);
      assert(mco_status(co) == MCO_SUSPENDED);
    
      res = mco_resume(co);
      assert(res == MCO_SUCCESS);
      assert(mco_status(co) == MCO_SUSPENDED);
    
      res = mco_resume(co);
      assert(res == MCO_SUCCESS);
      assert(mco_status(co) == MCO_DEAD);
    
      res = mco_destroy(co);
      assert(res == MCO_SUCCESS);
      return 0;
    }
  7. Implement a coroutine with data passing

    main

    To pass data between the caller and the coroutine, use mco_push inside the coroutine (or in the caller) and mco_pop to retrieve it. This is useful for sending parameters into a coroutine or receiving results from a mco_yield.

    In the example below, the coroutine receives a max value via mco_pop, calculates Fibonacci numbers, and sends them back to the caller using mco_push before yielding.

    #define MINICORO_IMPL
    #include "minicoro.h"
    #include <stdio.h>
    #include <stdlib.h>
    
    static void fail(const char* message, mco_result res) {
      printf("%s: %s\n", message, mco_result_description(res));
      exit(-1);
    }
    
    static void fibonacci_coro(mco_coro* co) {
      unsigned long m = 1;
      unsigned long n = 1;
    
      /* Retrieve max value. */
      unsigned long max;
      mco_result res = mco_pop(co, &max, sizeof(max));
      if(res != MCO_SUCCESS)
        fail("Failed to retrieve coroutine storage", res);
    
      while(1) {
        /* Yield the next Fibonacci number. */
        mco_push(co, &m, sizeof(m));
        res = mco_yield(co);
        if(res != MCO_SUCCESS)
          fail("Failed to yield coroutine", res);
    
        unsigned long tmp = m + n;
        m = n;
        n = tmp;
        if(m >= max)
          break;
      }
    
      mco_push(co, &m, sizeof(m));
    }
    
    int main() {
      mco_coro* co;
      mco_desc desc = mco_desc_init(fibonacci_coro, 0);
      mco_result res = mco_create(&co, &desc);
      if(res != MCO_SUCCESS)
        fail("Failed to create coroutine", res);
    
      unsigned long max = 1000000000;
      mco_push(co, &max, sizeof(max));
    
      int counter = 1;
      while(mco_status(co) == MCO_SUSPENDED) {
        res = mco_resume(co);
        if(res != MCO_SUCCESS)
          fail("Failed to resume coroutine", res);
    
        unsigned long ret = 0;
        res = mco_pop(co, &ret, sizeof(ret));
        if(res != MCO_SUCCESS)
          fail("Failed to retrieve coroutine storage", res);
        printf("fib %d = %lu\n", counter, ret);
        counter = counter + 1;
      }
    
      res = mco_destroy(co);
      if(res != MCO_SUCCESS)
        fail("Failed to destroy coroutine", res);
      return 0;
    }
  8. Yield from anywhere using mco_running()

    main

    If you are inside a nested function call and want to yield the current coroutine without passing the mco_coro* pointer through every function layer, use mco_yield(mco_running()).

    mco_yield(mco_running());
  9. Pass data between yield and resume

    main

    Minicoro provides a LIFO (Last-In, First-Out) storage buffer to pass temporary values between a mco_yield and a mco_resume.

    1. Use mco_push to send data before yielding or before resuming.
    2. Use mco_pop to retrieve data after yielding or after resuming.

    Warning: You must ensure that every mco_push is matched by an mco_pop. Mismatched calls will return an error.

    // Example pattern
    mco_push(co, data);
    mco_yield(co);
    // ... later ...
    data = mco_pop(co);
  10. Reference the minicoro API functions

    main

    The minicoro library provides functions for managing coroutine lifecycles, controlling execution flow, and passing data via a storage interface.

    Coroutine Lifecycle

    • mco_desc_init(func, stack_size): Initializes a mco_desc structure. If stack_size is 0, MCO_DEFAULT_STACK_SIZE is used.
    • mco_init(co, desc): Initializes an existing mco_coro using a description.
    • mco_uninit(co): Uninitializes a coroutine (fails if not dead or suspended).
    • mco_create(out_co, desc): Allocates and initializes a new coroutine.
    • mco_destroy(co): Uninitializes and deallocates a coroutine.

    Execution Control

    • mco_resume(co): Starts or continues coroutine execution.
    • mco_yield(co): Suspends the current coroutine.
    • mco_status(co): Returns the current mco_state.
    • mco_running(): Returns the mco_coro currently running in the calling thread.

    Storage Interface (Data Passing)

    Use these to pass bytes between mco_yield and mco_resume:

    • mco_push(co, src, len): Pushes bytes into coroutine storage.
    • mco_pop(co, dest, len): Pops bytes from storage (consumes them).
    • mco_peek(co, dest, len): Peeks at bytes without consuming them.
    • mco_get_bytes_stored(co): Returns available bytes for popping.
    • mco_get_storage_size(co): Returns total storage size.
    /* Structure used to initialize a coroutine. */
    typedef struct mco_desc {
      void (*func)(mco_coro* co); /* Entry point function for the coroutine. */
      void* user_data;            /* Coroutine user data, can be get with `mco_get_user_data`. */
      /* Custom allocation interface. */
      void* (*alloc_cb)(size_t size, void* allocator_data); /* Custom allocation function. */
      void  (*dealloc_cb)(void* ptr, size_t size, void* allocator_data);     /* Custom deallocation function. */
      void* allocator_data;       /* User data pointer passed to `alloc`/`dealloc` allocation functions. */
      size_t storage_size;        /* Coroutine storage size, to be used with the storage APIs. */
      /* These must be initialized only through `mco_init_desc`. */
      size_t coro_size;           /* Coroutine structure size. */
      size_t stack_size;           /* Coroutine stack size. */
    } mco_desc;
    
    /* Coroutine functions. */
    mco_desc mco_desc_init(void (*func)(mco_coro* co), size_t stack_size);
     mco_result mco_init(mco_coro* co, mco_desc* desc);
     mco_result mco_uninit(mco_coro* co);
     mco_result mco_create(mco_coro** out_co, mco_desc* desc);
     mco_result mco_destroy(mco_coro* co);
     mco_result mco_resume(mco_coro* co);
     mco_result mco_yield(mco_coro* co);
     mco_state mco_status(mco_coro* co);
     void* mco_get_user_data(mco_coro* co);
    
    /* Storage interface functions */
    mco_result mco_push(mco_coro* co, const void* src, size_t len);
     mco_result mco_pop(mco_coro* co, void* dest, size_t len);
     mco_result mco_peek(mco_coro* co, void* dest, size_t len);
     size_t mco_get_bytes_stored(mco_coro* co);
     size_t mco_get_storage_size(mco_coro* co);
    
    /* Misc functions. */
    mco_coro* mco_running(void);
     const char* mco_result_description(mco_result res);