Skip to content

TheComet/casync

Repository files navigation

Coroutines in C

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.

Features and Limitations

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.

Static API

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);

Starting co-routines dynamically

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.

Error Handling

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.

Building / Using as a library

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.h
  • src/casync.c
  • src/arch/stack_x86_64_sysv64.c
  • src/arch/yield_gas_x86_64_sysv64.s

These are the files required for x86_64-windows:

  • include/casync/casync.h
  • src/casync.c
  • src/arch/stack_x86_64_win64.c
  • src/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.c
  • util/sleep_win32.c