c-coroutine
c-coroutine is a cooperative multithreading library written in C89. Designed to be simple as possible in usage, but powerfully enough in execution, easily modifiable to any need. It incorporates most asynchronous patterns from various languages. They all the same behaviorally, just syntax layout differences.
This library was initially a rework/refactor and merge of libco with minicoro. These two differ among many coru, libdill, libmill, libwire, libcoro, libcsp, dyco-coroutine, in that Windows is supported, and not using ucontext. That was until I came across libtask, where the design is the underpinning of GoLang, and made it Windows compatible in an fork symplely/libtask. Libtask has it’s channel design origins from Richard Beton’s libcsp
This library currently represent a fully C
implementation of GoLang Go
routine.
To be clear, this is a programming paradigm on structuring your code. Which can be implemented in whatever language of choice. So this is also the C
representation of my purely PHP coroutine library by way of yield
. The same way Python usage evolved, see A Journey to Python Async.
“The role of the language, is to take care of the mechanics of the async pattern and provide a natural bridge to a language-specific implementation.” -Microsoft.
You can read Fibers, Oh My! for a breakdown on how the actual context switch here is achieved by assembly. This library incorporates libuv in a way that make providing callbacks unnecessary, same as in Using C++ Resumable Functions with Libuv. Libuv is handling any hardware or multi-threading CPU access. This not necessary for library usage, the setup can be replaced with some other Event Loop library, or just disabled. There is a unmaintained libasync package tried combining libco, with libuv too, Linux only.
Two videos covering things to keep in mind about concurrency, Building Scalable Deployments with Multiple Goroutines and Detecting and Fixing Unbound Concurrency Problems.
Table of Contents
Introduction
What’s the issue with no standard coroutine implementation in C, where that other languages seem to solve, or the ones still trying to solve?
- Probably the main answer is C’s manual memory/resource management requirement, the source of memory leaks, many bugs, just about everything.
- The other, the self impose adherence to idioms.
Whereas, a few languages derive there origins by using C as the development staring point.
The solution is quite amazing. Use what’s already have been assembled, or to be assembled differently.
There is another benefic of using coroutines besides concurrency, async abilities, better ululation of resources.
- The Go language has
defer
keyword, the given callback is used for general resource cleanup, memory management by garbage collection. - The Zig language has the
defer
keyword also, but used for semi-automatic memory management, memory cleanup attached to callback function, no garbage collection. - The Rust language has a complicated borrow checker system, memory owned/scope to caller, no garbage collection.
This library take these concepts and attach them to memory allocation routines, where the created/running, or switched to coroutine is the owner. All internal functions that needs memory allocation is using these routines.
co_new(size)
shortcut toco_malloc_full(coroutine, size, callback);
calls macro CO_MALLOC for allocation, and CO_FREE as the callback.co_new_by(count, size)
shortcut toco_calloc_full(coroutine, count, size, callback);
calls macro CO_CALLOC for allocation, and CO_FREE as the callback.co_defer(callback, *ptr)
will execute queued up callbacks when a coroutine exits/finish, LIFO.The macros can be set to use anything beside the default malloc/calloc/realloc/free.
There will be at least one coroutine always present, the initial, required co_main()
.
When a coroutine finish execution either by returning or exceptions, memory is released/freed.
The other problem with C is the low level usage view. I initially started out with the concept of creating Yet Another Programming language. But after discovering Cello High Level C, and the general issues and need to still integrate with exiting C libraries. This repo is now staging area the missing C runtime, ZeLang. The documentation WIP.
This page, coroutine.h
and examples folder files is the only current docs, but basic usage should be apparent.
The coroutine execution part here is completed, but how it operates/behaves with other system resources is what still being developed and tested.
There are five simple ways to create coroutines:
co_go(callable, *args);
schedules and returns int coroutine id, needed by other internal functions, this is a shortcut tocoroutine_create(callable, *args, CO_STACK_SIZE)
.co_await(callable, *args);
returns your value inside a generic union value_t type, after coroutine fully completes.- This is a combine shortcut to four functions:
co_wait_group();
returns hash-table storing coroutine-id’s of any future created,co_go(callable, *args);
calls, will end with a call to,co_wait(hash-table);
will suspend current coroutine, process coroutines until all are completed, returns hash-table of results for,co_group_get_result(hash-table, coroutine-id);
returns your value inside a generic union value_t type.
- This is a combine shortcut to four functions:
co_execute(function, *args)
creates coroutine and immediately execute, does not return any value.co_event(callable, *args)
same asco_await()
but for libuv or any event driven like library.co_handler(function, *handle, destructor)
initial setup for coroutine background handling of http request/response, the destructor function is passed toco_defer()
The coroutine stack size is set by defining
CO_STACK_SIZE
andCO_MAIN_STACK
forco_main()
,
The default for
CO_STACK_SIZE
is 10kb, andCO_MAIN_STACK
is 11kb incmake
build script, butcoroutine.h
has 64kb forCO_MAIN_STACK
.
Synopsis
/* Write this function instead of main, this library provides its own main, the scheduler,
which call this function as an coroutine! */
int co_main(int, char **);
/* Calls fn (with args as arguments) in separated thread, returning without waiting
for the execution of fn to complete. The value returned by fn can be accessed through
the future object returned (by calling `co_async_get()`). */
C_API future *co_async(callable_t, void_t);
/* Returns the value of a promise, a future thread's shared object, If not ready this
function blocks the calling thread and waits until it is ready. */
C_API value_t co_async_get(future *);
/* Waits for the future thread's state to change. this function pauses current coroutine
and execute others until future is ready, thread execution has ended. */
C_API void co_async_wait(future *);
/* Creates/initialize the next series/collection of coroutine's created to be part of wait group,
same behavior of Go's waitGroups, but without passing struct or indicating when done.
All coroutines here behaves like regular functions, meaning they return values, and indicate
a terminated/finish status.
The initialization ends when `co_wait()` is called, as such current coroutine will pause, and
execution will begin for the group of coroutines, and wait for all to finished. */
C_API wait_group_t *co_wait_group(void);
/* Pauses current coroutine, and begin execution for given coroutine wait group object, will
wait for all to finished. Returns hast table of results, accessible by coroutine id. */
C_API wait_result_t co_wait(wait_group_t *);
/* Returns results of the given completed coroutine id, value in union value_t storage format. */
C_API value_t co_group_get_result(wait_result_t *, int);
/* Creates an unbuffered channel, similar to golang channels. */
C_API channel_t *channel(void);
/* Creates an buffered channel of given element count,
similar to golang channels. */
C_API channel_t *channel_buf(int);
/* Send data to the channel. */
C_API int co_send(channel_t *, void_t);
/* Receive data from the channel. */
C_API value_t *co_recv(channel_t *);
/* The `for_select {` macro sets up a coroutine to wait on multiple channel operations.
Must be closed out with `} select_end;`, and if no `select_case(channel)`, `select_case_if(channel)`,
`select_break` provided, an infinite loop is created.
This behaves same as GoLang `select {}` statement.
*/
for_select {
select_case(channel) {
co_send(channel, void_t data);
// Or
value_t *r = co_recv(channel);
// Or
} select_case_if(channel) {
// co_send(channel); || co_recv(channel);
/* The `select_default` is run if no other case is ready.
Must also closed out with `select_break;`. */
} select_default {
// ...
} select_break;
} select_end;
/* Creates an coroutine of given function with argument,
and add to schedular, same behavior as Go in golang. */
C_API int co_go(callable_t, void_t);
/* Creates an coroutine of given function with argument, and immediately execute. */
C_API void co_execute(co_call_t, void_t);
/* Explicitly give up the CPU for at least ms milliseconds.
Other tasks continue to run during this time. */
C_API unsigned int co_sleep(unsigned int ms);
/* Call `CO_MALLOC` to allocate memory of given size in current coroutine,
will auto free `LIFO` on function exit/return, do not free! */
C_API void_t co_new(size_t);
/* Call `CO_CALLOC` to allocate memory array of given count and size in current coroutine,
will auto free `LIFO` on function exit/return, do not free! */
C_API void_t co_new_by(int count, size_t size);
/* Defer execution `LIFO` of given function with argument,
to when current coroutine exits/returns. */
C_API void co_defer(func_t, void_t);
/* An macro that stops the ordinary flow of control and begins panicking,
throws an exception of given message. */
co_panic(message);
/* Same as `defer` but allows recover from an Error condition throw/panic,
you must call `co_catch` inside function to mark Error condition handled. */
C_API void co_recover(func_t, void_t);
/* Compare `err` to current error condition of coroutine, will mark exception handled, if `true`. */
C_API bool co_catch(string_t err);
/* Get current error condition string. */
C_API string_t co_message(void);
/* Generic simple union storage types. */
typedef union
{
int integer;
unsigned int u_int;
signed long s_long;
unsigned long u_long;
long long long_long;
size_t max_size;
float point;
double precision;
bool boolean;
signed short s_short;
unsigned short u_short;
signed char schar;
unsigned char uchar;
unsigned char *uchar_ptr;
char *char_ptr;
char **array;
void_t object;
callable_t func;
const char str[512];
} value_t;
typedef struct values_s
{
value_t value;
value_types type;
} values_t;
/* Return an value in union type storage. */
C_API value_t co_value(void_t);
The above is the main and most likely functions to be used, see coroutine.h for additional.
Note: None of the functions above require passing/handling the underlying
routine_t
object/structure.
Usage
Original Go example from https://www.golinuxcloud.com/goroutines-golang/
GoLang | C89 |
---|---|
|
|
DEBUG run output
Thread #7f11090f11c0 running coroutine id: 1 () status: x cycles: x Start of main Goroutine Back at coroutine scheduling Thread #7f11090f11c0 running coroutine id: 2 () status: x cycles: x 0 ==> John Back at coroutine scheduling Thread #7f11090f11c0 running coroutine id: 3 () status: x cycles: x 0 ==> Mary Back at coroutine scheduling Thread #7f11090f11c0 running coroutine id: 4 () status: x cycles: x Back at coroutine scheduling Thread #7f11090f11c0 running coroutine id: 4 (coroutine_wait) status: x cycles: x Back at coroutine scheduling Thread #7f11090f11c0 running coroutine id: 2 () status: x cycles: x 1 ==> John Back at coroutine scheduling Thread #7f11090f11c0 running coroutine id: 3 () status: x cycles: x 1 ==> Mary Back at coroutine scheduling Thread #7f11090f11c0 running coroutine id: 4 (coroutine_wait) status: x cycles: x Back at coroutine scheduling Thread #7f11090f11c0 running coroutine id: 4 (coroutine_wait) status: x cycles: x Back at coroutine scheduling Thread #7f11090f11c0 running coroutine id: 2 () status: x cycles: x 2 ==> John Back at coroutine scheduling Thread #7f11090f11c0 running coroutine id: 3 () status: x cycles: x 2 ==> Mary Back at coroutine scheduling Thread #7f11090f11c0 running coroutine id: 4 (coroutine_wait) status: x cycles: x Back at coroutine scheduling Thread #7f11090f11c0 running coroutine id: 4 (coroutine_wait) status: x cycles: x Back at coroutine scheduling Thread #7f11090f11c0 running coroutine id: 2 () status: x cycles: x Back at coroutine scheduling Thread #7f11090f11c0 running coroutine id: 3 () status: x cycles: x Back at coroutine scheduling Thread #7f11090f11c0 running coroutine id: 4 (coroutine_wait) status: x cycles: x Back at coroutine scheduling Thread #7f11090f11c0 running coroutine id: 4 (coroutine_wait) status: x cycles: x Back at coroutine scheduling Thread #7f11090f11c0 running coroutine id: 4 (coroutine_wait) status: x cycles: x Back at coroutine scheduling Thread #7f11090f11c0 running coroutine id: 4 (coroutine_wait) status: x cycles: x Back at coroutine scheduling ... ... ... ... Thread #7f6ac8aa11c0 running coroutine id: 4 (coroutine_wait) status: x cycles: x Back at coroutine scheduling Thread #7f6ac8aa11c0 running coroutine id: 4 (coroutine_wait) status: x cycles: x Back at coroutine scheduling Thread #7f6ac8aa11c0 running coroutine id: 1 (co_main) status: x cycles: x End of main Goroutine Back at coroutine scheduling Coroutine scheduler exited
Original Go example from https://www.programiz.com/golang/channel
GoLang | C89 |
---|---|
|
|
DEBUG run output
Thread #7f87171711c0 running coroutine id: 1 () status: x cycles: x processed r:0x7fffb878dca0 Back at coroutine scheduling Thread #7f87171711c0 running coroutine id: 2 () status: x cycles: x processed s:0x7fffb878dca0* => s:0x7fffb878dca0 No receiver! Send Operation Blocked Back at coroutine scheduling Thread #7f87171711c0 running coroutine id: 1 (co_main) status: x cycles: x Received. Send Operation Successful Back at coroutine scheduling Coroutine scheduler exited
Original Go example from https://go.dev/tour/concurrency/5
GoLang | C89 |
---|---|
|
|
DEBUG run output
Thread #7f2dadbb11c0 running coroutine id: 1 () status: x cycles: x Back at coroutine scheduling Thread #7f2dadbb11c0 running coroutine id: 2 () status: x cycles: x processed r:0x7fffc1f35ca0 Back at coroutine scheduling Thread #7f2dadbb11c0 running coroutine id: 1 (co_main) status: x cycles: x processed s:0x7fffc1f35ca0* => s:0x7fffc1f35ca0 Back at coroutine scheduling Thread #7f2dadbb11c0 running coroutine id: 2 () status: x cycles: x 0 processed r:0x7fffc1f35ca0 Back at coroutine scheduling Thread #7f2dadbb11c0 running coroutine id: 1 (co_main) status: x cycles: x processed s:0x7fffc1f35ca0* => s:0x7fffc1f35ca0 Back at coroutine scheduling Thread #7f2dadbb11c0 running coroutine id: 2 () status: x cycles: x 1 processed r:0x7fffc1f35ca0 Back at coroutine scheduling Thread #7f2dadbb11c0 running coroutine id: 1 (co_main) status: x cycles: x processed s:0x7fffc1f35ca0* => s:0x7fffc1f35ca0 Back at coroutine scheduling Thread #7f2dadbb11c0 running coroutine id: 2 () status: x cycles: x 1 processed r:0x7fffc1f35ca0 Back at coroutine scheduling Thread #7f2dadbb11c0 running coroutine id: 1 (co_main) status: x cycles: x processed s:0x7fffc1f35ca0* => s:0x7fffc1f35ca0 Back at coroutine scheduling Thread #7f2dadbb11c0 running coroutine id: 2 () status: x cycles: x 2 processed r:0x7fffc1f35ca0 Back at coroutine scheduling Thread #7f2dadbb11c0 running coroutine id: 1 (co_main) status: x cycles: x processed s:0x7fffc1f35ca0* => s:0x7fffc1f35ca0 Back at coroutine scheduling Thread #7f2dadbb11c0 running coroutine id: 2 () status: x cycles: x 3 processed r:0x7fffc1f35ca0 Back at coroutine scheduling Thread #7f2dadbb11c0 running coroutine id: 1 (co_main) status: x cycles: x processed s:0x7fffc1f35ca0* => s:0x7fffc1f35ca0 Back at coroutine scheduling Thread #7f2dadbb11c0 running coroutine id: 2 () status: x cycles: x 5 processed r:0x7fffc1f35ca0 Back at coroutine scheduling Thread #7f2dadbb11c0 running coroutine id: 1 (co_main) status: x cycles: x processed s:0x7fffc1f35ca0* => s:0x7fffc1f35ca0 Back at coroutine scheduling Thread #7f2dadbb11c0 running coroutine id: 2 () status: x cycles: x 8 processed r:0x7fffc1f35ca0 Back at coroutine scheduling Thread #7f2dadbb11c0 running coroutine id: 1 (co_main) status: x cycles: x processed s:0x7fffc1f35ca0* => s:0x7fffc1f35ca0 Back at coroutine scheduling Thread #7f2dadbb11c0 running coroutine id: 2 () status: x cycles: x 13 processed r:0x7fffc1f35ca0 Back at coroutine scheduling Thread #7f2dadbb11c0 running coroutine id: 1 (co_main) status: x cycles: x processed s:0x7fffc1f35ca0* => s:0x7fffc1f35ca0 Back at coroutine scheduling Thread #7f2dadbb11c0 running coroutine id: 2 () status: x cycles: x 21 processed r:0x7fffc1f35ca0 Back at coroutine scheduling Thread #7f2dadbb11c0 running coroutine id: 1 (co_main) status: x cycles: x processed s:0x7fffc1f35ca0* => s:0x7fffc1f35ca0 Back at coroutine scheduling Thread #7f2dadbb11c0 running coroutine id: 2 () status: x cycles: x 34 processed s:0x7fffc1f35f10 Back at coroutine scheduling Thread #7f2dadbb11c0 running coroutine id: 1 (co_main) status: x cycles: x processed r:0x7fffc1f35f10* => r:0x7fffc1f35f10 quit Back at coroutine scheduling Thread #7f2dadbb11c0 running coroutine id: 2 () status: x cycles: x Back at coroutine scheduling Coroutine scheduler exited
Original Go example from https://www.developer.com/languages/go-error-handling-with-panic-recovery-and-defer/
GoLang | C89 |
---|---|
|
|
DEBUG run output
Thread #7fd29c4011c0 running coroutine id: 1 () status: x cycles: x Back at coroutine scheduling Thread #7fd29c4011c0 running coroutine id: 2 () status: x cycles: x panic occurred: sig_ill Back at coroutine scheduling Thread #7fd29c4011c0 running coroutine id: 1 (co_main) status: x cycles: x Although panicked. We recovered. We call mul() func mul func result: 50 Back at coroutine scheduling Coroutine scheduler exited
Original Go example from https://gobyexample.com/waitgroups
GoLang | C89 |
---|---|
|
|
DEBUG run output
Thread #7f48f6c211c0 running coroutine id: 1 () status: x cycles: x Back at coroutine scheduling Thread #7f48f6c211c0 running coroutine id: 2 () status: x cycles: x Worker 2 starting Back at coroutine scheduling Thread #7f48f6c211c0 running coroutine id: 3 () status: x cycles: x Worker 3 starting Back at coroutine scheduling Thread #7f48f6c211c0 running coroutine id: 4 () status: x cycles: x Worker 4 starting Back at coroutine scheduling Thread #7f48f6c211c0 running coroutine id: 5 () status: x cycles: x Worker 5 starting Back at coroutine scheduling Thread #7f48f6c211c0 running coroutine id: 6 () status: x cycles: x Worker 6 starting Back at coroutine scheduling Thread #7f48f6c211c0 running coroutine id: 1 (co_main) status: x cycles: x Back at coroutine scheduling Thread #7f48f6c211c0 running coroutine id: 7 () status: x cycles: x Back at coroutine scheduling Thread #7f48f6c211c0 running coroutine id: 6 () status: x cycles: x Worker 6 done Back at coroutine scheduling Thread #7f48f6c211c0 running coroutine id: 1 (co_main) status: x cycles: x Back at coroutine scheduling Thread #7f48f6c211c0 running coroutine id: 7 (coroutine_wait) status: x cycles: x Back at coroutine scheduling Thread #7f48f6c211c0 running coroutine id: 5 () status: x cycles: x Worker 5 done Back at coroutine scheduling Thread #7f48f6c211c0 running coroutine id: 1 (co_main) status: x cycles: x Back at coroutine scheduling Thread #7f48f6c211c0 running coroutine id: 7 (coroutine_wait) status: x cycles: x Back at coroutine scheduling Thread #7f48f6c211c0 running coroutine id: 4 () status: x cycles: x Worker 4 done Back at coroutine scheduling Thread #7f48f6c211c0 running coroutine id: 1 (co_main) status: x cycles: x Back at coroutine scheduling Thread #7f48f6c211c0 running coroutine id: 7 (coroutine_wait) status: x cycles: x Back at coroutine scheduling Thread #7f48f6c211c0 running coroutine id: 3 () status: x cycles: x Worker 3 done Back at coroutine scheduling Thread #7f48f6c211c0 running coroutine id: 1 (co_main) status: x cycles: x Back at coroutine scheduling Thread #7f48f6c211c0 running coroutine id: 7 (coroutine_wait) status: x cycles: x Back at coroutine scheduling Thread #7f48f6c211c0 running coroutine id: 2 () status: x cycles: x Worker 2 done Back at coroutine scheduling Thread #7f48f6c211c0 running coroutine id: 1 (co_main) status: x cycles: x Worker # 4 returned: 32 Worker # 3 returned: hello world Back at coroutine scheduling Coroutine scheduler exited
The C++ 20
concurrency thread model by way of future/promise implemented with same like semantics.
Original C++ 20 example from https://cplusplus.com/reference/future/future/wait/
C++ 20 | C89 |
---|---|
|
|
DEBUG run output
Thread #7fc61b3d11c0 running coroutine id: 1 () status: x cycles: x promise id(706099430) created in thread #7fc61b3d11c0 thread #7fc61b3d11c0 created thread #7fc61b2b0700 with status(0) future id(706099430) checking... Back at coroutine scheduling Thread #7fc61b3d11c0 running coroutine id: 1 (co_main) status: x cycles: x Back at coroutine scheduling Thread #7fc61b3d11c0 running coroutine id: 1 (co_main) status: x cycles: x Back at coroutine scheduling Thread #7fc61b3d11c0 running coroutine id: 1 (co_main) status: x cycles: x Back at coroutine scheduling ... ... ... ... Thread #7fc61b3d11c0 running coroutine id: 1 (co_main) status: x cycles: x Back at coroutine scheduling Thread #7fc61b3d11c0 running coroutine id: 1 (co_main) status: x cycles: x Back at coroutine scheduling Thread #7fc61b3d11c0 running coroutine id: 1 (co_main) status: x cycles: x Back at coroutine scheduling promise id(706099430) set LOCK in thread #7fc61b2b0700 promise id(706099430) set UNLOCK in thread #7fc61b2b0700 Thread #7fc61b3d11c0 running coroutine id: 1 (co_main) status: x cycles: x Back at coroutine scheduling Thread #7fc61b3d11c0 running coroutine id: 1 (co_main) status: x cycles: x 194232491 promise id(706099430) get LOCK in thread #7fc61b3d11c0 promise id(706099430) get UNLOCK in thread #7fc61b3d11c0 is prime! Back at coroutine scheduling Coroutine scheduler exited
See examples folder for more
Installation
The build system uses cmake, that produces single static library stored under coroutine-built
, and the complete include
folder is needed.
Linux
mkdir build
cd build
cmake .. -DCMAKE_BUILD_TYPE=Debug/Release -DBUILD_TESTING=ON/OFF # use to build files examples folder
cmake --build .
Windows
mkdir build
cd build
cmake .. -D BUILD_TESTING=ON/OFF # use to build files examples folder
cmake --build . --config Debug/Release
Contributing
Contributions are encouraged and welcome; I am always happy to get feedback or pull requests on Github :) Create Github Issues for bugs and new features and comment on the ones you are interested in.
License
The MIT License (MIT). Please see License File for more information.