diff --git a/src/lib/libasync.js b/src/lib/libasync.js index c79e4f6bfccbf..f8c45be74e2c1 100644 --- a/src/lib/libasync.js +++ b/src/lib/libasync.js @@ -525,6 +525,10 @@ addToLibrary({ $Fibers__deps: ['$Asyncify', 'emscripten_stack_set_limits', '$stackRestore'], $Fibers: { +#if ASYNCIFY == 2 + lastContinuationId: 0, + continuations: [], +#endif nextFiber: 0, trampolineRunning: false, trampoline() { @@ -555,6 +559,32 @@ addToLibrary({ stackRestore({{{ makeGetValue('newFiber', C_STRUCTS.emscripten_fiber_s.stack_ptr, '*') }}}); +#if ASYNCIFY == 2 + const continuationId = {{{ makeGetValue('newFiber', C_STRUCTS.emscripten_fiber_s.asyncify_data + C_STRUCTS.asyncify_data_s.rewind_id, 'i32') }}}; + + if (continuationId !== 0) { +#if ASYNCIFY_DEBUG + dbg('ASYNCIFY/FIBER: resuming fiber', newFiber); +#endif + const continuation = Fibers.continuations[continuationId]; + Fibers.continuations[continuationId] = null; + continuation(); + } else { + var entryPoint = {{{ makeGetValue('newFiber', C_STRUCTS.emscripten_fiber_s.entry, '*') }}}; + +#if ASYNCIFY_DEBUG + dbg('ASYNCIFY/FIBER: entering fiber', newFiber, 'for the first time'); +#endif + +#if STACK_OVERFLOW_CHECK + writeStackCookie(); +#endif + + var userData = {{{ makeGetValue('newFiber', C_STRUCTS.emscripten_fiber_s.user_data, '*') }}}; + + {{{ makeDynCall('vp', 'entryPoint', true) }}}(userData); + } +#else // ASYNCIFY == 1 var entryPoint = {{{ makeGetValue('newFiber', C_STRUCTS.emscripten_fiber_s.entry, '*') }}}; if (entryPoint !== 0) { @@ -580,6 +610,7 @@ addToLibrary({ _asyncify_start_rewind(asyncifyData); Asyncify.doRewind(asyncifyData); } +#endif // ASYNCIFY == 1 }, }, @@ -588,8 +619,36 @@ addToLibrary({ emscripten_fiber_swap: (oldFiber, newFiber) => { if (ABORT) return; #if ASYNCIFY_DEBUG - dbg('ASYNCIFY/FIBER: swap', oldFiber, '->', newFiber, 'state:', Asyncify.state); + dbg('ASYNCIFY/FIBER: swap', oldFiber, '->', newFiber, +#if ASYNCIFY == 1 + 'state:', Asyncify.state #endif + ); +#endif + +#if ASYNCIFY == 2 + var stackTop = stackSave(); + {{{ makeSetValue('oldFiber', C_STRUCTS.emscripten_fiber_s.stack_ptr, 'stackTop', '*') }}}; + + Fibers.nextFiber = newFiber; + + let continuationId = {{{ makeGetValue('oldFiber', C_STRUCTS.emscripten_fiber_s.asyncify_data + C_STRUCTS.asyncify_data_s.rewind_id, 'i32') }}}; + if (!continuationId) { + Fibers.lastContinuationId++; + continuationId = Fibers.lastContinuationId; + } + + {{{ makeSetValue('oldFiber', C_STRUCTS.emscripten_fiber_s.asyncify_data + C_STRUCTS.asyncify_data_s.rewind_id, 'continuationId', 'i32') }}}; + + const promise = new Promise(resolve => { Fibers.continuations[continuationId] = resolve; }); + + if (!Fibers.trampolineRunning) { + Promise.resolve().then(Fibers.trampoline); + } + + return promise; + +#else // ASYNCIFY == 1 if (Asyncify.state === Asyncify.State.Normal) { Asyncify.state = Asyncify.State.Unwinding; @@ -617,6 +676,7 @@ addToLibrary({ _asyncify_stop_rewind(); Asyncify.currData = null; } +#endif // ASYNCIFY == 1 }, #else // ASYNCIFY emscripten_sleep: () => { diff --git a/test/test_core.py b/test/test_core.py index f7b5c40688cf4..d6a51bb5bb34b 100644 --- a/test/test_core.py +++ b/test/test_core.py @@ -8404,12 +8404,25 @@ 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): - self.set_setting('ASYNCIFY') + @with_asyncify_and_jspi + def test_fibers(self): 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-*') + # Test that fibers do not break setjmp/longjmp, ensuring longjmp works after + # switching back from another fiber + @parameterized({ + '': (0,), + 'legacy': (1,), + }) + def test_fibers_setjmp(self, legacy): + self.maybe_closure() + self.set_setting('ASYNCIFY', 2) + self.require_jspi() + self.set_setting('SUPPORT_LONGJMP', 'wasm') + self.set_setting('WASM_LEGACY_EXCEPTIONS', legacy) + self.do_runf('test_fibers_setjmp.cpp', 'main: prejump\nf1: start arg:50\nmain: continuing1\nf1: cont1\nf2: start arg:60\nf1: cont2\nf2: cont1\nf2: exc 11\nmain: continuing2\nf2: cont2\nf1: cont3\nmain: continuing3\nmain: got exc 1\n') + @with_asyncify_and_jspi def test_asyncify_unused(self): # test a program not using asyncify, but the pref is set diff --git a/test/test_fibers_setjmp.cpp b/test/test_fibers_setjmp.cpp new file mode 100644 index 0000000000000..2024595c506dc --- /dev/null +++ b/test/test_fibers_setjmp.cpp @@ -0,0 +1,117 @@ +// Copyright 2025 The Emscripten Authors. All rights reserved. +// Emscripten is available under two separate licenses, the MIT license and the +// University of Illinois/NCSA Open Source License. Both these licenses can be +// found in the LICENSE file. + +#include +#include +#include +#include + +struct Fiber { + emscripten_fiber_t context; + char asyncify_stack[1024]; + alignas(16) char c_stack[4096]; + + void init_current() { + emscripten_fiber_init_from_current_context(&context, asyncify_stack, sizeof(asyncify_stack)); + } + + void init(em_arg_callback_func entry, void *arg) { + emscripten_fiber_init(&context, entry, arg, c_stack, sizeof(c_stack), asyncify_stack, sizeof(asyncify_stack)); + } + + void swap(Fiber *fiber) { + emscripten_fiber_swap(&context, &fiber->context); + } +}; + +static struct Globals { + Fiber main; + Fiber f1; + Fiber f2; + + jmp_buf buffer; + jmp_buf buffer2; +} G; + +[[noreturn]] +static void f1(void *arg) { + printf("f1: start arg:%d\n", *(int*)arg); + + G.f1.swap(&G.main); + + printf("f1: cont1\n"); + + G.f1.swap(&G.f2); + + printf("f1: cont2\n"); + + G.f1.swap(&G.f2); + + printf("f1: cont3\n"); + + G.f1.swap(&G.main); + abort(); +} + +[[noreturn]] +static void f2(void *arg) { + printf("f2: start arg:%d\n", *(int*)arg); + + int exc; + if ((exc = setjmp(G.buffer2)) == 0) { + G.f2.swap(&G.f1); + + printf("f2: cont1\n"); + + longjmp(G.buffer2, 11); + + printf("f2: not visible\n"); + } else { + printf("f2: exc %d\n", exc); + } + + G.f2.swap(&G.main); + + printf("f2: cont2\n"); + + G.f2.swap(&G.f1); + abort(); +} + +int main(int argc, char **argv) { + G.main.init_current(); + + int test = 50; + G.f1.init(f1, &test); + + int test2 = 60; + G.f2.init(f2, &test2); + + int exc; + if ((exc = setjmp(G.buffer)) == 0) { + printf("main: prejump\n"); + + G.main.swap(&G.f1); + + printf("main: continuing1\n"); + + G.main.swap(&G.f1); + + printf("main: continuing2\n"); + + G.main.swap(&G.f2); + + printf("main: continuing3\n"); + + longjmp(G.buffer, 1); + + printf("main: not visible\n"); + + } else { + printf("main: got exc %d\n", exc); + } + + return 0; +}