Created
July 11, 2020 07:42
-
-
Save MCJack123/a685752b3e094dc24805d5f4953d9d4e to your computer and use it in GitHub Desktop.
Unsafe, untrustworthy coroutine implementation in C (x64, GCC only, tested on Windows)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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"; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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