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..2786be292597e 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,60 @@ 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, '*') }}}; + // 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)); + }); + } 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 +669,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..414885abcac57 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 and asyncify_stack_size are ignored. + */ 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 *asyncify_stack, size_t asyncify_stack_size ); +/** + * Partially initializes a fiber based on the currently active context. + * Under JSPI (-sJSPI), asyncify_stack and asyncify_stack_size are ignored. + */ void emscripten_fiber_init_from_current_context( emscripten_fiber_t * _Nonnull fiber, - void * _Nonnull asyncify_stack, + void *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..bdd9f8262ce31 100644 --- a/system/lib/libc/emscripten_fiber.c +++ b/system/lib/libc/emscripten_fiber.c @@ -22,7 +22,7 @@ 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( @@ -34,5 +34,5 @@ void emscripten_fiber_init_from_current_context( 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..00e2c8b9497da 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,13 @@ int main(int argc, char **argv) { emscripten_fiber_swap(&G.main, &G.fibers[1].context); printf("%d-", i); } - printf("*\n"); + + // 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); + emscripten_fiber_swap(&G.main, &G.fibers[0].context); + printf("direct-%d-*\n", val); return 0; }