• Stars
    star
    619
  • Rank 72,496 (Top 2 %)
  • Language
    C
  • License
    Other
  • Created almost 4 years ago
  • Updated about 1 year ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Single header stackful cross-platform coroutine library in pure C.

minicoro

Minicoro is single-file library for using asymmetric coroutines in C. The API is inspired by Lua coroutines but with C use in mind.

The project is being developed mainly to be a coroutine backend for the Nelua programming language.

The library assembly implementation is inspired by Lua Coco by Mike Pall.

Features

  • Stackful asymmetric coroutines.
  • Supports nesting coroutines (resuming a coroutine from another coroutine).
  • Supports custom allocators.
  • Storage system to allow passing values between yield and resume.
  • Customizable stack size.
  • Coroutine API design inspired by Lua with C use in mind.
  • Yield across any C function.
  • Made to work in multithread applications.
  • Cross platform.
  • Minimal, self contained and no external dependencies.
  • Readable sources and documented.
  • Implemented via assembly, ucontext or fibers.
  • Lightweight and very efficient.
  • Works in most C89 compilers.
  • Error prone API, returning proper error codes on misuse.
  • Support running with Valgrind, ASan (AddressSanitizer) and TSan (ThreadSanitizer).

Supported Platforms

Most platforms are supported through different methods:

Platform Assembly Method Fallback Method
Android ARM/ARM64 N/A
iOS ARM/ARM64 N/A
Windows x86_64 Windows fibers
Linux x86_64/i686 ucontext
Mac OS X x86_64/ARM/ARM64 ucontext
WebAssembly N/A Emscripten fibers / Binaryen asyncify
Raspberry Pi ARM ucontext
RISC-V rv64/rv32 ucontext

The assembly method is used by default if supported by the compiler and CPU, otherwise ucontext or fiber method is used as a fallback.

The assembly method is very efficient, it just take a few cycles to create, resume, yield or destroy a coroutine.

Caveats

  • Don't use coroutines with C++ exceptions, this is not supported.
  • When using C++ RAII (i.e. destructors) you must resume the coroutine until it dies to properly execute all destructors.
  • To use in multithread applications, you must compile with C compiler that supports thread_local qualifier.
  • Some unsupported sanitizers for C may trigger false warnings when using coroutines.
  • The mco_coro object is not thread safe, you should lock each coroutine into a thread.
  • Stack space is fixed, it cannot grow. By default it has about 56KB of space, this can be changed on coroutine creation.
  • Take care to not cause stack overflows (run out of stack space), otherwise your program may crash or not, the behavior is undefined.
  • On WebAssembly you must compile with Emscripten flag -s ASYNCIFY=1.
  • The WebAssembly Binaryen asyncify method can be used when explicitly enabled, you may want to do this only to use minicoro with WebAssembly native interpreters (no Web browser). This method is confirmed to work well with Emscripten toolchain, however it fails on other WebAssembly toolchains like WASI SDK.

Introduction

A coroutine represents an independent "green" thread of execution. Unlike threads in multithread systems, however, a coroutine only suspends its execution by explicitly calling a yield function.

You create a coroutine by calling mco_create. Its sole argument is a mco_desc structure with a description for the coroutine. The mco_create function only creates a new coroutine and returns a handle to it, it does not start the coroutine.

You execute a coroutine by calling mco_resume. When calling a resume function the coroutine starts its execution by calling its body function. After the coroutine starts running, it runs until it terminates or yields.

A coroutine yields by calling mco_yield. When a coroutine yields, the corresponding resume returns immediately, even if the yield happens inside nested function calls (that is, not in the main function). The next time you resume the same coroutine, it continues its execution from the point where it yielded.

To associate a persistent value with the coroutine, you can optionally set user_data on its creation and later retrieve with mco_get_user_data.

To pass values between resume and yield, you can optionally use mco_push and mco_pop APIs, they are intended to pass temporary values using a LIFO (Last In, First Out) style buffer. The storage system can also be used to send and receive initial values on coroutine creation or before it finishes.

Usage

To use minicoro, do the following in one .c file:

#define MINICORO_IMPL
#include "minicoro.h"

You can do #include "minicoro.h" in other parts of the program just like any other header.

Minimal Example

The following simple example demonstrates on how to use the library:

#define MINICORO_IMPL
#include "minicoro.h"
#include <stdio.h>

// Coroutine entry function.
void coro_entry(mco_coro* co) {
  printf("coroutine 1\n");
  mco_yield(co);
  printf("coroutine 2\n");
}

int main() {
  // First initialize a `desc` object through `mco_desc_init`.
  mco_desc desc = mco_desc_init(coro_entry, 0);
  // Configure `desc` fields when needed (e.g. customize user_data or allocation functions).
  desc.user_data = NULL;
  // Call `mco_create` with the output coroutine pointer and `desc` pointer.
  mco_coro* co;
  mco_result res = mco_create(&co, &desc);
  assert(res == MCO_SUCCESS);
  // The coroutine should be now in suspended state.
  assert(mco_status(co) == MCO_SUSPENDED);
  // Call `mco_resume` to start for the first time, switching to its context.
  res = mco_resume(co); // Should print "coroutine 1".
  assert(res == MCO_SUCCESS);
  // We get back from coroutine context in suspended state (because it's unfinished).
  assert(mco_status(co) == MCO_SUSPENDED);
  // Call `mco_resume` to resume for a second time.
  res = mco_resume(co); // Should print "coroutine 2".
  assert(res == MCO_SUCCESS);
  // The coroutine finished and should be now dead.
  assert(mco_status(co) == MCO_DEAD);
  // Call `mco_destroy` to destroy the coroutine.
  res = mco_destroy(co);
  assert(res == MCO_SUCCESS);
  return 0;
}

NOTE: In case you don't want to use the minicoro allocator system you should allocate a coroutine object yourself using mco_desc.coro_size and call mco_init, then later to destroy call mco_uninit and deallocate it.

Yielding from anywhere

You can yield the current running coroutine from anywhere without having to pass mco_coro pointers around, to this just use mco_yield(mco_running()).

Passing data between yield and resume

The library has the storage interface to assist passing data between yield and resume. It's usage is straightforward, use mco_push to send data before a mco_resume or mco_yield, then later use mco_pop after a mco_resume or mco_yield to receive data. Take care to not mismatch a push and pop, otherwise these functions will return an error.

Error handling

The library return error codes in most of its API in case of misuse or system error, the user is encouraged to handle them properly.

Library customization

The following can be defined to change the library behavior:

  • MCO_API - Public API qualifier. Default is extern.
  • MCO_MIN_STACK_SIZE - Minimum stack size when creating a coroutine. Default is 32768.
  • MCO_DEFAULT_STORAGE_SIZE - Size of coroutine storage buffer. Default is 1024.
  • MCO_DEFAULT_STACK_SIZE - Default stack size when creating a coroutine. Default is 57344.
  • MCO_MALLOC - Default allocation function. Default is malloc.
  • MCO_FREE - Default deallocation function. Default is free.
  • MCO_DEBUG - Enable debug mode, logging any runtime error to stdout. Defined automatically unless NDEBUG or MCO_NO_DEBUG is defined.
  • MCO_NO_DEBUG - Disable debug mode.
  • MCO_NO_MULTITHREAD - Disable multithread usage. Multithread is supported when thread_local is supported.
  • MCO_NO_DEFAULT_ALLOCATORS - Disable the default allocator using MCO_MALLOC and MCO_FREE.
  • MCO_ZERO_MEMORY - Zero memory of stack for new coroutines and when poping storage, intended for garbage collected environments.
  • MCO_USE_ASM - Force use of assembly context switch implementation.
  • MCO_USE_UCONTEXT - Force use of ucontext context switch implementation.
  • MCO_USE_FIBERS - Force use of fibers context switch implementation.
  • MCO_USE_ASYNCIFY - Force use of Binaryen asyncify context switch implementation.
  • MCO_USE_VALGRIND - Define if you want run with valgrind to fix accessing memory errors.

Benchmarks

The coroutine library was benchmarked for x86_64 counting CPU cycles for context switch (triggered in resume or yield) and initialization.

CPU Arch OS Method Context switch Initialize Uninitialize
x86_64 Linux assembly 9 cycles 31 cycles 14 cycles
x86_64 Linux ucontext 352 cycles 383 cycles 14 cycles
x86_64 Windows fibers 69 cycles 10564 cycles 11167 cycles
x86_64 Windows assembly 33 cycles 74 cycles 14 cycles

NOTE: Tested on Intel Core i7-8750H CPU @ 2.20GHz with pre allocated coroutines.

Cheatsheet

Here is a list of all library functions for quick reference:

/* 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* (*malloc_cb)(size_t size, void* allocator_data); /* Custom allocation function. */
  void  (*free_cb)(void* ptr, void* allocator_data);     /* Custom deallocation function. */
  void* allocator_data;       /* User data pointer passed to `malloc`/`free` 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);  /* Initialize description of a coroutine. When stack size is 0 then MCO_DEFAULT_STACK_SIZE is used. */
mco_result mco_init(mco_coro* co, mco_desc* desc);                      /* Initialize the coroutine. */
mco_result mco_uninit(mco_coro* co);                                    /* Uninitialize the coroutine, may fail if it's not dead or suspended. */
mco_result mco_create(mco_coro** out_co, mco_desc* desc);               /* Allocates and initializes a new coroutine. */
mco_result mco_destroy(mco_coro* co);                                   /* Uninitialize and deallocate the coroutine, may fail if it's not dead or suspended. */
mco_result mco_resume(mco_coro* co);                                    /* Starts or continues the execution of the coroutine. */
mco_result mco_yield(mco_coro* co);                                     /* Suspends the execution of a coroutine. */
mco_state mco_status(mco_coro* co);                                     /* Returns the status of the coroutine. */
void* mco_get_user_data(mco_coro* co);                                  /* Get coroutine user data supplied on coroutine creation. */

/* Storage interface functions, used to pass values between yield and resume. */
mco_result mco_push(mco_coro* co, const void* src, size_t len); /* Push bytes to the coroutine storage. Use to send values between yield and resume. */
mco_result mco_pop(mco_coro* co, void* dest, size_t len);       /* Pop bytes from the coroutine storage. Use to get values between yield and resume. */
mco_result mco_peek(mco_coro* co, void* dest, size_t len);      /* Like `mco_pop` but it does not consumes the storage. */
size_t mco_get_bytes_stored(mco_coro* co);                      /* Get the available bytes that can be retrieved with a `mco_pop`. */
size_t mco_get_storage_size(mco_coro* co);                      /* Get the total storage size. */

/* Misc functions. */
mco_coro* mco_running(void);                        /* Returns the running coroutine for the current thread. */
const char* mco_result_description(mco_result res); /* Get the description of a result. */

Complete Example

The following is a more complete example, generating Fibonacci numbers:

#define MINICORO_IMPL
#include "minicoro.h"
#include <stdio.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;
  }

  /* Yield the last Fibonacci number. */
  mco_push(co, &m, sizeof(m));
}

int main() {
  /* Create the coroutine. */
  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);

  /* Set storage. */
  unsigned long max = 1000000000;
  mco_push(co, &max, sizeof(max));

  int counter = 1;
  while(mco_status(co) == MCO_SUSPENDED) {
    /* Resume the coroutine. */
    res = mco_resume(co);
    if(res != MCO_SUCCESS)
      fail("Failed to resume coroutine", res);

    /* Retrieve storage set in last coroutine yield. */
    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;
  }

  /* Destroy the coroutine. */
  res = mco_destroy(co);
  if(res != MCO_SUCCESS)
    fail("Failed to destroy coroutine", res);
  return 0;
}

Updates

  • 08-Jun-2022: Minicoro has been awarded by the Icculus Microgrant 2021, thanks @icculus for supporting open source work.
  • 26-Jan-2022: Added support for WebAssembly outside the WebBrowser using Binaryen asyncify pass.
  • 01-Sep-2021: Added support for DOSBox (MS-DOS Emulator).
  • 30-Aug-2021: Fix stack overflow crash on Windows 32 bits.
  • 22-Aug-2021: Added checks for stack overflow and iOS support (thanks @srberg).
  • 12-Mar-2021: Added support for RISC-V RV32.
  • 19-Jan-2021: Fix compilation and issues on Mac OS X, release v0.1.1.
  • 19-Jan-2021: First release, v0.1.0.
  • 18-Jan-2021: Fix issues when using Clang on Linux.
  • 17-Jan-2021: Add support for RISC-V 64 bits.
  • 16-Jan-2021: Add support for Mac OS X x86_64, thanks @RandyGaul for testing, debugging and researching about it.
  • 15-Jan-2021: Make assembly method the default one on Windows x86_64. Redesigned the storage API, thanks @RandyGaul for the suggestion.
  • 14-Jan-2021: Add support for running with ASan (AddressSanitizer) and TSan (ThreadSanitizer).
  • 13-Jan-2021: Add support for ARM and WebAssembly. Add Public Domain and MIT No Attribution license.
  • 12-Jan-2021: Some API changes and improvements.
  • 11-Jan-2021: Support valgrind and add benchmarks.
  • 10-Jan-2021: Minor API improvements and document more.
  • 09-Jan-2021: Library created.

Donation

I'm a full-time open source developer. Any amount of the donation will be appreciated and could bring me encouragement to keep supporting this and other open source projects.

Become a Patron

License

Your choice of either Public Domain or MIT No Attribution, see LICENSE file.

More Repositories

1

nelua-lang

Minimal, efficient, statically-typed and meta-programmable systems programming language heavily inspired by Lua, which compiles to C and native code.
Lua
1,986
star
2

otclient

An alternative tibia client for otserv written in C++11 and Lua, made with a modular system that uses lua scripts for ingame interface and functionality, making otclient flexible and easy to customize
C++
646
star
3

sokol_gp

Minimal modern efficient cross platform 2D graphics painter in C
C
445
star
4

minilua

Single-file port of Lua, a powerful scripting language.
C
252
star
5

minivorbis

Single-file port of libogg and libvorbis for decoding ogg sound files.
C
62
star
6

lpegrex

Parse programming languages syntax into an AST using PEGs with ease (LPeg Extension).
Lua
60
star
7

lua-bint

Arbitrary precision integer arithmetic library in pure Lua
Lua
59
star
8

miniphysfs

Single-file port of PhysFS, a library that provides abstract access to various archives.
C
58
star
9

luamon

Live development utility for Lua inspired by nodemon
Lua
56
star
10

ddosmon

Live DDOS monitor in the command line using ncurses
C++
55
star
11

nelua-decl

C binding generator for Nelua using GCC Lua plugin.
Lua
46
star
12

riscvm

Tiny RISC-V virtual machine
C
46
star
13

arraymancer-vision

Simple library for image loading, preprocessing and visualization for working with arraymancer.
Nim
29
star
14

nelua-batteries

Lua
26
star
15

tetrix

Tetris like game made in Nelua for RIV fantasy console
Lua
23
star
16

nelua-vscode

Nelua syntax highlighting for Visual Studio Code
JavaScript
21
star
17

lester

Minimal Lua test framework
Lua
21
star
18

nelua-game2048

Clone of the 2048 game in Nelua using Raylib
Lua
21
star
19

android-libs

Port of C++ libraries to Android that I use in my projects
C
17
star
20

antcopter

2D speed run platformer, originally made for the WASM4 game Jam, now also a game in RIVES.
Lua
16
star
21

lunray

Ray tracing experiment in Nelua programming language
Lua
15
star
22

nelua-sublime

Nelua syntax highlighting for Sublime Text
14
star
23

arraymancer-demos

Demos and benchmarks of arraymancer (WIP)
Nim
14
star
24

forkmon

Watch for file changes and auto restart an application using fork checkpoints to continue the process (for quick live development)
C
12
star
25

nelua-benchmarks

Benchmarks for comparing efficiency of Nelua with Lua, LuaJIT and C
Lua
12
star
26

nldoc

Tool to generate documentation for Nelua source files.
Lua
10
star
27

euluna-binder

Smart C++ binder for lua
C++
9
star
28

srpmalloc

Fork of rpmalloc to be used with single thread applications and old C compilers
C
8
star
29

otml

OTML, a human readable and easy typing data serialization file format. This is a compact version of its emitter/parser extracted from OTClient in just one header with no dependencies.
C++
8
star
30

ioslibs

Build scripts that I use to build required libraries for my iOS projects
Shell
8
star
31

minialloc

Allocator using multiple pools for small allocations, in a single C header.
C
7
star
32

miniminiz

Miniz in a single C header.
C
7
star
33

bwrapbox

Linux sandboxing utility on top of bubblewrap
C
7
star
34

marcherstein3d

Realtime pseudo 3D raycaster on the CPU using 2D ray marching
Lua
7
star
35

nelua-lsp

Experimenting creating a minimal LSP for Nelua to be used for tooling in text editors
Lua
6
star
36

nelua-vim

Vim plugin for Nelua, originally created by Stefanos
Vim Script
5
star
37

seqtoy

Toy music sequencer written in Nelua for RIV fantasy console
Lua
5
star
38

gcc-lua

Mirror of https://git.colberg.org/gcc-lua with minor fixes.
C
5
star
39

cartesi-wasm-term

Cartesi Machine interactive terminal in the browser
C++
5
star
40

synack

Efficient network DDOS attacker tool for testing
C
5
star
41

lua-dumper

A small library for dumping lua variables quickly. This library is typically used for quick debugging values to the terminal where one would use print but want more information about the value (like table content), output to stderr by default (or other file handle) and optionally time elapsed.
Lua
5
star
42

nelua-webview-demo

Example using Nelua for WebView apps
C++
4
star
43

nelua-samples

Some sample applications write in Nelua
C
4
star
44

cartesi-doom-example

Cartesi Machine running DOOM example
C
4
star
45

lua-hasher

A small library with hash and encoding functions implemented in C. Currently supports blake2b hashing and base58 encode/decode.
C
3
star
46

aoc

Advent of Code 2020 in Nelua
Lua
3
star
47

riv

RIV game framework - WIP
C
3
star
48

neuralnet

Simple and compact neural network in C using backpropagation algorithm
C
3
star
49

fwmonitor

Lua
2
star
50

nelua-decl-extra

Extra bindings for Nelua that I won't maintain regularly
Lua
2
star
51

osxlibs

Homebrew formulas that I use for compiling libraries for my Mac OS X projects
Ruby
2
star
52

TibiaEye

Old project that I've found lost in an old HD, it was a functional TibiaCam in Qt
C++
2
star
53

hypervisor-playground

Playing with hypervisor, running guest machines inside a host machine being emulated
Makefile
2
star
54

glwall

Simple experiment for animated wallpapers in Linux using GLSL shaders
C
1
star
55

edubart.github.com

Ruby
1
star
56

bladebomber

Game made during first RIVES game jam to demontrate RIV C API
C
1
star
57

edubart

GitHub profile
1
star
58

minislirp

Libslirp modified to be easy to cross compile
C
1
star