From f4a06affd8a6fb9b27cf4e252c177b5854b04994 Mon Sep 17 00:00:00 2001 From: Brendan Dahl Date: Mon, 31 Aug 2026 17:10:20 +0000 Subject: [PATCH 1/2] Support fiber API with JSPI The fiber API currently only supports Asyncify. JSPI allows suspending and resuming WebAssembly execution via native stack switching without the code size and performance overhead of Asyncify bytecode instrumentation. Mark asyncify_stack parameters as _Nullable in fiber.h since an Asyncify stack buffer is unnecessary under JSPI. Update the fiber test to run under both Asyncify and JSPI. --- ChangeLog.md | 3 + site/source/docs/api_reference/fiber.h.rst | 27 +++--- src/lib/libasync.js | 99 +++++++++++++++++++--- system/include/emscripten/fiber.h | 18 ++-- system/lib/libc/emscripten_fiber.c | 8 +- test/test_core.py | 28 +++++- test/test_fibers.cpp | 31 ++++++- 7 files changed, 178 insertions(+), 36 deletions(-) diff --git a/ChangeLog.md b/ChangeLog.md index 56caf6e57b2f2..bdce03d07ed5b 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -20,6 +20,9 @@ See docs/process.md for more on how version tagging works. 6.0.10 (in development) ---------------------- +- The fiber API (`emscripten/fiber.h`) is now supported under JSPI (`-sJSPI`). + When compiling with JSPI, the `asyncify_stack` argument to `emscripten_fiber_init` + and `emscripten_fiber_init_from_current_context` is optional and can be `NULL`. 6.0.9 - 09/01/26 ---------------- diff --git a/site/source/docs/api_reference/fiber.h.rst b/site/source/docs/api_reference/fiber.h.rst index c8c0d79d4f757..7abb70964bd72 100644 --- a/site/source/docs/api_reference/fiber.h.rst +++ b/site/source/docs/api_reference/fiber.h.rst @@ -8,8 +8,8 @@ fiber.h co-operative threads of execution. The `fiber.h `_ header defines a low-level API for manipulating Fibers in Emscripten. Fibers are -implemented with :ref:`asyncify section`, so you must link your program with -:ref:`ASYNCIFY` if you intend to use them. +implemented with :ref:`asyncify section` or JSPI, so you must link your program with +:ref:`ASYNCIFY` or ``-sJSPI`` if you intend to use them. Fibers are intended as a building block for asynchronous control flow constructs, such as coroutines. They supersede the legacy coroutine API that was @@ -53,8 +53,8 @@ Types .. c:member:: em_arg_callback_func entry Entry point. If not NULL, this function will be called when the fiber is - switched into. Otherwise, :c:member:`emscripten_fiber_t.asyncify_data` is - used to rewind the call stack. + switched into. Otherwise, :c:member:`emscripten_fiber_t.asyncify_data` (under + Asyncify) or native stack switching (under JSPI) is used to resume the call stack. .. c:member:: void *user_data @@ -62,7 +62,7 @@ Types .. c:member:: asyncify_data_t asyncify_data - Asyncify data structure. Used to unwind and rewind the call stack when switching fibers. + Asyncify data structure. Used to unwind and rewind the call stack when switching fibers under Asyncify (unused under JSPI). .. c:type:: asyncify_data_t @@ -98,8 +98,8 @@ Functions :param void* entry_func_arg: Opaque pointer passed to `entry_func`. :param void* c_stack: Pointer to memory region to use for the C stack. Must be at least 16-byte aligned. This points to the lower bound of the stack, regardless of growth direction. :param size_t c_stack_size: Size of the C stack memory region, in bytes. - :param void* asyncify_stack: Pointer to memory region to use for the Asyncify stack. No special alignment requirements. - :param size_t asyncify_stack_size: Size of the Asyncify stack memory region, in bytes. + :param void* asyncify_stack: Pointer to memory region to use for the Asyncify stack. No special alignment requirements. Under JSPI, this parameter may be `NULL`. + :param size_t asyncify_stack_size: Size of the Asyncify stack memory region, in bytes. Under JSPI, this parameter may be `0`. .. note:: If `entry_func` returns, the entire program will end, as if `main` had returned. To avoid this, you can use :c:func:`emscripten_fiber_swap` to jump to another fiber. @@ -122,8 +122,10 @@ Functions :param emscripten_fiber_t* fiber: Pointer to the fiber structure. :param void* asyncify_stack: Pointer to memory region to use for the Asyncify - stack. No special alignment requirements. + stack. No special alignment requirements. Under JSPI, + this parameter may be `NULL`. :param size_t asyncify_stack_size: Size of the Asyncify stack memory region, in bytes. + Under JSPI, this parameter may be `0`. .. c:function:: void emscripten_fiber_swap(emscripten_fiber_t *old_fiber, emscripten_fiber_t *new_fiber) @@ -137,8 +139,9 @@ Functions :param emscripten_fiber_t* new_fiber: Fiber representing the target context. If the fiber has an entry point, it will be called in the new context and set - to `NULL`. Otherwise, + to `NULL`. Otherwise, the call stack is + resumed (using :c:member:`emscripten_fiber_t.asyncify_data` - is used to rewind the call stack. If the - fiber is invalid or incomplete, the - behavior is undefined. + under Asyncify or native stack switching + under JSPI). If the fiber is invalid or + incomplete, the behavior is undefined. diff --git a/src/lib/libasync.js b/src/lib/libasync.js index b0d59fa7503df..b9fc478a27029 100644 --- a/src/lib/libasync.js +++ b/src/lib/libasync.js @@ -515,8 +515,23 @@ addToLibrary({ }); }, - $Fibers__deps: ['$Asyncify', 'emscripten_stack_set_limits', '$stackRestore'], + $Fibers__deps: ['emscripten_stack_set_limits', '$stackRestore', +#if ASYNCIFY == 1 + '$Asyncify', +#endif + ], $Fibers: { + restoreStack(fiber) { + var stack_base = {{{ makeGetValue('fiber', C_STRUCTS.emscripten_fiber_s.stack_base, '*') }}}; + var stack_max = {{{ makeGetValue('fiber', C_STRUCTS.emscripten_fiber_s.stack_limit, '*') }}}; + _emscripten_stack_set_limits(stack_base, stack_max); +#if STACK_OVERFLOW_CHECK >= 2 + ___set_stack_limits(stack_base, stack_max); +#endif + stackRestore({{{ makeGetValue('fiber', C_STRUCTS.emscripten_fiber_s.stack_ptr, '*') }}}); + }, + +#if ASYNCIFY == 1 nextFiber: 0, trampolineRunning: false, trampoline() { @@ -537,15 +552,7 @@ addToLibrary({ * NOTE: This function is the asynchronous part of emscripten_fiber_swap. */ finishContextSwitch(newFiber) { - var stack_base = {{{ makeGetValue('newFiber', C_STRUCTS.emscripten_fiber_s.stack_base, '*') }}}; - var stack_max = {{{ makeGetValue('newFiber', C_STRUCTS.emscripten_fiber_s.stack_limit, '*') }}}; - _emscripten_stack_set_limits(stack_base, stack_max); - -#if STACK_OVERFLOW_CHECK >= 2 - ___set_stack_limits(stack_base, stack_max); -#endif - - stackRestore({{{ makeGetValue('newFiber', C_STRUCTS.emscripten_fiber_s.stack_ptr, '*') }}}); + Fibers.restoreStack(newFiber); var entryPoint = {{{ makeGetValue('newFiber', C_STRUCTS.emscripten_fiber_s.entry, '*') }}}; @@ -562,6 +569,10 @@ addToLibrary({ var userData = {{{ makeGetValue('newFiber', C_STRUCTS.emscripten_fiber_s.user_data, '*') }}}; {{{ makeDynCall('vp', 'entryPoint') }}}(userData); } else { +#if ASSERTIONS + var newAsyncifyStack = {{{ makeGetValue('newFiber', C_STRUCTS.emscripten_fiber_s.asyncify_data + C_STRUCTS.asyncify_data_s.stack_ptr, '*') }}}; + assert(newAsyncifyStack, 'finishContextSwitch: fiber was initialized with a null asyncify_stack, which is only supported under JSPI (-sJSPI)'); +#endif var asyncifyData = newFiber + {{{ C_STRUCTS.emscripten_fiber_s.asyncify_data }}}; Asyncify.currData = asyncifyData; @@ -573,12 +584,58 @@ addToLibrary({ Asyncify.doRewind(asyncifyData); } }, +#elif ASYNCIFY == 2 + fiberResolvers: new Map(), + + swap(oldFiber, newFiber) { + return new Promise((resolve) => { + Fibers.fiberResolvers.set(oldFiber, resolve); + var entryPoint = {{{ makeGetValue('newFiber', C_STRUCTS.emscripten_fiber_s.entry, '*') }}}; + if (entryPoint) { + {{{ makeSetValue('newFiber', C_STRUCTS.emscripten_fiber_s.entry, 0, '*') }}}; + Fibers.restoreStack(newFiber); +#if STACK_OVERFLOW_CHECK + writeStackCookie(); +#endif +#if ASYNCIFY_DEBUG + dbg('ASYNCIFY/FIBER: entering fiber', newFiber, 'for the first time'); +#endif + var userData = {{{ makeGetValue('newFiber', C_STRUCTS.emscripten_fiber_s.user_data, '*') }}}; + var start = {{{ makeDynCall('vp', 'entryPoint', true) }}}; + start(userData).catch((e) => { + abort(String(e)); + }); + } else { + var resume = Fibers.fiberResolvers.get(newFiber); +#if ASSERTIONS + assert(resume, `fiber ${newFiber} is not suspended`); +#endif +#if ASYNCIFY_DEBUG + dbg('ASYNCIFY/FIBER: resume fiber', newFiber); +#endif + Fibers.fiberResolvers.delete(newFiber); + resume(); + } + }); + }, +#endif }, - emscripten_fiber_swap__deps: ['$Asyncify', '$Fibers', '$stackSave'], + emscripten_fiber_swap__deps: ['$Fibers', '$stackSave', +#if ASYNCIFY == 1 + '$Asyncify', +#endif + ], emscripten_fiber_swap__async: true, +#if ASYNCIFY == 1 emscripten_fiber_swap: (oldFiber, newFiber) => { if (ABORT) return; +#if ASSERTIONS + assert(oldFiber, 'emscripten_fiber_swap: oldFiber must not be null'); + assert(newFiber, 'emscripten_fiber_swap: newFiber must not be null'); + var asyncifyStack = {{{ makeGetValue('oldFiber', C_STRUCTS.emscripten_fiber_s.asyncify_data + C_STRUCTS.asyncify_data_s.stack_ptr, '*') }}}; + assert(asyncifyStack, 'emscripten_fiber_swap: fiber was initialized with a null asyncify_stack, which is only supported under JSPI (-sJSPI)'); +#endif #if ASYNCIFY_DEBUG dbg('ASYNCIFY/FIBER: swap', oldFiber, '->', newFiber, 'state:', Asyncify.state); #endif @@ -610,6 +667,26 @@ addToLibrary({ Asyncify.currData = null; } }, +#elif ASYNCIFY == 2 + emscripten_fiber_swap: async (oldFiber, newFiber) => { + if (ABORT) return; +#if ASSERTIONS + assert(oldFiber, 'emscripten_fiber_swap: oldFiber must not be null'); + assert(newFiber, 'emscripten_fiber_swap: newFiber must not be null'); +#endif +#if ASYNCIFY_DEBUG + dbg('ASYNCIFY/FIBER: swap', oldFiber, '->', newFiber); +#endif + if (oldFiber === newFiber) return; + + var stackTop = stackSave(); + {{{ makeSetValue('oldFiber', C_STRUCTS.emscripten_fiber_s.stack_ptr, 'stackTop', '*') }}}; + + await Fibers.swap(oldFiber, newFiber); + + Fibers.restoreStack(oldFiber); + }, +#endif #else // ASYNCIFY emscripten_sleep: () => { abort('Please compile your program with async support in order to use asynchronous operations like emscripten_sleep'); diff --git a/system/include/emscripten/fiber.h b/system/include/emscripten/fiber.h index 4cb6a67ae581a..2d2fbe7f77759 100644 --- a/system/include/emscripten/fiber.h +++ b/system/include/emscripten/fiber.h @@ -26,30 +26,38 @@ typedef struct emscripten_fiber_s { void *stack_base; /** Where the C stack starts (NOTE: grows down). */ void *stack_limit; /** Where the C stack ends. */ void *stack_ptr; /** Current position in the C stack. */ - em_arg_callback_func entry; /** Function to call when resuming this context. If NULL, asyncify_data is used to rewind the call stack. */ + em_arg_callback_func entry; /** Function to call when resuming this context. If NULL, asyncify_data (under Asyncify) or native stack switching (under JSPI) is used to resume the call stack. */ void *user_data; /** Opaque pointer, passed as-is to the entry function. */ - asyncify_data_t asyncify_data; + asyncify_data_t asyncify_data; /** Asyncify data structure (unused under JSPI). */ } emscripten_fiber_t; +/** + * Initializes a fiber context. + * Under JSPI (-sJSPI), asyncify_stack may be NULL and asyncify_stack_size 0. + */ void emscripten_fiber_init( emscripten_fiber_t * _Nonnull fiber, em_arg_callback_func entry_func, void *entry_func_arg, void * _Nonnull c_stack, size_t c_stack_size, - void * _Nonnull asyncify_stack, + void * _Nullable asyncify_stack, size_t asyncify_stack_size ); +/** + * Partially initializes a fiber based on the currently active context. + * Under JSPI (-sJSPI), asyncify_stack may be NULL and asyncify_stack_size 0. + */ void emscripten_fiber_init_from_current_context( emscripten_fiber_t * _Nonnull fiber, - void * _Nonnull asyncify_stack, + void * _Nullable asyncify_stack, size_t asyncify_stack_size ); void emscripten_fiber_swap( emscripten_fiber_t * _Nonnull old_fiber, - emscripten_fiber_t * _Nonnull new_fibe + emscripten_fiber_t * _Nonnull new_fiber ); #ifdef __cplusplus diff --git a/system/lib/libc/emscripten_fiber.c b/system/lib/libc/emscripten_fiber.c index e674421117ede..b11642457582b 100644 --- a/system/lib/libc/emscripten_fiber.c +++ b/system/lib/libc/emscripten_fiber.c @@ -12,7 +12,7 @@ void emscripten_fiber_init( void *entry_func_arg, void *c_stack, size_t c_stack_size, - void *asyncify_stack, + void * _Nullable asyncify_stack, size_t asyncify_stack_size ) { char *c_stack_base = (char*)c_stack + c_stack_size; @@ -22,17 +22,17 @@ void emscripten_fiber_init( fiber->entry = entry_func; fiber->user_data = entry_func_arg; fiber->asyncify_data.stack_ptr = asyncify_stack; - fiber->asyncify_data.stack_limit = (char*)asyncify_stack + asyncify_stack_size; + fiber->asyncify_data.stack_limit = asyncify_stack ? (char*)asyncify_stack + asyncify_stack_size : NULL; } void emscripten_fiber_init_from_current_context( emscripten_fiber_t *fiber, - void *asyncify_stack, + void * _Nullable asyncify_stack, size_t asyncify_stack_size ) { fiber->stack_base = (void*)emscripten_stack_get_base(); fiber->stack_limit = (void*)emscripten_stack_get_end(); fiber->entry = NULL; fiber->asyncify_data.stack_ptr = asyncify_stack; - fiber->asyncify_data.stack_limit = (char*)asyncify_stack + asyncify_stack_size; + fiber->asyncify_data.stack_limit = asyncify_stack ? (char*)asyncify_stack + asyncify_stack_size : NULL; } diff --git a/test/test_core.py b/test/test_core.py index c267d0575d67c..170e1fefc2def 100644 --- a/test/test_core.py +++ b/test/test_core.py @@ -8469,11 +8469,33 @@ def test_async_ccall_promise(self, exit_runtime): self.cflags += ['--pre-js', 'pre.js', '-sINCOMING_MODULE_JS_API=onRuntimeInitialized'] self.do_runf('main.c', 'stringf: first\nsecond\n6.4') - @no_esm_integration('WASM_ESM_INTEGRATION is not compatible with ASYNCIFY=1') - def test_fibers_asyncify(self): + @with_asyncify_and_jspi + def test_fibers(self): + self.maybe_closure() + if self.get_setting('JSPI'): + self.cflags += ['-DJSPI'] + self.do_runf('test_fibers.cpp', '*leaf-0-100-1-101-1-102-2-103-3-104-5-105-8-106-13-107-21-108-34-109-direct-1035-*\n') + + def test_fibers_asyncify_null_stack(self): self.set_setting('ASYNCIFY') + self.set_setting('ASSERTIONS') self.maybe_closure() - self.do_runf('test_fibers.cpp', '*leaf-0-100-1-101-1-102-2-103-3-104-5-105-8-106-13-107-21-108-34-109-*') + self.do_run(''' +#include +#include + +static emscripten_fiber_t main_fiber; + +int main() { + emscripten_fiber_init_from_current_context(&main_fiber, NULL, 0); + emscripten_fiber_t child; + alignas(16) char c_stack[4096]; + emscripten_fiber_init(&child, NULL, NULL, c_stack, sizeof(c_stack), NULL, 0); + emscripten_fiber_swap(&main_fiber, &child); + return 0; +} +''', 'Assertion failed: emscripten_fiber_swap: fiber was initialized with a null asyncify_stack, which is only supported under JSPI (-sJSPI)', + assert_returncode=NON_ZERO) @with_asyncify_and_jspi def test_asyncify_unused(self): diff --git a/test/test_fibers.cpp b/test/test_fibers.cpp index 09e2878eafabe..7d34df55240fc 100644 --- a/test/test_fibers.cpp +++ b/test/test_fibers.cpp @@ -16,7 +16,11 @@ struct Fiber { int result = 0; void init_with_api(em_arg_callback_func entry, void *arg) { +#ifdef JSPI + emscripten_fiber_init(&context, entry, arg, c_stack, sizeof(c_stack), nullptr, 0); +#else emscripten_fiber_init(&context, entry, arg, c_stack, sizeof(c_stack), asyncify_stack, sizeof(asyncify_stack)); +#endif } void init_manually(em_arg_callback_func entry, void *arg) { @@ -40,7 +44,11 @@ static struct Globals { Fiber fibers[2]; Globals() { +#ifdef JSPI + emscripten_fiber_init_from_current_context(&main, nullptr, 0); +#else emscripten_fiber_init_from_current_context(&main, asyncify_stack, sizeof(asyncify_stack)); +#endif } } G; @@ -87,6 +95,22 @@ static void g(void *arg) { abort(); } +static void h2(void *arg) { + int *p = (int*)arg; + *p += 10; + // Swap directly back to fiber 0 without going through main + emscripten_fiber_swap(&G.fibers[1].context, &G.fibers[0].context); +} + +static void h1(void *arg) { + int *p = (int*)arg; + *p += 5; + // Swap directly to fiber 1 + emscripten_fiber_swap(&G.fibers[0].context, &G.fibers[1].context); + *p += 20; + emscripten_fiber_swap(&G.fibers[0].context, &G.main); +} + int main(int argc, char **argv) { int i; G.fibers[0].init_with_api(f, &i); @@ -98,7 +122,12 @@ int main(int argc, char **argv) { emscripten_fiber_swap(&G.main, &G.fibers[1].context); printf("%d-", i); } - printf("*\n"); + + int val = 1000; + G.fibers[0].init_with_api(h1, &val); + G.fibers[1].init_with_api(h2, &val); + emscripten_fiber_swap(&G.main, &G.fibers[0].context); + printf("direct-%d-*\n", val); return 0; } From c0a3763f9a711e3043fb7e35c60445622197aed3 Mon Sep 17 00:00:00 2001 From: Brendan Dahl Date: Tue, 1 Sep 2026 20:44:15 +0000 Subject: [PATCH 2/2] review comments --- src/lib/libasync.js | 8 +++++--- system/include/emscripten/fiber.h | 8 ++++---- system/lib/libc/emscripten_fiber.c | 4 ++-- test/test_fibers.cpp | 1 + 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/lib/libasync.js b/src/lib/libasync.js index b9fc478a27029..2786be292597e 100644 --- a/src/lib/libasync.js +++ b/src/lib/libasync.js @@ -598,9 +598,11 @@ addToLibrary({ writeStackCookie(); #endif #if ASYNCIFY_DEBUG - dbg('ASYNCIFY/FIBER: entering fiber', newFiber, 'for the first time'); + dbg(`ASYNCIFY/FIBER: entering fiber ${newFiber} for the first time`); #endif var userData = {{{ makeGetValue('newFiber', C_STRUCTS.emscripten_fiber_s.user_data, '*') }}}; + // makeDynCall with promising=true wraps entryPoint in WebAssembly.promising, + // guaranteeing that start() returns a Promise. var start = {{{ makeDynCall('vp', 'entryPoint', true) }}}; start(userData).catch((e) => { abort(String(e)); @@ -611,7 +613,7 @@ addToLibrary({ assert(resume, `fiber ${newFiber} is not suspended`); #endif #if ASYNCIFY_DEBUG - dbg('ASYNCIFY/FIBER: resume fiber', newFiber); + dbg(`ASYNCIFY/FIBER: resume fiber ${newFiber}`); #endif Fibers.fiberResolvers.delete(newFiber); resume(); @@ -675,7 +677,7 @@ addToLibrary({ assert(newFiber, 'emscripten_fiber_swap: newFiber must not be null'); #endif #if ASYNCIFY_DEBUG - dbg('ASYNCIFY/FIBER: swap', oldFiber, '->', newFiber); + dbg(`ASYNCIFY/FIBER: swap ${oldFiber} -> ${newFiber}`); #endif if (oldFiber === newFiber) return; diff --git a/system/include/emscripten/fiber.h b/system/include/emscripten/fiber.h index 2d2fbe7f77759..414885abcac57 100644 --- a/system/include/emscripten/fiber.h +++ b/system/include/emscripten/fiber.h @@ -33,7 +33,7 @@ typedef struct emscripten_fiber_s { /** * Initializes a fiber context. - * Under JSPI (-sJSPI), asyncify_stack may be NULL and asyncify_stack_size 0. + * Under JSPI (-sJSPI), asyncify_stack and asyncify_stack_size are ignored. */ void emscripten_fiber_init( emscripten_fiber_t * _Nonnull fiber, @@ -41,17 +41,17 @@ void emscripten_fiber_init( void *entry_func_arg, void * _Nonnull c_stack, size_t c_stack_size, - void * _Nullable asyncify_stack, + void *asyncify_stack, size_t asyncify_stack_size ); /** * Partially initializes a fiber based on the currently active context. - * Under JSPI (-sJSPI), asyncify_stack may be NULL and asyncify_stack_size 0. + * Under JSPI (-sJSPI), asyncify_stack and asyncify_stack_size are ignored. */ void emscripten_fiber_init_from_current_context( emscripten_fiber_t * _Nonnull fiber, - void * _Nullable asyncify_stack, + void *asyncify_stack, size_t asyncify_stack_size ); diff --git a/system/lib/libc/emscripten_fiber.c b/system/lib/libc/emscripten_fiber.c index b11642457582b..bdd9f8262ce31 100644 --- a/system/lib/libc/emscripten_fiber.c +++ b/system/lib/libc/emscripten_fiber.c @@ -12,7 +12,7 @@ void emscripten_fiber_init( void *entry_func_arg, void *c_stack, size_t c_stack_size, - void * _Nullable asyncify_stack, + void *asyncify_stack, size_t asyncify_stack_size ) { char *c_stack_base = (char*)c_stack + c_stack_size; @@ -27,7 +27,7 @@ void emscripten_fiber_init( void emscripten_fiber_init_from_current_context( emscripten_fiber_t *fiber, - void * _Nullable asyncify_stack, + void *asyncify_stack, size_t asyncify_stack_size ) { fiber->stack_base = (void*)emscripten_stack_get_base(); diff --git a/test/test_fibers.cpp b/test/test_fibers.cpp index 7d34df55240fc..00e2c8b9497da 100644 --- a/test/test_fibers.cpp +++ b/test/test_fibers.cpp @@ -123,6 +123,7 @@ int main(int argc, char **argv) { printf("%d-", i); } + // Test swapping directly between two child fibers without returning to main. int val = 1000; G.fibers[0].init_with_api(h1, &val); G.fibers[1].init_with_api(h2, &val);