diff options
Diffstat (limited to 'libcr')
-rw-r--r-- | libcr/CMakeLists.txt | 37 | ||||
-rw-r--r-- | libcr/coroutine.c | 636 | ||||
-rw-r--r-- | libcr/include/libcr/coroutine.h | 104 | ||||
-rw-r--r-- | libcr/tests/test_matrix.c | 24 | ||||
-rw-r--r-- | libcr/tests/test_matrix/config.h | 14 |
5 files changed, 565 insertions, 250 deletions
diff --git a/libcr/CMakeLists.txt b/libcr/CMakeLists.txt index ae7c8fe..80a4ece 100644 --- a/libcr/CMakeLists.txt +++ b/libcr/CMakeLists.txt @@ -1,13 +1,44 @@ -# libcr/CMakeLists.txt - TODO +# libcr/CMakeLists.txt - Simple embeddable coroutine implementation # -# Copyright (C) 2024 Luke T. Shumaker <lukeshu@lukeshu.com> +# Copyright (C) 2024-2025 Luke T. Shumaker <lukeshu@lukeshu.com> # SPDX-License-Identifier: AGPL-3.0-or-later add_library(libcr INTERFACE) -target_include_directories(libcr SYSTEM INTERFACE ${CMAKE_CURRENT_LIST_DIR}/include) +target_include_directories(libcr PUBLIC INTERFACE ${CMAKE_CURRENT_LIST_DIR}/include) target_sources(libcr INTERFACE coroutine.c ) +target_link_libraries(libcr INTERFACE + libmisc +) target_compile_options(libcr INTERFACE -fno-split-stack ) + +set(cfg_matrix + "CONFIG_COROUTINE_MEASURE_STACK;[0;1]" + "CONFIG_COROUTINE_PROTECT_STACK;[0;1]" + "CONFIG_COROUTINE_DEBUG;[0;1]" + "CONFIG_COROUTINE_VALGRIND;[0;1]" + "CONFIG_COROUTINE_GDB;[0;1]" +) +function(add_libcr_matrix_test n defs) + add_executable("test_matrix${n}" "tests/test_matrix.c") + target_link_libraries("test_matrix${n}" "libcr") + target_include_directories("test_matrix${n}" PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/tests/test_matrix) + target_compile_definitions("test_matrix${n}" PUBLIC "${defs}") + if ("CONFIG_COROUTINE_VALGRIND=1" IN_LIST defs) + add_test( + NAME "libcr/test_matrix${n}" + COMMAND valgrind --error-exitcode=2 "./test_matrix${n}" + ) + else() + add_test( + NAME "libcr/test_matrix${n}" + COMMAND "./test_matrix${n}" + ) + endif() +endfunction() +if (ENABLE_TESTS) + apply_matrix(add_libcr_matrix_test "${cfg_matrix}") +endif() diff --git a/libcr/coroutine.c b/libcr/coroutine.c index 41d987e..bf44219 100644 --- a/libcr/coroutine.c +++ b/libcr/coroutine.c @@ -1,57 +1,70 @@ /* libcr/coroutine.c - Simple embeddable coroutine implementation * - * Copyright (C) 2024 Luke T. Shumaker <lukeshu@lukeshu.com> + * Copyright (C) 2024-2025 Luke T. Shumaker <lukeshu@lukeshu.com> * 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> +#include <libmisc/macro.h> + +#define LOG_NAME COROUTINE +#include <libmisc/log.h> #include <libcr/coroutine.h> +#undef COROUTINE /* Configuration **************************************************************/ #include "config.h" -#ifndef CONFIG_COROUTINE_DEFAULT_STACK_SIZE - #error config.h must define CONFIG_COROUTINE_DEFAULT_STACK_SIZE +#ifndef CONFIG_COROUTINE_NAME_LEN + #error config.h must define CONFIG_COROUTINE_NAME_LEN (non-negative integer) #endif #ifndef CONFIG_COROUTINE_NUM - #error config.h must define CONFIG_COROUTINE_NUM + #error config.h must define CONFIG_COROUTINE_NUM (non-negative integer) #endif #ifndef CONFIG_COROUTINE_MEASURE_STACK - #error config.h must define CONFIG_COROUTINE_MEASURE_STACK + #error config.h must define CONFIG_COROUTINE_MEASURE_STACK (bool) #endif #ifndef CONFIG_COROUTINE_PROTECT_STACK - #error config.h must define CONFIG_COROUTINE_PROTECT_STACK + #error config.h must define CONFIG_COROUTINE_PROTECT_STACK (bool) #endif #ifndef CONFIG_COROUTINE_DEBUG - #error config.h must define CONFIG_COROUTINE_DEBUG + #error config.h must define CONFIG_COROUTINE_DEBUG (bool) #endif #ifndef CONFIG_COROUTINE_VALGRIND - #error config.h must define CONFIG_COROUTINE_VALGRIND + #error config.h must define CONFIG_COROUTINE_VALGRIND (bool) +#endif +#ifndef CONFIG_COROUTINE_GDB + #error config.h must define CONFIG_COROUTINE_GDB (bool) #endif +/* Enforce that CONFIG_COROUTINE_NUM is greater than 1, to work around + * https://gcc.gnu.org/bugzilla/show_bug.cgi?id=118212 */ +static_assert(CONFIG_COROUTINE_NUM > 1); + +/* 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,81 +130,151 @@ * no longer exists. */ -#define ALWAYS_INLINE inline __attribute__((always_inline)) - /* platform support ***********************************************************/ /* As part of sbc-harness, this only really needs to support ARM-32, but being * 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. */ + * Interrupt management routines. */ #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__) + #include <signal.h> /* for sig*, SIG* */ + + /* For a signal to be *in* the mask means that the signal is + * *blocked*. */ + + #define _CR_SIG_SENTINEL SIGURG + #if CONFIG_COROUTINE_GDB + #define _CR_SIG_GDB SIGWINCH + #endif + +#if CONFIG_COROUTINE_VALGRIND + /* Hack around a bug in Valgrind where it runs the + * sigsuspend(tmp_sigmask)-triggered handler function with the + * original mask, not the `tmp_mask`. */ + static bool _cr_plat_in_sigsuspend = false; #endif -/*==================================================================== - * Interrupt management routines. */ -#if __unix__ - #include <signal.h> /* for sig*, SIG_* */ - #include <unistd.h> /* for pause() */ + bool cr_plat_is_in_intrhandler(void) { +#if CONFIG_COROUTINE_VALGRIND + if (_cr_plat_in_sigsuspend) + return true; +#endif + sigset_t cur_mask; + sigprocmask(0, NULL, &cur_mask); + if (sigismember(&cur_mask, _CR_SIG_SENTINEL)) + /* 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_plat_is_in_intrhandler()); + sigset_t cur_mask; + sigfillset(&cur_mask); + sigprocmask(0, NULL, &cur_mask); + return !sigismember(&cur_mask, _CR_SIG_SENTINEL); + } static inline void cr_plat_wait_for_interrupt(void) { - pause(); + assert(!cr_plat_is_in_intrhandler()); + assert(!_cr_plat_are_interrupts_enabled()); + sigset_t set; + sigemptyset(&set); +#if CONFIG_COROUTINE_VALGRIND + _cr_plat_in_sigsuspend = true; +#endif + sigsuspend(&set); +#if CONFIG_COROUTINE_VALGRIND + _cr_plat_in_sigsuspend = false; +#endif } - void _cr_plat_disable_interrupts(void) { - sigset_t all; + bool _cr_plat_save_and_disable_interrupts(void) { + assert(!cr_plat_is_in_intrhandler()); + sigset_t all, old; sigfillset(&all); - sigprocmask(SIG_BLOCK, &all, NULL); + sigprocmask(SIG_SETMASK, &all, &old); + return !sigismember(&old, _CR_SIG_SENTINEL); } void _cr_plat_enable_interrupts(void) { - sigset_t all; - sigfillset(&all); - sigprocmask(SIG_UNBLOCK, &all, NULL); + assert(!cr_plat_is_in_intrhandler()); + assert(!_cr_plat_are_interrupts_enabled()); + sigset_t zero; + sigemptyset(&zero); + sigprocmask(SIG_SETMASK, &zero, NULL); + } + #if CONFIG_COROUTINE_GDB + static void _cr_gdb_intrhandler(int LM_UNUSED(sig)) {} + #endif + static void cr_plat_init(void) { + #if CONFIG_COROUTINE_GDB + int r; + struct sigaction action = { + .sa_handler = _cr_gdb_intrhandler, + }; + r = sigaction(_CR_SIG_GDB, &action, NULL); + assert(r == 0); + #endif + } +#elif __ARM_ARCH_6M__ && __ARM_EABI__ + bool cr_plat_is_in_intrhandler(void) { + uint32_t isr_number; + asm volatile ("mrs %0, ipsr" + : /* %0 */"=l"(isr_number) + ); + return isr_number != 0; } -#elif __arm__ - /* Assume bare-metal if !__unix__. */ - static ALWAYS_INLINE void cr_plat_wait_for_interrupt(void) { - asm volatile ("wfi":::"memory"); + LM_ALWAYS_INLINE static bool _cr_plat_are_interrupts_enabled(void) { + assert(!cr_plat_is_in_intrhandler()); + uint32_t primask; + asm volatile ("mrs %0, PRIMASK" + : /* %0 */"=l"(primask) + ); + return primask == 0; } - void _cr_plat_disable_interrupts(void) { - asm volatile ("cpsid i":::"memory"); + + LM_ALWAYS_INLINE static void cr_plat_wait_for_interrupt(void) { + assert(!cr_plat_is_in_intrhandler()); + assert(!_cr_plat_are_interrupts_enabled()); + asm volatile ("wfi\n" + "cpsie i\n" + "isb\n" + "cpsid i" + :::"memory"); + } + bool _cr_plat_save_and_disable_interrupts(void) { + assert(!cr_plat_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_plat_is_in_intrhandler()); + assert(!_cr_plat_are_interrupts_enabled()); + asm volatile ("cpsie i"); } + static void cr_plat_init(void) {} #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) { + LM_ALWAYS_INLINE static uintptr_t cr_plat_get_sp(void) { uintptr_t sp; asm volatile ("mov %0, sp":"=r"(sp)); return sp; @@ -209,13 +292,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 +306,7 @@ #define CR_PLAT_STACK_GROWS_DOWNWARD 1 #if CONFIG_COROUTINE_MEASURE_STACK - static ALWAYS_INLINE uintptr_t cr_plat_get_sp(void) { + LM_ALWAYS_INLINE static uintptr_t cr_plat_get_sp(void) { uintptr_t sp; asm volatile ("movq %%rsp, %0":"=r"(sp)); return sp; @@ -247,46 +330,78 @@ ); } #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. + */ + #include <setjmp.h> /* for setjmp(), longjmp(), jmp_buf */ + 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 void _cr_plat_setjmp_pre(cr_plat_jmp_buf *env [[gnu::unused]]) { + #if CONFIG_COROUTINE_MEASURE_STACK + env->sp = cr_plat_get_sp(); + #endif + } + #if CONFIG_COROUTINE_MEASURE_STACK + static uintptr_t cr_plat_setjmp_get_sp(cr_plat_jmp_buf *env) { return env->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 } #endif -/* preprocessor macros ********************************************************/ - -/** Return `n` rounded up to the nearest multiple of `d` */ -#define ROUND_UP(n, d) ( ( ((n)+(d)-1) / (d) ) * (d) ) -#define ARRAY_LEN(arr) (sizeof(arr)/sizeof((arr)[0])) -#define NEXT_POWER_OF_2(x) ((1ULL)<<((sizeof(unsigned long long)*8)-__builtin_clzll(x))) - /* types **********************************************************************/ -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 { + /* 1. state *************************************************/ 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 + + /* 2. name **************************************************/ + [[gnu::nonstring]] char name[CONFIG_COROUTINE_NAME_LEN]; + + /* 3. stack *************************************************/ + /* stack_size *includes* CR_STACK_GUARD at each end. */ size_t stack_size; + /* stack is the bottom of the CR_STACK_GUARD at the bottom of the stack. */ void *stack; #if CONFIG_COROUTINE_VALGRIND unsigned stack_id; #endif + + /* 4. env ***************************************************/ + cr_plat_jmp_buf env; }; /* constants ******************************************************************/ @@ -295,7 +410,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", }; @@ -309,16 +423,20 @@ static const uint8_t stack_pattern[] = { }; #endif #if CONFIG_COROUTINE_PROTECT_STACK - #define STACK_GUARD_SIZE \ - ROUND_UP(sizeof(stack_pattern), CR_PLAT_STACK_ALIGNMENT) + #define CR_STACK_GUARD_SIZE \ + ((size_t)LM_ROUND_UP(sizeof(stack_pattern), CR_PLAT_STACK_ALIGNMENT)) #else - #define STACK_GUARD_SIZE 0 + #define CR_STACK_GUARD_SIZE ((size_t)0) #endif /* global variables ***********************************************************/ -static jmp_buf coroutine_add_env; -static jmp_buf coroutine_main_env; +static bool coroutine_initialized = false; +static cr_plat_jmp_buf coroutine_add_env; +static cr_plat_jmp_buf coroutine_main_env; +#if CONFIG_COROUTINE_GDB +static cr_plat_jmp_buf coroutine_gdb_env; +#endif /* * Invariants (and non-invariants): @@ -349,47 +467,46 @@ static struct { * compiler will optimize `%array_len` to &(array_len-1)`, (b) * we don't have to worry about funny wrap-around behavior * when head or tail overflow. */ - cid_t buf[NEXT_POWER_OF_2(CONFIG_COROUTINE_NUM)]; + cid_t buf[LM_NEXT_POWER_OF_2(CONFIG_COROUTINE_NUM)]; } coroutine_ringbuf = {0}; static cid_t coroutine_running = 0; +static size_t coroutine_cnt = 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]; -} - static inline void coroutine_ringbuf_push(cid_t val) { - coroutine_ringbuf.buf[coroutine_ringbuf.head++ % ARRAY_LEN(coroutine_ringbuf.buf)] = val; - assert((coroutine_ringbuf.head % ARRAY_LEN(coroutine_ringbuf.buf)) != - (coroutine_ringbuf.tail % ARRAY_LEN(coroutine_ringbuf.buf))); + coroutine_ringbuf.buf[coroutine_ringbuf.head++ % LM_ARRAY_LEN(coroutine_ringbuf.buf)] = val; + assert((coroutine_ringbuf.head % LM_ARRAY_LEN(coroutine_ringbuf.buf)) != + (coroutine_ringbuf.tail % LM_ARRAY_LEN(coroutine_ringbuf.buf))); } static inline cid_t coroutine_ringbuf_pop(void) { if (coroutine_ringbuf.tail == coroutine_ringbuf.head) return 0; - return coroutine_ringbuf.buf[coroutine_ringbuf.tail++ % ARRAY_LEN(coroutine_ringbuf.buf)]; + return coroutine_ringbuf.buf[coroutine_ringbuf.tail++ % LM_ARRAY_LEN(coroutine_ringbuf.buf)]; +} + +#if CONFIG_COROUTINE_GDB +LM_NEVER_INLINE void cr_gdb_breakpoint(void) { + /* Prevent the call from being optimized away. */ + asm (""); } +LM_NEVER_INLINE void cr_gdb_readjmp(cr_plat_jmp_buf *env) { + if (!cr_plat_setjmp(&coroutine_gdb_env)) + cr_plat_longjmp(env, 2); +} +#define cr_setjmp(env) ({ \ + int val = cr_plat_setjmp(env); \ + if (val == 2) { \ + cr_gdb_breakpoint(); \ + cr_plat_longjmp(&coroutine_gdb_env, 1); \ + } \ + val; \ + }) +#else +#define cr_setjmp(env) cr_plat_setjmp(env) +#endif +#define cr_longjmp(env) cr_plat_longjmp(env, 1) static inline void assert_cid(cid_t cid) { assert(cid > 0); @@ -398,7 +515,7 @@ static inline void assert_cid(cid_t cid) { assert(coroutine_table[cid-1].stack_size); uint8_t *stack = coroutine_table[cid-1].stack; assert(stack); - for (size_t i = 0; i < STACK_GUARD_SIZE; i++) { + for (size_t i = 0; i < CR_STACK_GUARD_SIZE; i++) { size_t j = coroutine_table[cid-1].stack_size - (i+1); assert(stack[i] == stack_pattern[i%sizeof(stack_pattern)]); assert(stack[j] == stack_pattern[j%sizeof(stack_pattern)]); @@ -415,96 +532,131 @@ 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) { +LM_NEVER_INLINE +cid_t coroutine_allocate_cid(void) { static cid_t last_created = 0; + + size_t base = last_created; + for (size_t shift = 0; shift < CONFIG_COROUTINE_NUM; shift++) { + cid_t child = ((base + shift) % CONFIG_COROUTINE_NUM) + 1; + if (coroutine_table[child-1].state == CR_NONE) { + last_created = child; + return child; + } + } + return 0; +} + +cid_t coroutine_add_with_stack_size(size_t stack_size, + const char *name, + cr_fn_t fn, void *args) { cid_t parent = coroutine_running; if (parent) 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); - - cid_t child; - { - size_t base = last_created; - for (size_t shift = 0; shift < CONFIG_COROUTINE_NUM; shift++) { - child = ((base + shift) % CONFIG_COROUTINE_NUM) + 1; - if (coroutine_table[child-1].state == CR_NONE) - goto found; - } - return 0; - found: + debugf("coroutine_add_with_stack_size(%zu, \"%s\", %p, %p)...", + stack_size, name, fn, args); + + if (!coroutine_initialized) { + cr_plat_init(); + coroutine_initialized = true; } - debugf("...child=%zu\n", child); - last_created = child; + cid_t child = coroutine_allocate_cid(); + if (!child) + return 0; + debugf("...child=%zu", child); + + /* 1. state *************************************************/ + coroutine_table[child-1].state = CR_INITIALIZING; - coroutine_table[child-1].stack_size = stack_size; + /* 2. name **************************************************/ + 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)); + + /* 3. stack *************************************************/ + coroutine_table[child-1].stack_size = stack_size + 2*CR_STACK_GUARD_SIZE; + infof("allocing \"%s\" stack with size %zu+2*%zu=%zu", + name, stack_size, CR_STACK_GUARD_SIZE, coroutine_table[child-1].stack_size); coroutine_table[child-1].stack = - aligned_alloc(CR_PLAT_STACK_ALIGNMENT, stack_size); + aligned_alloc(CR_PLAT_STACK_ALIGNMENT, coroutine_table[child-1].stack_size); + infof("... done, stack is [0x%p,0x%p)", + coroutine_table[child-1].stack + CR_STACK_GUARD_SIZE, + coroutine_table[child-1].stack + CR_STACK_GUARD_SIZE + stack_size); #if CONFIG_COROUTINE_MEASURE_STACK || CONFIG_COROUTINE_PROTECT_STACK - for (size_t i = 0; i < stack_size; i++) + for (size_t i = 0; i < coroutine_table[child-1].stack_size; i++) ((uint8_t *)coroutine_table[child-1].stack)[i] = stack_pattern[i%sizeof(stack_pattern)]; #endif #if CONFIG_COROUTINE_VALGRIND coroutine_table[child-1].stack_id = VALGRIND_STACK_REGISTER( - coroutine_table[child-1].stack + STACK_GUARD_SIZE, - coroutine_table[child-1].stack + stack_size - STACK_GUARD_SIZE); + coroutine_table[child-1].stack + CR_STACK_GUARD_SIZE, + coroutine_table[child-1].stack + CR_STACK_GUARD_SIZE + stack_size); #endif + /* 4. env ***************************************************/ coroutine_running = child; - coroutine_table[child-1].state = CR_INITIALIZING; - if (!cr_plat_setjmp(coroutine_add_env)) { /* point=a */ + coroutine_cnt++; + if (!cr_setjmp(&coroutine_add_env)) { /* point=a */ void *stack_base = coroutine_table[child-1].stack + + CR_STACK_GUARD_SIZE #if CR_PLAT_STACK_GROWS_DOWNWARD + stack_size - - STACK_GUARD_SIZE -#else - + 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) { - return coroutine_add_with_stack_size( - CONFIG_COROUTINE_DEFAULT_STACK_SIZE, fn, args); -} - /* coroutine_main() ***********************************************************/ void coroutine_main(void) { - debugf("coroutine_main()\n"); - cr_disable_interrupts(); + debugf("coroutine_main()"); + if (!coroutine_initialized) { + cr_plat_init(); + coroutine_initialized = true; + } + bool saved = cr_save_and_disable_interrupts(); + assert(saved); + assert(!cr_plat_is_in_intrhandler()); coroutine_running = 0; - for (;;) { +#if CONFIG_COROUTINE_GDB + /* Some pointless call to prevent cr_gdb_readjmp() from + * getting pruned out by `ld --gc-sections`. */ + if (coroutine_table[0].state != CR_NONE) + cr_gdb_readjmp(&coroutine_table[0].env); +#endif + while (coroutine_cnt) { 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_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_longjmp(&coroutine_table[coroutine_running-1].env); /* jump to point=c */ } /* This is where we jump to from cr_exit(), and from * nowhere else. */ @@ -514,21 +666,24 @@ void coroutine_main(void) { #endif free(coroutine_table[coroutine_running-1].stack); coroutine_table[coroutine_running-1] = (struct coroutine){0}; + coroutine_cnt--; } + coroutine_running = 0; + cr_restore_interrupts(saved); } /* 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_setjmp(&coroutine_table[coroutine_running-1].env)) /* point=c1 */ + cr_longjmp(&coroutine_add_env); /* jump to point=a */ + cr_restore_interrupts(saved); } static inline void _cr_yield() { @@ -536,9 +691,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,99 +699,135 @@ 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_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_longjmp(&coroutine_table[coroutine_running-1].env); /* 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_plat_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_plat_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_plat_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_longjmp(&coroutine_main_env); /* 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_plat_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_plat_is_in_intrhandler()); + + _cr_unpause(cid); } cid_t cr_getcid(void) { + assert(!cr_plat_is_in_intrhandler()); + assert_cid_state(coroutine_running, state == CR_RUNNING); return coroutine_running; } -/* cr_cid_info() **************************************************************/ +#ifndef NDEBUG +void cr_assert_in_coroutine(void) { + assert(!cr_plat_is_in_intrhandler()); + assert_cid_state(coroutine_running, state == CR_RUNNING); +} + +void cr_assert_in_intrhandler(void) { + assert(cr_plat_is_in_intrhandler()); +} +#endif -#if CONFIG_COROUTINE_MEASURE_STACK +/* answering questions about coroutines ***************************************/ void cr_cid_info(cid_t cid, struct cr_cid_info *ret) { - assert_cid(cid); + assert(cid > 0); + assert(cid <= CONFIG_COROUTINE_NUM); assert(ret); + memset(ret, 0, sizeof(*ret)); + if (coroutine_table[cid-1].state == CR_NONE) + return; + assert_cid(cid); + + /* 1. state *************************************************/ + ret->state = coroutine_table[cid-1].state; + + /* 2. name **************************************************/ + memcpy(ret->name, coroutine_table[cid-1].name, CONFIG_COROUTINE_NAME_LEN); + + /* 3. stack *************************************************/ +#if CONFIG_COROUTINE_MEASURE_STACK + uint8_t *stack = (uint8_t *)coroutine_table[cid-1].stack; + uint8_t *stack_lo = stack + CR_STACK_GUARD_SIZE; + uint8_t *stack_hi = stack + coroutine_table[cid-1].stack_size - CR_STACK_GUARD_SIZE; + /* stack_cap */ - ret->stack_cap = coroutine_table[cid-1].stack_size - 2*STACK_GUARD_SIZE; + ret->stack_cap = stack_hi - stack_lo; /* stack_max */ ret->stack_max = ret->stack_cap; - uint8_t *stack = (uint8_t *)coroutine_table[cid-1].stack; for (;;) { size_t i = #if CR_PLAT_STACK_GROWS_DOWNWARD - STACK_GUARD_SIZE + ret->stack_cap - ret->stack_max + CR_STACK_GUARD_SIZE + ret->stack_cap - ret->stack_max #else - ret->stack_max - 1 - STACK_GUARD_SIZE + ret->stack_max - 1 - CR_STACK_GUARD_SIZE #endif ; - if (ret->stack_max == 0 || - stack[i] != stack_pattern[i%sizeof(stack_pattern)]) + if (ret->stack_max == 0) + break; + assert(stack_lo <= &stack[i] && &stack[i] < stack_hi); +#if CONFIG_COROUTINE_VALGRIND + VALGRIND_DISABLE_ERROR_REPORTING; +#endif + uint8_t v = stack[i]; +#if CONFIG_COROUTINE_VALGRIND + VALGRIND_ENABLE_ERROR_REPORTING; +#endif + if (v != stack_pattern[i%sizeof(stack_pattern)]) break; ret->stack_max--; } @@ -647,15 +836,22 @@ 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 = cr_plat_setjmp_get_sp(&coroutine_add_env); else - sp = coroutine_table[cid-1].sp; + sp = cr_plat_setjmp_get_sp(&coroutine_table[cid-1].env); assert(sp); - uintptr_t sb = (uintptr_t)coroutine_table[cid-1].stack; #if CR_PLAT_STACK_GROWS_DOWNWARD - ret->stack_cur = (sb - STACK_GUARD_SIZE) - sp; + uintptr_t sb = (uintptr_t)stack_hi; + ret->stack_cur = sb - sp; #else - ret->stack_cur = sp - (sb + STACK_GUARD_SIZE); + uintptr_t sb = (uintptr_t)stack_lo; + ret->stack_cur = sp - sb; #endif +#endif /* CONFIG_COROUTINE_MEASURE_STACK */ } -#endif /* CONFIG_COROUTINE_MEASURE_STACK */ +const char *coroutine_state_str(enum coroutine_state state) { + assert(state < LM_ARRAY_LEN(coroutine_state_strs)); + return coroutine_state_strs[state]; +} diff --git a/libcr/include/libcr/coroutine.h b/libcr/include/libcr/coroutine.h index 368974f..2505782 100644 --- a/libcr/include/libcr/coroutine.h +++ b/libcr/include/libcr/coroutine.h @@ -1,6 +1,6 @@ /* libcr/coroutine.h - Simple embeddable coroutine implementation * - * Copyright (C) 2024 Luke T. Shumaker <lukeshu@lukeshu.com> + * Copyright (C) 2024-2025 Luke T. Shumaker <lukeshu@lukeshu.com> * SPDX-License-Identifier: AGPL-3.0-or-later */ @@ -29,6 +29,14 @@ #include <stddef.h> /* for size_t */ #include <stdbool.h> /* for bool */ +/* Configuration **************************************************************/ + +#include "config.h" + +#ifndef CONFIG_COROUTINE_MEASURE_STACK + #error config.h must define CONFIG_COROUTINE_MEASURE_STACK (bool) +#endif + /* typedefs *******************************************************************/ /** @@ -68,7 +76,7 @@ typedef size_t cid_t; * cr_rpc_*() and cr_chan_*() macros call these functions). */ typedef void (*cr_fn_t)(void *args); -#define COROUTINE __attribute__ ((noreturn)) void +#define COROUTINE __attribute__((noreturn)) void /* managing coroutines ********************************************************/ @@ -84,20 +92,24 @@ typedef void (*cr_fn_t)(void *args); * Returns the cid of the newly-created coroutine. May return 0 if * there are already COROUTINE_NUM active coroutines. */ -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); /** * Like coroutine_add_with_stack_size(), but uses a default stack size so * you don't need to think about it. + * + * Either define CONFIG_COROUTINE_STACK_SIZE_DEFAULT to use for all + * coroutines, or CONFIG_COROUTINE_STACK_SIZE_{fn} for each COROUTINE + * function. */ -cid_t coroutine_add(cr_fn_t fn, void *args); +#ifdef CONFIG_COROUTINE_STACK_SIZE_DEFAULT +#define coroutine_add(name, fn, args) coroutine_add_with_stack_size(CONFIG_COROUTINE_STACK_SIZE_DEFAULT, name, fn, args) +#else +#define coroutine_add(name, fn, args) coroutine_add_with_stack_size(CONFIG_COROUTINE_STACK_SIZE_##fn, name, fn, args) +#endif /** - * The main scheduler loop. - * - * "Should" never return, but will print a message to stderr and - * return if there are no coroutines (there were no calls to - * coroutine_add(), or all coroutines cr_exit()). + * The main scheduler loop. Returns if all coroutines exit. */ void coroutine_main(void); @@ -106,7 +118,7 @@ void coroutine_main(void); /** cr_begin() goes at the beginning of a coroutine, after it has initialized its stack. */ void cr_begin( void); /** cr_exit() terminates the currently-running coroutine. */ -__attribute__ ((noreturn)) void cr_exit(void); +[[noreturn]] void cr_exit(void); /** cr_yield() switches to another coroutine (if there is another runnable coroutine to switch to). */ void cr_yield(void); /** cr_pause_and_yield() marks the current coroutine as not-runnable and switches to another coroutine. */ @@ -132,46 +144,84 @@ cid_t cr_getcid(void); * * This is fast on bare-metal, but slow on an OS (because on an OS it * uses a syscall). + * + * Returns whether interrupts were enabled before the call. */ -#define cr_disable_interrupts() do { \ - _cr_plat_disable_interrupts(); \ - asm volatile ("":::"memory"); \ - } while (0) -void _cr_plat_disable_interrupts(void); +#define cr_save_and_disable_interrupts() ({ \ + bool was_enabled = _cr_plat_save_and_disable_interrupts(); \ + asm volatile ("":::"memory"); \ + was_enabled; \ + }) +bool _cr_plat_save_and_disable_interrupts(void); /** - * Enable interrupts. Any "pending" interrupts that came in while + * Re-enable interrupts. Any "pending" interrupts that came in while * interrupts were disabled will have their handlers called. * * This is fast on bare-metal, but slow on an OS (because on an OS it * uses a syscall). */ -#define cr_enable_interrupts() do { \ - asm volatile ("":::"memory"); \ - _cr_plat_enable_interrupts(); \ +#define cr_restore_interrupts(enable) do { \ + asm volatile ("":::"memory"); \ + if (enable) \ + _cr_plat_enable_interrupts(); \ } while (0) void _cr_plat_enable_interrupts(void); /** + * Return whether the current code is running in an interrupt handler + * or "normally". + */ +bool cr_is_in_intrhandler(void); + +/** * cr_unpause_from_intrhandler() is like cr_unpause(), but safe to - * call from a interrupt handler that might race with the coroutine - * actually pausing itself. - * - * It is also safe to call from a regular coroutine, but compared to - * regular cr_unpause() it is less capable of detecting programming - * errors. So don't do that? + * call from a interrupt handler. */ void cr_unpause_from_intrhandler(cid_t); +/** + * cr_assert_in_coroutine() asserts that it is being called from a + * running coroutine. + */ +#ifdef NDEBUG +#define cr_assert_in_coroutine() ((void)0) +#else +void cr_assert_in_coroutine(void); +#endif + + +/** + * cr_assert_in_intrhandler() asserts that it is being called from an + * interrupt handler. + */ +#ifdef NDEBUG +#define cr_assert_in_intrhandler() ((void)0) +#else +void cr_assert_in_intrhandler(void); +#endif + /* answering questions about coroutines ***************************************/ -/* While the following are defined here unconditionally, the - * implementations are #if'd on CONFIG_COROUTINE_MEASURE_STACK. */ +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_RUNNABLE, /* not running, but runnable */ + CR_PAUSED, /* not running, and not runnable */ +}; + +const char *coroutine_state_str(enum coroutine_state); struct cr_cid_info { + enum coroutine_state state; + [[gnu::nonstring]] char name[CONFIG_COROUTINE_NAME_LEN]; + +#if CONFIG_COROUTINE_MEASURE_STACK size_t stack_cap; size_t stack_max; size_t stack_cur; +#endif }; void cr_cid_info(cid_t cid, struct cr_cid_info *ret); diff --git a/libcr/tests/test_matrix.c b/libcr/tests/test_matrix.c new file mode 100644 index 0000000..1f23455 --- /dev/null +++ b/libcr/tests/test_matrix.c @@ -0,0 +1,24 @@ +/* libcr/tests/test_matrix.c - Tests for libcr + * + * Copyright (C) 2024 Luke T. Shumaker <lukeshu@lukeshu.com> + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +#include <libcr/coroutine.h> + +int a = 1; + +COROUTINE cr_init(void *) { + cr_begin(); + a = 2; + cr_end(); +} + +int main() { + coroutine_add("init", cr_init, NULL); + coroutine_main(); + if (a != 2) + return 1; + coroutine_main(); + return 0; +} diff --git a/libcr/tests/test_matrix/config.h b/libcr/tests/test_matrix/config.h new file mode 100644 index 0000000..978b9ac --- /dev/null +++ b/libcr/tests/test_matrix/config.h @@ -0,0 +1,14 @@ +/* config.h - Compile-time configuration for libcr test_matrix + * + * Copyright (C) 2024-2025 Luke T. Shumaker <lukeshu@lukeshu.com> + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +#ifndef _CONFIG_H_ +#define _CONFIG_H_ + +#define CONFIG_COROUTINE_STACK_SIZE_DEFAULT (4*1024) +#define CONFIG_COROUTINE_NAME_LEN 16 +#define CONFIG_COROUTINE_NUM 2 + +#endif /* _CONFIG_H_ */ |