Skip to content

Instantly share code, notes, and snippets.

@MCJack123
Created July 11, 2020 07:42
Show Gist options
  • Save MCJack123/a685752b3e094dc24805d5f4953d9d4e to your computer and use it in GitHub Desktop.
Save MCJack123/a685752b3e094dc24805d5f4953d9d4e to your computer and use it in GitHub Desktop.
Unsafe, untrustworthy coroutine implementation in C (x64, GCC only, tested on Windows)
#include <stdlib.h>
#include <string.h>
#include <setjmp.h>
#include <errno.h>
struct coroutine {
int status;
void* (*fn)(void*);
void* arg;
jmp_buf env_resume;
jmp_buf env_yield;
unsigned long stack_size;
void * stack_bottom;
void * stack_top;
};
typedef struct coroutine * coroutine_t;
struct stack {
coroutine_t val;
struct stack * previous;
};
typedef struct stack * stack;
enum coroutine_status {
COROUTINE_STATUS_NEW,
COROUTINE_STATUS_SUSPENDED,
COROUTINE_STATUS_RUNNING,
COROUTINE_STATUS_NORMAL,
COROUTINE_STATUS_DEAD
};
static stack stack_create_entry(coroutine_t num) {
stack s = (stack)malloc(sizeof(struct stack));
s->val = num;
s->previous = NULL;
return s;
}
static void stack_push(stack* s, coroutine_t num) {
stack e = stack_create_entry(num);
e->previous = *s;
*s = e;
}
static coroutine_t stack_read(stack* s) {
if (*s == NULL) return NULL;
return (*s)->val;
}
static coroutine_t stack_pop(stack* s) {
stack n = *s;
if (n == NULL) return NULL;
coroutine_t retval = n->val;
*s = n->previous;
free(n);
return retval;
}
static stack coro_stack = NULL;
coroutine_t coroutine_create(void*(*fn)(void*)) {
coroutine_t coro = (coroutine_t)malloc(sizeof(struct coroutine));
coro->status = COROUTINE_STATUS_NEW;
coro->fn = fn;
coro->arg = NULL;
coro->stack_size = 16 * 1024;
coro->stack_bottom = malloc(coro->stack_size);
coro->stack_top = coro->stack_bottom + coro->stack_size;
memset(coro->stack_bottom, 0, coro->stack_size);
return coro;
}
void coroutine_delete(coroutine_t coro) {
free(coro->stack_bottom);
free(coro);
}
static void execute_coro(coroutine_t coro) {
coro->status = COROUTINE_STATUS_RUNNING;
coro->arg = coro->fn(coro->arg);
if (stack_read(&coro_stack) == coro) stack_pop(&coro_stack);
coro->status = COROUTINE_STATUS_DEAD;
longjmp(coro->env_resume, 1);
}
void* coroutine_resume(coroutine_t coro, void* arg) {
void* retval;
int val;
static coroutine_t coro_tmp; // for use once we change stacks
if (coro->status != COROUTINE_STATUS_NEW && coro->status != COROUTINE_STATUS_SUSPENDED) {
errno = EINVAL;
switch (coro->status) {
case COROUTINE_STATUS_RUNNING: return "cannot resume running coroutine";
case COROUTINE_STATUS_NORMAL: return "cannot resume normal coroutine";
case COROUTINE_STATUS_DEAD: return "cannot resume dead coroutine";
default: return "cannot resume unknown coroutine";
}
}
if (stack_read(&coro_stack) != NULL) stack_read(&coro_stack)->status = COROUTINE_STATUS_NORMAL;
stack_push(&coro_stack, coro);
coro->arg = arg;
val = setjmp(coro->env_resume);
((_JUMP_BUFFER*)coro->env_resume)->Frame = 0;
if (val) { // function yielded
if (stack_read(&coro_stack) != NULL) stack_read(&coro_stack)->status = COROUTINE_STATUS_RUNNING;
errno = 0;
return coro->arg;
} else if (coro->status == COROUTINE_STATUS_NEW) { // coroutine hasn't started
// set the stack for the function
coro_tmp = coro;
coro_tmp->arg = arg;
register void *top = coro->stack_top - ((unsigned long long)coro->stack_top % 16) - 16;
asm volatile(
"mov %[rs], %%rsp \n"
: [rs] "+r" (top) ::
);
asm volatile(
"mov %[rs], %%rbp \n"
: [rs] "+r" (top) ::
);
execute_coro(coro_tmp);
} else if (coro->status == COROUTINE_STATUS_SUSPENDED) { // coroutine was started before
coro->status = COROUTINE_STATUS_RUNNING;
longjmp(coro->env_yield, 1);
} else {return NULL;}
}
void* coroutine_yield(void* arg) {
int val;
coroutine_t coro = stack_pop(&coro_stack);
if (coro == NULL) {
errno = EOPNOTSUPP;
return NULL;
}
coro->arg = arg;
coro->status = COROUTINE_STATUS_SUSPENDED;
val = setjmp(coro->env_yield);
((_JUMP_BUFFER*)coro->env_yield)->Frame = 0; // Windows needs this for some reason to prevent crashes
if (val) { // function resumed
return coro->arg;
} else { // yield time
longjmp(coro->env_resume, 1);
}
}
coroutine_t coroutine_running() {
return stack_read(&coro_stack);
}
const char * coroutine_status(coroutine_t coro) {
switch (coro->status) {
case COROUTINE_STATUS_NEW: case COROUTINE_STATUS_SUSPENDED: return "suspended";
case COROUTINE_STATUS_RUNNING: return "running";
case COROUTINE_STATUS_NORMAL: return "normal";
case COROUTINE_STATUS_DEAD: return "dead";
default: return "unknown";
}
}
typedef struct coroutine * coroutine_t;
coroutine_t coroutine_create(void*(*fn)(void*));
void coroutine_delete(coroutine_t coro);
void* coroutine_resume(coroutine_t coro, void* arg);
void* coroutine_yield(void* arg);
coroutine_t coroutine_running();
const char * coroutine_status(coroutine_t coro);
#include "coroutine.h"
#include <stdio.h>
#include <string.h>
#include <assert.h>
#include <stdlib.h>
/** Adapted from http://www.computercraft.info/wiki/Coroutine_(API) **/
void print(int num) { printf("%d\n", num); }
// These variables will hold the coroutine objects.
coroutine_t c1 = NULL, c2 = NULL;
// This function will be the body of the first coroutine.
void* f1(void* x) {
void* value;
int* num = (int*)malloc(sizeof(int));
// The main code, below, starts c1 first, passing 1.
// Thus c1 should be running, c2 should be suspended (not started yet), and x should receive the value from main, 1.
// STEP 2: Check these assumptions.
print(2);
assert(strcmp(coroutine_status(c1), "running") == 0);
assert(strcmp(coroutine_status(c2), "suspended") == 0);
assert(*(int*)x == 1);
// STEP 3: Run c2 until it yields, start it off with the value 2.
print(3);
*num = 2;
value = coroutine_resume(c2, num);
assert(errno == 0);
// In step 6, c2 yields and passes back the value 3.
// Then c2, having yielded, should be suspended, and c1 should be running again.
// STEP 7: Check all that.
print(7);
assert(strcmp(coroutine_status(c1), "running") == 0);
assert(strcmp(coroutine_status(c2), "suspended") == 0);
assert(*(int*)value == 3);
// STEP 8: Run c2 again, passing it the value 4.
// Since c2 has already run before, this time the 4 will come out of the yield() instead of appear as a function parameter.
print(8);
*num = 4;
value = coroutine_resume(c2, num);
// In step 10, c2 yields and passes back the value 5.
// Then c2, having yielded, should be suspended, and c1 should be running again.
// STEP 11: Check all that.
print(11);
assert(strcmp(coroutine_status(c1), "running") == 0);
assert(strcmp(coroutine_status(c2), "suspended") == 0);
assert(*(int*)value == 5);
// STEP 12: Yield, turning c1 back into a suspended coroutine and continuing execution of the main program, passing back value 6.
print(12);
*num = 6;
value = coroutine_yield(num);
free(num);
return NULL;
}
// This function will be the body of the second coroutine.
void* f2(void* x) {
void* value;
int * num = (int*)malloc(sizeof(int));
// The first time c2 is run, it is run from f1, which passes 2.
// Thus c1 should be normal (as we are nested inside it), c2 should be running, and x should receive the value from f1, 2.
// STEP 4: Check these assumptions.
print(4);
assert(strcmp(coroutine_status(c1), "normal") == 0);
assert(strcmp(coroutine_status(c2), "running") == 0);
assert(*(int*)x == 2);
// Because c1 is not suspended, we should not be able to resume it.
// STEP 5: Check this is correct.
print(5);
value = coroutine_resume(c1, NULL);
assert(errno != 0);
assert(strcmp((const char*)value, "cannot resume normal coroutine") == 0);
// STEP 6: Yield, turning c2 back into a suspended coroutine and continuing execution of c1, passing back value 3.
print(6);
*num = 3;
value = coroutine_yield(num);
// In step 8, c1 resumed c2 again and passed the value 4.
// c2 should now be running again, and c1 normal again.
// STEP 9: Check all that.
print(9);
assert(strcmp(coroutine_status(c1), "normal") == 0);
assert(strcmp(coroutine_status(c2), "running") == 0);
assert(*(int*)value == 4);
// STEP 10: Yield, turning c2 back into a suspended coroutine and continuing execution of c1, passing back value 5.
print(10);
*num = 5;
value = coroutine_yield(num);
// In step 14, the main program resumed c2 directly and passed the value 7, bypassing c1.
// So c2 should be running and c1 should be suspended.
// STEP 15: Check all that.
print(15);
assert(strcmp(coroutine_status(c1), "suspended") == 0);
assert(strcmp(coroutine_status(c2), "running") == 0);
assert(*(int*)value == 7);
// STEP 16: Now die, returning value 8 to the main program.
print(16);
*num = 8;
return num;
}
int main() {
void* value;
int* num = (int*)malloc(sizeof(int));
// Construct the two coroutines.
c1 = coroutine_create(f1);
c2 = coroutine_create(f2);
// Newly constructed coroutines are always suspended.
assert(strcmp(coroutine_status(c1), "suspended") == 0);
assert(strcmp(coroutine_status(c2), "suspended") == 0);
// STEP 1: Run c1 until it yields, starting it off with the value 1.
print(1);
*num = 1;
value = coroutine_resume(c1, num);
if (errno) printf("%s\n", value);
assert(errno == 0);
// In step 12, c1 should have yielded and returned the value 6.
// So now c1 and c2 should both be suspended.
// STEP 13: Check all that.
print(13);
assert(strcmp(coroutine_status(c1), "suspended") == 0);
assert(strcmp(coroutine_status(c2), "suspended") == 0);
assert(*(int*)value == 6);
// STEP 14: Run c2 directly, NOT nested inside c1 this time, passing it the value 7.
print(14);
*num = 7;
value = coroutine_resume(c2, num);
assert(errno == 0);
// c2 should have exited normally, returning the value 8.
// Therefore c1 should still be suspended, but c2 should be dead.
// STEP 17: Check all that.
print(17);
assert(strcmp(coroutine_status(c1), "suspended") == 0);
assert(strcmp(coroutine_status(c2), "dead") == 0);
assert(*(int*)value == 8);
free(num);
free(value);
coroutine_delete(c1);
coroutine_delete(c2);
return 0;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment