From 3a703e790c6154673506b57e07225fc095a89600 Mon Sep 17 00:00:00 2001 From: Brendan Dahl Date: Mon, 31 Aug 2026 17:10:20 +0000 Subject: [PATCH 1/3] 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 e09084933ccd9..fd3275a3d1bf8 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -29,6 +29,9 @@ See docs/process.md for more on how version tagging works. performed when the linker inputs carry the wasm-bindgen Emscripten marker section, so `-sWASM_BINDGEN` can safely be passed to non-wasm-bindgen builds. (#27208) +- 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 ff54c14942cec..f714ad60d1952 100644 --- a/test/test_core.py +++ b/test/test_core.py @@ -8450,11 +8450,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 7e31829ed3975852215c7c5035c417502f4c28a5 Mon Sep 17 00:00:00 2001 From: Brendan Dahl Date: Tue, 1 Sep 2026 20:44:15 +0000 Subject: [PATCH 2/3] 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); From 97d99515b09c86132e0a0496e4388d1f7e1f07aa Mon Sep 17 00:00:00 2001 From: Brendan Dahl Date: Tue, 8 Sep 2026 23:17:05 +0000 Subject: [PATCH 3/3] use fiber id --- site/source/docs/api_reference/fiber.h.rst | 2 +- src/lib/libasync.js | 29 ++++++++++++++++------ system/include/emscripten/fiber.h | 2 +- system/lib/libc/emscripten_fiber.c | 2 ++ test/test_core.py | 2 +- test/test_fibers.cpp | 26 +++++++++++++++++++ 6 files changed, 52 insertions(+), 11 deletions(-) diff --git a/site/source/docs/api_reference/fiber.h.rst b/site/source/docs/api_reference/fiber.h.rst index 7abb70964bd72..796b72bf2002d 100644 --- a/site/source/docs/api_reference/fiber.h.rst +++ b/site/source/docs/api_reference/fiber.h.rst @@ -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 under Asyncify (unused under JSPI). + Asyncify data structure. Used to unwind and rewind the call stack when switching fibers under Asyncify (under JSPI, only rewind_id is used). .. c:type:: asyncify_data_t diff --git a/src/lib/libasync.js b/src/lib/libasync.js index 2786be292597e..1f03a0893246f 100644 --- a/src/lib/libasync.js +++ b/src/lib/libasync.js @@ -586,10 +586,21 @@ addToLibrary({ }, #elif ASYNCIFY == 2 fiberResolvers: new Map(), + nextFiberId: 0, + + allocateFiberId() { + do { + // Keep IDs positive and non-zero (fits in signed i32 rewind_id, with 0 reserved). + Fibers.nextFiberId = (Fibers.nextFiberId + 1) & 0x7fffffff || 1; + } while (Fibers.fiberResolvers.has(Fibers.nextFiberId)); + return Fibers.nextFiberId; + }, swap(oldFiber, newFiber) { return new Promise((resolve) => { - Fibers.fiberResolvers.set(oldFiber, resolve); + var oldId = Fibers.allocateFiberId(); + {{{ makeSetValue('oldFiber', C_STRUCTS.emscripten_fiber_s.asyncify_data + C_STRUCTS.asyncify_data_s.rewind_id, 'oldId', 'i32') }}}; + Fibers.fiberResolvers.set(oldId, resolve); var entryPoint = {{{ makeGetValue('newFiber', C_STRUCTS.emscripten_fiber_s.entry, '*') }}}; if (entryPoint) { {{{ makeSetValue('newFiber', C_STRUCTS.emscripten_fiber_s.entry, 0, '*') }}}; @@ -608,15 +619,17 @@ addToLibrary({ abort(String(e)); }); } else { - var resume = Fibers.fiberResolvers.get(newFiber); + var newId = {{{ makeGetValue('newFiber', C_STRUCTS.emscripten_fiber_s.asyncify_data + C_STRUCTS.asyncify_data_s.rewind_id, 'i32') }}}; + var resume = Fibers.fiberResolvers.get(newId); #if ASSERTIONS - assert(resume, `fiber ${newFiber} is not suspended`); + assert(resume, `fiber ${newFiber} (id ${newId}) is not suspended`); #endif #if ASYNCIFY_DEBUG - dbg(`ASYNCIFY/FIBER: resume fiber ${newFiber}`); + dbg(`ASYNCIFY/FIBER: resume fiber ${newFiber} (id ${newId})`); #endif - Fibers.fiberResolvers.delete(newFiber); - resume(); + Fibers.fiberResolvers.delete(newId); + {{{ makeSetValue('newFiber', C_STRUCTS.emscripten_fiber_s.asyncify_data + C_STRUCTS.asyncify_data_s.rewind_id, 0, 'i32') }}}; + resume(newFiber); } }); }, @@ -684,9 +697,9 @@ addToLibrary({ var stackTop = stackSave(); {{{ makeSetValue('oldFiber', C_STRUCTS.emscripten_fiber_s.stack_ptr, 'stackTop', '*') }}}; - await Fibers.swap(oldFiber, newFiber); + var resumedFiber = await Fibers.swap(oldFiber, newFiber); - Fibers.restoreStack(oldFiber); + Fibers.restoreStack(resumedFiber); }, #endif #else // ASYNCIFY diff --git a/system/include/emscripten/fiber.h b/system/include/emscripten/fiber.h index 414885abcac57..ecabaa634390c 100644 --- a/system/include/emscripten/fiber.h +++ b/system/include/emscripten/fiber.h @@ -28,7 +28,7 @@ typedef struct emscripten_fiber_s { 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 (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 structure (unused under JSPI). */ + asyncify_data_t asyncify_data; /** Asyncify data structure (under JSPI, only rewind_id is used). */ } emscripten_fiber_t; /** diff --git a/system/lib/libc/emscripten_fiber.c b/system/lib/libc/emscripten_fiber.c index bdd9f8262ce31..15068e2e1a166 100644 --- a/system/lib/libc/emscripten_fiber.c +++ b/system/lib/libc/emscripten_fiber.c @@ -23,6 +23,7 @@ void emscripten_fiber_init( fiber->user_data = entry_func_arg; fiber->asyncify_data.stack_ptr = asyncify_stack; fiber->asyncify_data.stack_limit = asyncify_stack ? (char*)asyncify_stack + asyncify_stack_size : NULL; + fiber->asyncify_data.rewind_id = 0; } void emscripten_fiber_init_from_current_context( @@ -35,4 +36,5 @@ void emscripten_fiber_init_from_current_context( fiber->entry = NULL; fiber->asyncify_data.stack_ptr = asyncify_stack; fiber->asyncify_data.stack_limit = asyncify_stack ? (char*)asyncify_stack + asyncify_stack_size : NULL; + fiber->asyncify_data.rewind_id = 0; } diff --git a/test/test_core.py b/test/test_core.py index f714ad60d1952..308b8626ba54d 100644 --- a/test/test_core.py +++ b/test/test_core.py @@ -8455,7 +8455,7 @@ 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') + 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-*\nmove-342-*\n') def test_fibers_asyncify_null_stack(self): self.set_setting('ASYNCIFY') diff --git a/test/test_fibers.cpp b/test/test_fibers.cpp index 00e2c8b9497da..2121948cf3eb0 100644 --- a/test/test_fibers.cpp +++ b/test/test_fibers.cpp @@ -7,6 +7,7 @@ #include #include +#include #include struct Fiber { @@ -111,6 +112,17 @@ static void h1(void *arg) { emscripten_fiber_swap(&G.fibers[0].context, &G.main); } +static Fiber *dyn_fiber_ptr = nullptr; + +static void relocate_test(void *arg) { + int *p = (int*)arg; + *p += 100; + // Swap back to main + emscripten_fiber_swap(&dyn_fiber_ptr->context, &G.main); + *p += 200; + emscripten_fiber_swap(&dyn_fiber_ptr->context, &G.main); +} + int main(int argc, char **argv) { int i; G.fibers[0].init_with_api(f, &i); @@ -130,5 +142,19 @@ int main(int argc, char **argv) { emscripten_fiber_swap(&G.main, &G.fibers[0].context); printf("direct-%d-*\n", val); + // Test moving/reallocating a suspended fiber context structure. + dyn_fiber_ptr = (Fiber*)malloc(sizeof(Fiber)); + int move_val = 42; + dyn_fiber_ptr->init_with_api(relocate_test, &move_val); + emscripten_fiber_swap(&G.main, &dyn_fiber_ptr->context); + // Relocate to a new address while suspended. + Fiber *new_ptr = (Fiber*)malloc(sizeof(Fiber) * 4); + memcpy(&new_ptr[2], dyn_fiber_ptr, sizeof(Fiber)); + free(dyn_fiber_ptr); + dyn_fiber_ptr = &new_ptr[2]; + emscripten_fiber_swap(&G.main, &dyn_fiber_ptr->context); + printf("move-%d-*\n", move_val); + free(new_ptr); + return 0; }