I was inspired by Coroutines in C by Simon Tatham.
Unlike the article, though, this library implements a very simple round-robin scheduler with a proper context switching mechanism to jump between co-routines. It is cooperative multitasking in userspace.
The two main functions to do this are:
casync_gather()casync_yield()
For example:
#include "casync/casync.h"
#include <stdio.h>
static int task(void* arg) {
fprintf(stderr, "task %d\n", (int)(intptr_t)arg);
casync_yield();
fprintf(stderr, "task %d\n", (int)(intptr_t)arg);
return 0;
}
int main(void) {
task((void*)1);
task((void*)2);
return casync_gather(2,
task, (void*)1,
task, (void*)2);
}Prints:
task 1
task 1
task 2
task 2
task 1
task 2
task 1
task 2
As you can see, the the task() function is executed as a co-routine
twice, and execution jumps between the two via casync_yield().
Also notice how it's possible to call a co-routine from a "normal" function,
without the need of a scheduler. When a co-routine is called directly, then
casync_yield() will return immediately and do nothing.
Limitations:
- Currently, only the general purpose registers are saved. That means FPU and SIMD state will leak across co-routines. Adding support for this is trivial, though, so expect an update soon.
- The Windows i386 port has not been done yet. This is coming soon as well.
Features:
- The scheduler state is stored in TLS (thread-local storage), meaning, you can run co-routines in multiple threads at the same time. The usual synchronization primitives can be used to synchronize data across threaded co-routines.
The "normal" API uses malloc() internally to allocate the stack for each
co-routine. The default stack size is 1M.
If you want to supply your own memory to be used as stack space, then you can
use the _static() functions such as casync_gather_static(). For
example, if you know that you will have at most 5 co-routines running
simulatneously in a call to gather, then you can define the memory up
front:
static size_t stacks[1024 * 64][5] __attribute__((aligned(16)));
struct casync_task* freelist = casync_stack_pool_init_linear(stacks,
sizeof(stacks) / sizeof(*stacks),
sizeof(*stacks) / sizeof(**stacks));The freelist can then be passed to gather:
casync_gather_static(freelist, 2,
casync_gather(2,
task, (void*)1,
task, (void*)2);This use-case is covered in example3.c where a server will start a new
co-routine for every client that joins. This is accomplished using the
casync_start() function. casync_start() will create a new
co-routine and add it to the active list within the current gather()
context. It will be as if you had called gather() with the co-routine
added there. For example:
static int dynamic(void* arg)
{
fprintf(stderr, "dynamic\n");
return 0;
}
static int task(void* arg) {
casync_start(dynamic, NULL);
fprintf(stderr, "task %d\n", (int)(intptr_t)arg);
casync_yield();
fprintf(stderr, "task %d\n", (int)(intptr_t)arg);
return 0;
}
int main(void) {
casync_gather(2,
task, (void*)1,
task, (void*)2);
}This will print:
task 1
task 2
dynamic
task 1
dynamic
task 2
As you can see, dynamic is printed twice. Why? Well, because task()
is run twice, and each instance of task() adds dynamic as well.
Co-routines can return an integer status code to indicate success or failure. Returning 0 indicates success. Returning any other value indicates an error.
If any co-routine returns an error, then the same error value will be returned
by gather(). Note, though, that all co-routines must still complete
before gather() can return. gather() will return the value of the
last co-routine that returned an error.
The simplest way to include casync in your own project is probably to add the source files directly. casync consists of one header file, and a few platform-specific source files.
For example, these are the files required for x86_64-linux:
include/casync/casync.hsrc/casync.csrc/arch/stack_x86_64_sysv64.csrc/arch/yield_gas_x86_64_sysv64.s
These are the files required for x86_64-windows:
include/casync/casync.hsrc/casync.csrc/arch/stack_x86_64_win64.csrc/arch/yield_masm_x86_64_win64.asm
There are additionally some platform-specific utility functions such as
casync_sleep_ns(). These are optional, but if you need these functions,
you must also add one of the following source files:
util/sleep_posix.cutil/sleep_win32.c