summaryrefslogtreecommitdiff
path: root/libcr
diff options
context:
space:
mode:
Diffstat (limited to 'libcr')
-rw-r--r--libcr/CMakeLists.txt37
-rw-r--r--libcr/coroutine.c443
-rw-r--r--libcr/include/libcr/coroutine.h97
-rw-r--r--libcr/tests/test_matrix.c24
-rw-r--r--libcr/tests/test_matrix/config.h14
5 files changed, 458 insertions, 157 deletions
diff --git a/libcr/CMakeLists.txt b/libcr/CMakeLists.txt
index fbc7618..472820c 100644
--- a/libcr/CMakeLists.txt
+++ b/libcr/CMakeLists.txt
@@ -1,10 +1,13 @@
-# 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_headers INTERFACE)
+target_include_directories(libcr_headers PUBLIC INTERFACE ${CMAKE_CURRENT_LIST_DIR}/include)
+
add_library(libcr INTERFACE)
-target_include_directories(libcr SYSTEM INTERFACE ${CMAKE_CURRENT_LIST_DIR}/include)
+target_link_libraries(libcr INTERFACE libcr_headers)
target_sources(libcr INTERFACE
coroutine.c
)
@@ -14,3 +17,31 @@ target_link_libraries(libcr INTERFACE
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 "${CMAKE_SOURCE_DIR}/build-aux/valgrind" "./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 726b732..182e85c 100644
--- a/libcr/coroutine.c
+++ b/libcr/coroutine.c
@@ -1,15 +1,15 @@
/* 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 <setjmp.h> /* for setjmp(), longjmp(), jmp_buf */
#include <stdint.h> /* for uint8_t */
#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>
@@ -21,28 +21,32 @@
#include "config.h"
-#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
+ #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
@@ -126,8 +130,6 @@
* no longer exists.
*/
-#define ALWAYS_INLINE [[gnu::always_inline]] inline
-
/* platform support ***********************************************************/
/* As part of sbc-harness, this only really needs to support ARM-32, but being
@@ -148,11 +150,26 @@
/* For a signal to be *in* the mask means that the signal is
* *blocked*. */
- bool cr_is_in_intrhandler(void) {
+ #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
+
+ bool cr_plat_is_in_intrhandler(void) {
+#if CONFIG_COROUTINE_VALGRIND
+ if (_cr_plat_in_sigsuspend)
+ return true;
+#endif
sigset_t cur_mask;
- sigfillset(&cur_mask);
sigprocmask(0, NULL, &cur_mask);
- if (sigismember(&cur_mask, SIGHUP))
+ if (sigismember(&cur_mask, _CR_SIG_SENTINEL))
/* Interrupts are disabled, so we cannot be in
* an interrupt handler. */
return false;
@@ -162,47 +179,63 @@
return false;
}
static inline bool _cr_plat_are_interrupts_enabled(void) {
- assert(!cr_is_in_intrhandler());
+ assert(!cr_plat_is_in_intrhandler());
sigset_t cur_mask;
sigfillset(&cur_mask);
sigprocmask(0, NULL, &cur_mask);
- return !sigismember(&cur_mask, SIGHUP);
+ return !sigismember(&cur_mask, _CR_SIG_SENTINEL);
}
static inline void cr_plat_wait_for_interrupt(void) {
- assert(!cr_is_in_intrhandler());
+ 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);
-
- sigfillset(&set);
- sigprocmask(SIG_SETMASK, &set, NULL);
+#if CONFIG_COROUTINE_VALGRIND
+ _cr_plat_in_sigsuspend = false;
+#endif
}
bool _cr_plat_save_and_disable_interrupts(void) {
- assert(!cr_is_in_intrhandler());
+ assert(!cr_plat_is_in_intrhandler());
sigset_t all, old;
sigfillset(&all);
sigprocmask(SIG_SETMASK, &all, &old);
- return !sigismember(&old, SIGHUP);
+ return !sigismember(&old, _CR_SIG_SENTINEL);
}
void _cr_plat_enable_interrupts(void) {
- assert(!cr_is_in_intrhandler());
+ 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_is_in_intrhandler(void) {
+ bool cr_plat_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());
+ 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)
@@ -210,8 +243,8 @@
return primask == 0;
}
- ALWAYS_INLINE static void cr_plat_wait_for_interrupt(void) {
- assert(!cr_is_in_intrhandler());
+ 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"
@@ -220,16 +253,17 @@
:::"memory");
}
bool _cr_plat_save_and_disable_interrupts(void) {
- assert(!cr_is_in_intrhandler());
+ 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) {
- assert(!cr_is_in_intrhandler());
+ 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 __ARM_ARCH_6M__ && __ARM_EABI__)
#endif
@@ -240,7 +274,7 @@
#define CR_PLAT_STACK_GROWS_DOWNWARD 1
#if CONFIG_COROUTINE_MEASURE_STACK
- ALWAYS_INLINE static 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;
@@ -272,7 +306,7 @@
#define CR_PLAT_STACK_GROWS_DOWNWARD 1
#if CONFIG_COROUTINE_MEASURE_STACK
- ALWAYS_INLINE static 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;
@@ -304,6 +338,7 @@
* 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
@@ -314,11 +349,14 @@
uintptr_t sp;
#endif
} cr_plat_jmp_buf;
- static inline void _cr_plat_setjmp_pre(cr_plat_jmp_buf *env) {
+ static void _cr_plat_setjmp_pre(cr_plat_jmp_buf *env [[maybe_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.
@@ -344,32 +382,27 @@
}
#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_RUNNABLE, /* not running, but runnable */
- CR_PAUSED, /* not running, and not runnable */
-};
-
struct coroutine {
+ /* 1. state *************************************************/
volatile enum coroutine_state state;
- cr_plat_jmp_buf env;
+
+ /* 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
- char name[CONFIG_COROUTINE_NAME_LEN];
+ bool stack_free;
+
+ /* 4. env ***************************************************/
+ cr_plat_jmp_buf env;
};
/* constants ******************************************************************/
@@ -391,16 +424,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 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):
@@ -421,7 +458,7 @@ static cr_plat_jmp_buf coroutine_main_env;
* coroutine_ringbuf queue.
*/
-static struct coroutine coroutine_table[CONFIG_COROUTINE_NUM] = {0};
+static struct coroutine coroutine_table[CONFIG_COROUTINE_NUM] = {};
static struct {
/* tail == head means empty */
/* buf[tail] is the next thing to run */
@@ -431,28 +468,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)];
-} coroutine_ringbuf = {0};
+ cid_t buf[LM_NEXT_POWER_OF_2(CONFIG_COROUTINE_NUM)];
+} coroutine_ringbuf = {};
static cid_t coroutine_running = 0;
+static size_t coroutine_cnt = 0;
/* utility functions **********************************************************/
-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);
@@ -461,7 +516,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)]);
@@ -478,66 +533,94 @@ static inline void assert_cid(cid_t cid) {
/* coroutine_add() ************************************************************/
-cid_t coroutine_add_with_stack_size(size_t stack_size,
- const char *name,
- 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;
+}
+
+static cid_t _coroutine_add(void *stack, size_t full_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(full_stack_size > 2*CR_STACK_GUARD_SIZE);
assert(fn);
- debugf("coroutine_add_with_stack_size(%zu, \"%s\", %p, %p)...",
- stack_size, name, 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:
+
+ if (!coroutine_initialized) {
+ cr_plat_init();
+ coroutine_initialized = true;
}
- debugf("...child=%zu", child);
- last_created = child;
+ cid_t child = coroutine_allocate_cid();
+ if (!child)
+ return 0;
+ log_debugln("...child=", child);
+
+ /* 1. state *************************************************/
+ coroutine_table[child-1].state = CR_INITIALIZING;
+ /* 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));
- coroutine_table[child-1].stack_size = stack_size;
- coroutine_table[child-1].stack =
- aligned_alloc(CR_PLAT_STACK_ALIGNMENT, stack_size);
+ log_infoln("starting cid ", child,
+ " ", (qstrn, coroutine_table[child-1].name, sizeof(coroutine_table[child-1].name)),
+ ":");
+
+ /* 3. stack *************************************************/
+ coroutine_table[child-1].stack_size = full_stack_size;
+ coroutine_table[child-1].stack_free = (stack == NULL);
+ if (!stack) {
+ log_infoln("allocing stack with size ", full_stack_size, "...");
+ stack = aligned_alloc(CR_PLAT_STACK_ALIGNMENT, full_stack_size);
+ log_infoln("...done");
+ }
+ coroutine_table[child-1].stack = stack;
+ log_infoln(" -> full stack is [",
+ (ptr, coroutine_table[child-1].stack), ",",
+ (ptr, coroutine_table[child-1].stack + full_stack_size), ")",
+ " ; size=", full_stack_size);
+ log_infoln(" -> usable stack is [",
+ (ptr, coroutine_table[child-1].stack + CR_STACK_GUARD_SIZE), ",",
+ (ptr, coroutine_table[child-1].stack + full_stack_size - CR_STACK_GUARD_SIZE), ")",
+ " ; size=", full_stack_size - 2*CR_STACK_GUARD_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 < full_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 + full_stack_size - CR_STACK_GUARD_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
#if CR_PLAT_STACK_GROWS_DOWNWARD
- + stack_size
- - STACK_GUARD_SIZE
+ + full_stack_size - CR_STACK_GUARD_SIZE
#else
- + STACK_GUARD_SIZE
+ + CR_STACK_GUARD_SIZE
#endif
;
- debugf("...stack =%p", coroutine_table[child-1].stack);
- debugf("...stack_base=%p", stack_base);
+ log_debugln("...stack_base=", (ptr, stack_base));
/* run until cr_begin() */
cr_plat_call_with_stack(stack_base, fn, args);
assert_notreached("should cr_begin() instead of returning");
@@ -551,24 +634,52 @@ cid_t coroutine_add_with_stack_size(size_t stack_size,
* didn't actually check. */
cr_restore_interrupts(true);
coroutine_running = parent;
+ log_infoln(" -> done");
return child;
}
-cid_t coroutine_add(const char *name, cr_fn_t fn, void *args) {
- return coroutine_add_with_stack_size(
- CONFIG_COROUTINE_DEFAULT_STACK_SIZE, name, fn, args);
+cid_t coroutine_add_with_stack(void *stack, size_t full_stack_size,
+ const char *name,
+ cr_fn_t fn, void *args) {
+ if (name)
+ log_debugln("coroutine_add_with_stack(", (ptr, stack), full_stack_size, ", ", (qstr, name), ", ", (ptr, fn), ", ", (ptr, args), ")...");
+ else
+ log_debugln("coroutine_add_with_stack(", (ptr, stack), full_stack_size, ", ", (ptr, name), ", ", (ptr, fn), ", ", (ptr, args), ")...");
+
+ return _coroutine_add(stack, full_stack_size, name, fn, args);
+}
+
+cid_t coroutine_add_with_stack_size(size_t usable_stack_size,
+ const char *name,
+ cr_fn_t fn, void *args) {
+ if (name)
+ log_debugln("coroutine_add_with_stack_size(", usable_stack_size, ", ", (qstr, name), ", ", (ptr, fn), ", ", (ptr, args), ")...");
+ else
+ log_debugln("coroutine_add_with_stack_size(", usable_stack_size, ", ", (ptr, name), ", ", (ptr, fn), ", ", (ptr, args), ")...");
+
+ return _coroutine_add(NULL, usable_stack_size + 2*CR_STACK_GUARD_SIZE, name, fn, args);
}
/* coroutine_main() ***********************************************************/
-[[noreturn]] void coroutine_main(void) {
- debugf("coroutine_main()");
+void coroutine_main(void) {
+ log_debugln("coroutine_main()");
+ if (!coroutine_initialized) {
+ cr_plat_init();
+ coroutine_initialized = true;
+ }
bool saved = cr_save_and_disable_interrupts();
assert(saved);
- assert(!cr_is_in_intrhandler());
+ 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
@@ -576,10 +687,10 @@ cid_t coroutine_add(const char *name, cr_fn_t fn, void *args) {
cr_plat_wait_for_interrupt();
}
- 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. */
@@ -587,26 +698,30 @@ cid_t coroutine_add(const char *name, cr_fn_t fn, void *args) {
#if CONFIG_COROUTINE_VALGRIND
VALGRIND_STACK_DEREGISTER(coroutine_table[coroutine_running-1].stack_id);
#endif
- free(coroutine_table[coroutine_running-1].stack);
- coroutine_table[coroutine_running-1] = (struct coroutine){0};
+ if (coroutine_table[coroutine_running-1].stack_free)
+ free(coroutine_table[coroutine_running-1].stack);
+ coroutine_table[coroutine_running-1] = (struct coroutine){};
+ coroutine_cnt--;
}
+ coroutine_running = 0;
+ cr_restore_interrupts(saved);
}
/* cr_*() *********************************************************************/
void cr_begin(void) {
- debugf("cid=%zu: cr_begin()", coroutine_running);
+ log_debugln("cid=", coroutine_running, ": cr_begin()");
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);
- if (!cr_plat_setjmp(&coroutine_table[coroutine_running-1].env)) /* point=c1 */
- cr_plat_longjmp(&coroutine_add_env, 1); /* jump to point=a */
+ 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() {
+static inline void _cr_yield(void) {
cid_t next;
while ( !((next = coroutine_ringbuf_pop())) ) {
/* No coroutines are runnable, wait for an interrupt
@@ -619,16 +734,16 @@ static inline void _cr_yield() {
return;
}
- 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()", coroutine_running);
- assert(!cr_is_in_intrhandler());
+ log_debugln("cid=", coroutine_running ,": cr_yield()");
+ assert(!cr_plat_is_in_intrhandler());
assert_cid_state(coroutine_running, state == CR_RUNNING);
bool saved = cr_save_and_disable_interrupts();
@@ -639,8 +754,8 @@ void cr_yield(void) {
}
void cr_pause_and_yield(void) {
- debugf("cid=%zu: cr_pause_and_yield()", coroutine_running);
- assert(!cr_is_in_intrhandler());
+ log_debugln("cid=", coroutine_running, ": cr_pause_and_yield()");
+ assert(!cr_plat_is_in_intrhandler());
assert_cid_state(coroutine_running, state == CR_RUNNING);
bool saved = cr_save_and_disable_interrupts();
@@ -650,13 +765,13 @@ void cr_pause_and_yield(void) {
}
[[noreturn]] void cr_exit(void) {
- debugf("cid=%zu: cr_exit()", coroutine_running);
- assert(!cr_is_in_intrhandler());
+ log_debugln("cid=", coroutine_running, ": cr_exit()");
+ assert(!cr_plat_is_in_intrhandler());
assert_cid_state(coroutine_running, state == CR_RUNNING);
(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 */
}
static void _cr_unpause(cid_t cid) {
@@ -667,8 +782,8 @@ static void _cr_unpause(cid_t cid) {
}
void cr_unpause(cid_t cid) {
- debugf("cr_unpause(%zu)", cid);
- assert(!cr_is_in_intrhandler());
+ log_debugln("cr_unpause(", cid, ")");
+ assert(!cr_plat_is_in_intrhandler());
assert_cid_state(coroutine_running, state == CR_RUNNING);
bool saved = cr_save_and_disable_interrupts();
@@ -677,42 +792,77 @@ void cr_unpause(cid_t cid) {
}
void cr_unpause_from_intrhandler(cid_t cid) {
- debugf("cr_unpause_from_intrhandler(%zu)", cid);
- assert(cr_is_in_intrhandler());
+ log_debugln("cr_unpause_from_intrhandler(", cid, ")");
+ assert(cr_plat_is_in_intrhandler());
_cr_unpause(cid);
}
cid_t cr_getcid(void) {
- assert(!cr_is_in_intrhandler());
+ 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);
+}
-#if CONFIG_COROUTINE_MEASURE_STACK
+void cr_assert_in_intrhandler(void) {
+ assert(cr_plat_is_in_intrhandler());
+}
+#endif
+
+/* 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--;
}
@@ -722,16 +872,21 @@ void cr_cid_info(cid_t cid, struct cr_cid_info *ret) {
if (cid == coroutine_running)
sp = cr_plat_get_sp();
else if (coroutine_table[cid-1].state == CR_RUNNING)
- sp = coroutine_add_env.sp;
+ sp = cr_plat_setjmp_get_sp(&coroutine_add_env);
else
- sp = coroutine_table[cid-1].env.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 86b8452..f0e6e61 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
*/
@@ -26,8 +26,18 @@
#ifndef _LIBCR_COROUTINE_H_
#define _LIBCR_COROUTINE_H_
-#include <stddef.h> /* for size_t */
-#include <stdbool.h> /* for bool */
+#include <stddef.h> /* for size_t */
+
+/* Configuration **************************************************************/
+
+#include "config.h"
+
+#ifndef CONFIG_COROUTINE_MEASURE_STACK
+ #error config.h must define CONFIG_COROUTINE_MEASURE_STACK (bool)
+#endif
+#ifndef CONFIG_COROUTINE_NAME_LEN
+ #error config.h must define CONFIG_COROUTINE_NAME_LEN (non-negative integer)
+#endif
/* typedefs *******************************************************************/
@@ -73,7 +83,15 @@ typedef void (*cr_fn_t)(void *args);
/* managing coroutines ********************************************************/
/**
- * Call `fn(args)` in a new coroutine with stack size `stack_size`.
+ * Call `fn(args)` in a new coroutine with the `full_stack_size`-sized
+ * block of memory `stack` as the coroutine stack.
+ *
+ * There may be CPU-specific requirements on the alignment of the
+ * `stack` pointer.
+ *
+ * If CONFIG_COROUTINE_PROTECT_STACK: The usable stack size will be
+ * slightly less than `full_stack_size`, in order to make room for a
+ * stack guard at each end.
*
* See the doc comment on c_fn_t for the requirements imposed on fn.
*
@@ -84,18 +102,44 @@ 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, const char *name, cr_fn_t fn, void *args);
+cid_t coroutine_add_with_stack(void *stack, size_t full_stack_size, const char *name, cr_fn_t fn, void *args);
+
+/**
+ * Like coroutine_add_with_stack(), but will use aligned_alloc() to
+ * allocate a block of memory to use as the stack.
+ *
+ * If CONFIG_COROUTINE_PROTECT_STACK: `usable_stack_size` does *not*
+ * include the size of the stack guard at each end; the amount of
+ * memory allocated for the stack will be slightly larger than
+ * `usable_stack_size`.
+ */
+cid_t coroutine_add_with_stack_size(size_t usable_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
+ * - define/declare CONFIG_COROUTINE_STACK_SIZE_{fn} for each COROUTINE
+ * function; or
+ * - define CONFIG_COROUTINE_STACK_PREALLOCATE and then
+ * define/declare COROUTINE_STACK_{fn} and COROUTINE_STACK_{fn}_len
+ * for each COROUTINE function.
*/
-cid_t coroutine_add(const char *name, cr_fn_t fn, void *args);
+#if defined(CONFIG_COROUTINE_STACK_PREALLOCATE)
+#define coroutine_add(name, fn, args) coroutine_add_with_stack(COROUTINE_STACK_##fn, COROUTINE_STACK_##fn##_len, name, fn, args)
+#elif defined(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.
+ * The main scheduler loop. Returns if all coroutines exit.
*/
-[[noreturn]] void coroutine_main(void);
+void coroutine_main(void);
/* inside of coroutines *******************************************************/
@@ -164,15 +208,48 @@ bool cr_is_in_intrhandler(void);
*/
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..eaa4bdc
--- /dev/null
+++ b/libcr/tests/test_matrix.c
@@ -0,0 +1,24 @@
+/* libcr/tests/test_matrix.c - Tests for libcr
+ *
+ * Copyright (C) 2024-2025 Luke T. Shumaker <lukeshu@lukeshu.com>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+#include <libcr/coroutine.h>
+
+int a = 1;
+
+COROUTINE init_cr(void *) {
+ cr_begin();
+ a = 2;
+ cr_end();
+}
+
+int main() {
+ coroutine_add("init", init_cr, 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..decd6de
--- /dev/null
+++ b/libcr/tests/test_matrix/config.h
@@ -0,0 +1,14 @@
+/* libcr/tests/test_matrix/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_ */