diff options
Diffstat (limited to 'libcr/coroutine.c')
-rw-r--r-- | libcr/coroutine.c | 366 |
1 files changed, 221 insertions, 145 deletions
diff --git a/libcr/coroutine.c b/libcr/coroutine.c index 41d987e..726b732 100644 --- a/libcr/coroutine.c +++ b/libcr/coroutine.c @@ -4,13 +4,18 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -#include <assert.h> #include <setjmp.h> /* for setjmp(), longjmp(), jmp_buf */ #include <stdint.h> /* for uint8_t */ -#include <stdio.h> /* for printf(), fprintf(), stderr */ #include <stdlib.h> /* for aligned_alloc(), free() */ +#include <string.h> /* for strncpy(), memset() */ + +#include <libmisc/assert.h> + +#define LOG_NAME COROUTINE +#include <libmisc/log.h> #include <libcr/coroutine.h> +#undef COROUTINE /* Configuration **************************************************************/ @@ -19,6 +24,9 @@ #ifndef CONFIG_COROUTINE_DEFAULT_STACK_SIZE #error config.h must define CONFIG_COROUTINE_DEFAULT_STACK_SIZE #endif +#ifndef CONFIG_COROUTINE_NAME_LEN + #error config.h must define CONFIG_COROUTINE_NAME_LEN +#endif #ifndef CONFIG_COROUTINE_NUM #error config.h must define CONFIG_COROUTINE_NUM #endif @@ -35,23 +43,24 @@ #error config.h must define CONFIG_COROUTINE_VALGRIND #endif +/* Implementation *************************************************************/ + #if CONFIG_COROUTINE_VALGRIND #include <valgrind/valgrind.h> #endif -/* Implementation *************************************************************/ - /* * Portability notes: * - * - It uses GCC `__attribute__`s, and the GNUC ({ ... }) statement - * exprs extension. + * - It uses GCC `gnu::` attributes, and the GNUC `({ ... })` + * statement exprs extension. * * - It has a small bit of platform-specific code in the "platform * support" section. Other than this, it should be portable to * other platforms CPUs. It currently contains implementations for - * __x86_64__ (assumes POSIX) and __arm__ (assumes bare-metal), and - * should be fairly easy to add implementations for other platforms. + * __unix__ and __ARM_EABI__ "operating systems" on __x86_64__ and + * __ARM_ARCH_6M__ CPUs, and should be fairly easy to add + * implementations for other platforms. * * - It uses setjmp()/longjmp() in "unsafe" ways. POSIX-2017 * longjmp(3p) says @@ -117,7 +126,7 @@ * no longer exists. */ -#define ALWAYS_INLINE inline __attribute__((always_inline)) +#define ALWAYS_INLINE [[gnu::always_inline]] inline /* platform support ***********************************************************/ @@ -125,73 +134,113 @@ * able to run it on my x86-64 GNU/Linux laptop is useful for debugging. */ #define CR_PLAT_STACK_ALIGNMENT \ - ({ __attribute__((aligned)) void fn(void) {}; __alignof__(fn); }) + ({ [[gnu::aligned]] void fn(void) {}; __alignof__(fn); }) #if 0 { /* bracket to get Emacs indentation to work how I want */ #endif /*==================================================================== - * Wrappers for setjmp()/longjmp() that do *not* save the - * interrupt mask. */ -#if __unix__ - /* On a *NIX OS, we use signals as interrupts. POSIX leaves - * it implementation-defined whether setjmp()/longjmp() save - * the signal mask; while glibc does not save it, let's not - * rely on that. */ - #define cr_plat_setjmp(env) sigsetjmp(env, 0) - #define cr_plat_longjmp(env, val) siglongjmp(env, val) -#elif __NEWLIB__ - /* newlib does not have sigsetjmp()/sigsetlongjmp(), but - * setjmp()/longjmp() do not save the interrupt mask, * so we - * can use them directly. */ - #define cr_plat_setjmp(env) setjmp(env) - #define cr_plat_longjmp(env, val) longjmp(env, val) -#else - #error unsupported platform (not __unix__, not __NEWLIB__) -#endif - -/*==================================================================== * Interrupt management routines. */ #if __unix__ - #include <signal.h> /* for sig*, SIG_* */ - #include <unistd.h> /* for pause() */ + #include <signal.h> /* for sig*, SIG* */ + + /* For a signal to be *in* the mask means that the signal is + * *blocked*. */ + + bool cr_is_in_intrhandler(void) { + sigset_t cur_mask; + sigfillset(&cur_mask); + sigprocmask(0, NULL, &cur_mask); + if (sigismember(&cur_mask, SIGHUP)) + /* Interrupts are disabled, so we cannot be in + * an interrupt handler. */ + return false; + for (int sig = SIGRTMIN; sig <= SIGRTMAX; sig++) + if (sigismember(&cur_mask, sig)) + return true; + return false; + } + static inline bool _cr_plat_are_interrupts_enabled(void) { + assert(!cr_is_in_intrhandler()); + sigset_t cur_mask; + sigfillset(&cur_mask); + sigprocmask(0, NULL, &cur_mask); + return !sigismember(&cur_mask, SIGHUP); + } static inline void cr_plat_wait_for_interrupt(void) { - pause(); + assert(!cr_is_in_intrhandler()); + assert(!_cr_plat_are_interrupts_enabled()); + sigset_t set; + sigemptyset(&set); + sigsuspend(&set); + + sigfillset(&set); + sigprocmask(SIG_SETMASK, &set, NULL); } - void _cr_plat_disable_interrupts(void) { - sigset_t all; + bool _cr_plat_save_and_disable_interrupts(void) { + assert(!cr_is_in_intrhandler()); + sigset_t all, old; sigfillset(&all); - sigprocmask(SIG_BLOCK, &all, NULL); + sigprocmask(SIG_SETMASK, &all, &old); + return !sigismember(&old, SIGHUP); } void _cr_plat_enable_interrupts(void) { - sigset_t all; - sigfillset(&all); - sigprocmask(SIG_UNBLOCK, &all, NULL); + assert(!cr_is_in_intrhandler()); + assert(!_cr_plat_are_interrupts_enabled()); + sigset_t zero; + sigemptyset(&zero); + sigprocmask(SIG_SETMASK, &zero, NULL); + } +#elif __ARM_ARCH_6M__ && __ARM_EABI__ + bool cr_is_in_intrhandler(void) { + uint32_t isr_number; + asm volatile ("mrs %0, ipsr" + : /* %0 */"=l"(isr_number) + ); + return isr_number != 0; + } + ALWAYS_INLINE static bool _cr_plat_are_interrupts_enabled(void) { + assert(!cr_is_in_intrhandler()); + uint32_t primask; + asm volatile ("mrs %0, PRIMASK" + : /* %0 */"=l"(primask) + ); + return primask == 0; } -#elif __arm__ - /* Assume bare-metal if !__unix__. */ - static ALWAYS_INLINE void cr_plat_wait_for_interrupt(void) { - asm volatile ("wfi":::"memory"); + + ALWAYS_INLINE static void cr_plat_wait_for_interrupt(void) { + assert(!cr_is_in_intrhandler()); + assert(!_cr_plat_are_interrupts_enabled()); + asm volatile ("wfi\n" + "cpsie i\n" + "isb\n" + "cpsid i" + :::"memory"); } - void _cr_plat_disable_interrupts(void) { - asm volatile ("cpsid i":::"memory"); + bool _cr_plat_save_and_disable_interrupts(void) { + assert(!cr_is_in_intrhandler()); + bool were_enabled = _cr_plat_are_interrupts_enabled(); + asm volatile ("cpsid i"); + return were_enabled; } void _cr_plat_enable_interrupts(void) { - asm volatile ("cpsie i":::"memory"); + assert(!cr_is_in_intrhandler()); + assert(!_cr_plat_are_interrupts_enabled()); + asm volatile ("cpsie i"); } #else - #error unsupported platform (not __unix__, not bare-metal __arm__) + #error unsupported platform (not __unix__, not __ARM_ARCH_6M__ && __ARM_EABI__) #endif /*==================================================================== * Stack management routines. */ -#if __arm__ +#if __ARM_ARCH_6M__ #define CR_PLAT_STACK_GROWS_DOWNWARD 1 #if CONFIG_COROUTINE_MEASURE_STACK - static ALWAYS_INLINE uintptr_t cr_plat_get_sp(void) { + ALWAYS_INLINE static uintptr_t cr_plat_get_sp(void) { uintptr_t sp; asm volatile ("mov %0, sp":"=r"(sp)); return sp; @@ -209,13 +258,13 @@ "mov sp, %1\n\t" /* [sp = stack] */ "mov r0, %3\n\t" /* [arg0 = args] */ "blx %2\n\t" /* [fn()] */ - "ldr r0, %0\n\t" /* [sp = staved_sp */ + "ldr r0, %0\n\t" /* [sp = saved_sp */ "mov sp, r0" /* ] */ : : /* %0 */"m"(saved_sp), - /* %1 */"r"(stack), - /* %2 */"r"(fn), - /* %3 */"r"(args) + /* %1 */"l"(stack), + /* %2 */"l"(fn), + /* %3 */"l"(args) : "r0" ); } @@ -223,7 +272,7 @@ #define CR_PLAT_STACK_GROWS_DOWNWARD 1 #if CONFIG_COROUTINE_MEASURE_STACK - static ALWAYS_INLINE uintptr_t cr_plat_get_sp(void) { + ALWAYS_INLINE static uintptr_t cr_plat_get_sp(void) { uintptr_t sp; asm volatile ("movq %%rsp, %0":"=r"(sp)); return sp; @@ -247,7 +296,48 @@ ); } #else - #error unsupported platform (not __arm__, not __x86__) + #error unsupported CPU (not __ARM_ARCH_6M__, not __x86_64__) +#endif + +/*==================================================================== + * Wrappers for setjmp()/longjmp() that: + * 1. Allow us to inspect the buffer. + * 2. Do *not* save the interrupt mask. + */ + typedef struct { + jmp_buf raw; + #if CONFIG_COROUTINE_MEASURE_STACK + /* We aught to be able to get sp out of the raw + * `jmp_buf`, but libc authors insist on jmp_buf being + * opaque, glibc going as far as to xor it with a + * secret to obfuscate it! */ + uintptr_t sp; + #endif + } cr_plat_jmp_buf; + static inline void _cr_plat_setjmp_pre(cr_plat_jmp_buf *env) { + #if CONFIG_COROUTINE_MEASURE_STACK + env->sp = cr_plat_get_sp(); + #endif + } + /* cr_plat_setjmp *NEEDS* to be a preprocessor macro rather + * than a real function, because [[gnu::returns_twice]] + * doesn't work. + * https://gcc.gnu.org/bugzilla/show_bug.cgi?id=117469 */ +#if __unix__ + /* On __unix__, we use POSIX real-time signals as interrupts. + * POSIX leaves it implementation-defined whether + * setjmp()/longjmp() save the signal mask; while glibc does + * not save it, let's not rely on that. */ + #define cr_plat_setjmp(env) ({ _cr_plat_setjmp_pre(env); sigsetjmp((env)->raw, 0); }) + [[noreturn]] static void cr_plat_longjmp(cr_plat_jmp_buf *env, int val) { siglongjmp(env->raw, val); } +#elif __NEWLIB__ + /* newlib does not have sigsetjmp()/sigsetlongjmp(), but + * setjmp()/longjmp() do not save the interrupt mask, so we + * can use them directly. */ + #define cr_plat_setjmp(env) ({ _cr_plat_setjmp_pre(env); setjmp((env)->raw); }) + [[noreturn]] static void cr_plat_longjmp(cr_plat_jmp_buf *env, int val) { longjmp(env->raw, val); } +#else + #error unsupported platform (not __unix__, not __NEWLIB__) #endif #if 0 @@ -267,26 +357,19 @@ enum coroutine_state { CR_NONE = 0, /* this slot in the table is empty */ CR_INITIALIZING, /* running, before cr_begin() */ CR_RUNNING, /* running, after cr_begin() */ - CR_PRE_RUNNABLE, /* running, after cr_unpause_from_intrhandler() - * but before cr_pause() */ CR_RUNNABLE, /* not running, but runnable */ CR_PAUSED, /* not running, and not runnable */ }; struct coroutine { volatile enum coroutine_state state; - jmp_buf env; -#if CONFIG_COROUTINE_MEASURE_STACK - /* We aught to be able to get this out of .env, but libc - * authors insist on jmp_buf being opaque, glibc going as far - * as to xor it with a secret ot obfuscate it! */ - uintptr_t sp; -#endif + cr_plat_jmp_buf env; size_t stack_size; void *stack; #if CONFIG_COROUTINE_VALGRIND unsigned stack_id; #endif + char name[CONFIG_COROUTINE_NAME_LEN]; }; /* constants ******************************************************************/ @@ -295,7 +378,6 @@ const char *coroutine_state_strs[] = { [CR_NONE] = "CR_NONE", [CR_INITIALIZING] = "CR_INITIALIZING", [CR_RUNNING] = "CR_RUNNING", - [CR_PRE_RUNNABLE] = "CR_PRE_RUNNABLE", [CR_RUNNABLE] = "CR_RUNNABLE", [CR_PAUSED] = "CR_PAUSED", }; @@ -317,8 +399,8 @@ static const uint8_t stack_pattern[] = { /* global variables ***********************************************************/ -static jmp_buf coroutine_add_env; -static jmp_buf coroutine_main_env; +static cr_plat_jmp_buf coroutine_add_env; +static cr_plat_jmp_buf coroutine_main_env; /* * Invariants (and non-invariants): @@ -355,25 +437,6 @@ static cid_t coroutine_running = 0; /* utility functions **********************************************************/ -#define errorf(...) fprintf(stderr, "error: " __VA_ARGS__) -#define infof(...) printf("info: " __VA_ARGS__) -#if CONFIG_COROUTINE_DEBUG - #define debugf(...) printf("dbg: " __VA_ARGS__) -#else - #define debugf(...) -#endif - -#ifdef __GLIBC__ - #define assertf(expr, ...) ({ \ - if (!(expr)) { \ - errorf("assertion: " __VA_ARGS__); \ - __assert_fail(#expr, __FILE__, __LINE__, __func__); \ - } \ - }) -#else - #define assertf(expr, ...) assert(expr) -#endif - static inline const char* coroutine_state_str(enum coroutine_state state) { assert(state < ARRAY_LEN(coroutine_state_strs)); return coroutine_state_strs[state]; @@ -415,7 +478,9 @@ static inline void assert_cid(cid_t cid) { /* coroutine_add() ************************************************************/ -cid_t coroutine_add_with_stack_size(size_t stack_size, cr_fn_t fn, void *args) { +cid_t coroutine_add_with_stack_size(size_t stack_size, + const char *name, + cr_fn_t fn, void *args) { static cid_t last_created = 0; cid_t parent = coroutine_running; @@ -423,8 +488,8 @@ cid_t coroutine_add_with_stack_size(size_t stack_size, cr_fn_t fn, void *args) { assert_cid_state(parent, state == CR_RUNNING); assert(stack_size); assert(fn); - debugf("coroutine_add_with_stack_size(%zu, %p, %p)...\n", - stack_size, fn, args); + debugf("coroutine_add_with_stack_size(%zu, \"%s\", %p, %p)...", + stack_size, name, fn, args); cid_t child; { @@ -437,10 +502,15 @@ cid_t coroutine_add_with_stack_size(size_t stack_size, cr_fn_t fn, void *args) { return 0; found: } - debugf("...child=%zu\n", child); + debugf("...child=%zu", child); last_created = child; + if (name) + strncpy(coroutine_table[child-1].name, name, sizeof(coroutine_table[child-1].name)); + else + memset(coroutine_table[child-1].name, 0, sizeof(coroutine_table[child-1].name)); + coroutine_table[child-1].stack_size = stack_size; coroutine_table[child-1].stack = aligned_alloc(CR_PLAT_STACK_ALIGNMENT, stack_size); @@ -457,7 +527,7 @@ cid_t coroutine_add_with_stack_size(size_t stack_size, cr_fn_t fn, void *args) { coroutine_running = child; coroutine_table[child-1].state = CR_INITIALIZING; - if (!cr_plat_setjmp(coroutine_add_env)) { /* point=a */ + if (!cr_plat_setjmp(&coroutine_add_env)) { /* point=a */ void *stack_base = coroutine_table[child-1].stack #if CR_PLAT_STACK_GROWS_DOWNWARD + stack_size @@ -466,45 +536,50 @@ cid_t coroutine_add_with_stack_size(size_t stack_size, cr_fn_t fn, void *args) { + STACK_GUARD_SIZE #endif ; - debugf("...stack =%p\n", coroutine_table[child-1].stack); - debugf("...stack_base=%p\n", stack_base); + debugf("...stack =%p", coroutine_table[child-1].stack); + debugf("...stack_base=%p", stack_base); /* run until cr_begin() */ cr_plat_call_with_stack(stack_base, fn, args); - __builtin_unreachable(); /* should cr_begin() instead of returning */ + assert_notreached("should cr_begin() instead of returning"); } assert_cid_state(child, state == CR_RUNNABLE); if (parent) assert_cid_state(parent, state == CR_RUNNING); + /* Restore interrupts because cr_begin() disables interrupts + * before the context switch. XXX: This assumes that + * interrupts were enabled when _add() was called, which we + * didn't actually check. */ + cr_restore_interrupts(true); coroutine_running = parent; return child; } -cid_t coroutine_add(cr_fn_t fn, void *args) { +cid_t coroutine_add(const char *name, cr_fn_t fn, void *args) { return coroutine_add_with_stack_size( - CONFIG_COROUTINE_DEFAULT_STACK_SIZE, fn, args); + CONFIG_COROUTINE_DEFAULT_STACK_SIZE, name, fn, args); } /* coroutine_main() ***********************************************************/ -void coroutine_main(void) { - debugf("coroutine_main()\n"); - cr_disable_interrupts(); +[[noreturn]] void coroutine_main(void) { + debugf("coroutine_main()"); + bool saved = cr_save_and_disable_interrupts(); + assert(saved); + assert(!cr_is_in_intrhandler()); coroutine_running = 0; for (;;) { cid_t next; while ( !((next = coroutine_ringbuf_pop())) ) { /* No coroutines are runnable, wait for an interrupt * to change that. */ - cr_enable_interrupts(); cr_plat_wait_for_interrupt(); - cr_disable_interrupts(); } - if (!cr_plat_setjmp(coroutine_main_env)) { /* point=b */ + if (!cr_plat_setjmp(&coroutine_main_env)) { /* point=b */ coroutine_running = next; coroutine_table[coroutine_running-1].state = CR_RUNNING; - cr_plat_longjmp(coroutine_table[coroutine_running-1].env, 1); /* jump to point=c */ + cr_plat_longjmp(&coroutine_table[coroutine_running-1].env, 1); /* jump to point=c */ } /* This is where we jump to from cr_exit(), and from * nowhere else. */ @@ -520,15 +595,15 @@ void coroutine_main(void) { /* cr_*() *********************************************************************/ void cr_begin(void) { - debugf("cid=%zu: cr_begin()\n", coroutine_running); + debugf("cid=%zu: cr_begin()", coroutine_running); assert_cid_state(coroutine_running, state == CR_INITIALIZING); + bool saved = cr_save_and_disable_interrupts(); coroutine_table[coroutine_running-1].state = CR_RUNNABLE; coroutine_ringbuf_push(coroutine_running); - coroutine_table[coroutine_running-1].sp = cr_plat_get_sp(); - if (!cr_plat_setjmp(coroutine_table[coroutine_running-1].env)) /* point=c1 */ - cr_plat_longjmp(coroutine_add_env, 1); /* jump to point=a */ - cr_enable_interrupts(); + if (!cr_plat_setjmp(&coroutine_table[coroutine_running-1].env)) /* point=c1 */ + cr_plat_longjmp(&coroutine_add_env, 1); /* jump to point=a */ + cr_restore_interrupts(saved); } static inline void _cr_yield() { @@ -536,9 +611,7 @@ static inline void _cr_yield() { while ( !((next = coroutine_ringbuf_pop())) ) { /* No coroutines are runnable, wait for an interrupt * to change that. */ - cr_enable_interrupts(); cr_plat_wait_for_interrupt(); - cr_disable_interrupts(); } if (next == coroutine_running) { @@ -546,72 +619,73 @@ static inline void _cr_yield() { return; } - coroutine_table[coroutine_running-1].sp = cr_plat_get_sp(); - if (!cr_plat_setjmp(coroutine_table[coroutine_running-1].env)) { /* point=c2 */ + if (!cr_plat_setjmp(&coroutine_table[coroutine_running-1].env)) { /* point=c2 */ coroutine_running = next; coroutine_table[coroutine_running-1].state = CR_RUNNING; - cr_plat_longjmp(coroutine_table[coroutine_running-1].env, 1); /* jump to point=c */ + cr_plat_longjmp(&coroutine_table[coroutine_running-1].env, 1); /* jump to point=c */ } } void cr_yield(void) { - debugf("cid=%zu: cr_yield()\n", coroutine_running); + debugf("cid=%zu: cr_yield()", coroutine_running); + assert(!cr_is_in_intrhandler()); assert_cid_state(coroutine_running, state == CR_RUNNING); - cr_disable_interrupts(); + bool saved = cr_save_and_disable_interrupts(); coroutine_table[coroutine_running-1].state = CR_RUNNABLE; coroutine_ringbuf_push(coroutine_running); _cr_yield(); - cr_enable_interrupts(); + cr_restore_interrupts(saved); } void cr_pause_and_yield(void) { - debugf("cid=%zu: cr_pause_and_yield()\n", coroutine_running); - assert_cid_state(coroutine_running, state == CR_RUNNING || state == CR_PRE_RUNNABLE); - - cr_disable_interrupts(); - if (coroutine_table[coroutine_running-1].state == CR_PRE_RUNNABLE) { - coroutine_table[coroutine_running-1].state = CR_RUNNABLE; - coroutine_ringbuf_push(coroutine_running); - } else - coroutine_table[coroutine_running-1].state = CR_PAUSED; + debugf("cid=%zu: cr_pause_and_yield()", coroutine_running); + assert(!cr_is_in_intrhandler()); + assert_cid_state(coroutine_running, state == CR_RUNNING); + + bool saved = cr_save_and_disable_interrupts(); + coroutine_table[coroutine_running-1].state = CR_PAUSED; _cr_yield(); - cr_enable_interrupts(); + cr_restore_interrupts(saved); } -void cr_exit(void) { - debugf("cid=%zu: cr_exit()\n", coroutine_running); +[[noreturn]] void cr_exit(void) { + debugf("cid=%zu: cr_exit()", coroutine_running); + assert(!cr_is_in_intrhandler()); assert_cid_state(coroutine_running, state == CR_RUNNING); - cr_disable_interrupts(); + (void)cr_save_and_disable_interrupts(); coroutine_table[coroutine_running-1].state = CR_NONE; - cr_plat_longjmp(coroutine_main_env, 1); /* jump to point=b */ + cr_plat_longjmp(&coroutine_main_env, 1); /* jump to point=b */ } -void cr_unpause(cid_t cid) { - debugf("cr_unpause(%zu)\n", cid); +static void _cr_unpause(cid_t cid) { assert_cid_state(cid, state == CR_PAUSED); coroutine_table[cid-1].state = CR_RUNNABLE; coroutine_ringbuf_push(cid); } +void cr_unpause(cid_t cid) { + debugf("cr_unpause(%zu)", cid); + assert(!cr_is_in_intrhandler()); + assert_cid_state(coroutine_running, state == CR_RUNNING); + + bool saved = cr_save_and_disable_interrupts(); + _cr_unpause(cid); + cr_restore_interrupts(saved); +} + void cr_unpause_from_intrhandler(cid_t cid) { - debugf("cr_unpause_from_intrhandler(%zu)\n", cid); - assert_cid_state(cid, state == CR_RUNNING || state == CR_PAUSED); - - if (coroutine_table[cid-1].state == CR_RUNNING) { - assert(cid == coroutine_running); - debugf("... raced, deferring unpause\n"); - coroutine_table[cid-1].state = CR_PRE_RUNNABLE; - } else { - debugf("... actual unpause\n"); - coroutine_table[cid-1].state = CR_RUNNABLE; - coroutine_ringbuf_push(cid); - } + debugf("cr_unpause_from_intrhandler(%zu)", cid); + assert(cr_is_in_intrhandler()); + + _cr_unpause(cid); } cid_t cr_getcid(void) { + assert(!cr_is_in_intrhandler()); + assert_cid_state(coroutine_running, state == CR_RUNNING); return coroutine_running; } @@ -647,8 +721,10 @@ void cr_cid_info(cid_t cid, struct cr_cid_info *ret) { uintptr_t sp; if (cid == coroutine_running) sp = cr_plat_get_sp(); + else if (coroutine_table[cid-1].state == CR_RUNNING) + sp = coroutine_add_env.sp; else - sp = coroutine_table[cid-1].sp; + sp = coroutine_table[cid-1].env.sp; assert(sp); uintptr_t sb = (uintptr_t)coroutine_table[cid-1].stack; #if CR_PLAT_STACK_GROWS_DOWNWARD |