From 0984b2de53a06d091b0b92b7e1b9e92d9db49b38 Mon Sep 17 00:00:00 2001 From: zb3 Date: Fri, 29 Aug 2025 23:51:13 +0200 Subject: [PATCH 1/4] Add basic support for Fibers with JSPI --- src/lib/libasync.js | 60 +++++++++++++++++- test/test_core.py | 17 +++++- test/test_fibers_setjmp.cpp | 117 ++++++++++++++++++++++++++++++++++++ 3 files changed, 190 insertions(+), 4 deletions(-) create mode 100644 test/test_fibers_setjmp.cpp diff --git a/src/lib/libasync.js b/src/lib/libasync.js index c79e4f6bfccbf..47f5c21eb0d38 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,30 @@ 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 + Fibers.continuations[continuationId](); + } 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 +608,7 @@ addToLibrary({ _asyncify_start_rewind(asyncifyData); Asyncify.doRewind(asyncifyData); } +#endif // ASYNCIFY == 1 }, }, @@ -588,8 +617,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) { + queueMicrotask(Fibers.trampoline); + } + + return promise; + +#else // ASYNCIFY == 1 if (Asyncify.state === Asyncify.State.Normal) { Asyncify.state = Asyncify.State.Unwinding; @@ -617,6 +674,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..224a67e2bb668 100644 --- a/test/test_core.py +++ b/test/test_core.py @@ -8404,12 +8404,23 @@ 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-*') + @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; +} From 46d377b84af5b9386518cbcc28cdf456e3bcf2d5 Mon Sep 17 00:00:00 2001 From: zb3 Date: Sat, 30 Aug 2025 01:32:14 +0200 Subject: [PATCH 2/4] Don't depend on queueMicrotask d8 used for tests doesn't provide that --- src/lib/libasync.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/libasync.js b/src/lib/libasync.js index 47f5c21eb0d38..64aac20f78b00 100644 --- a/src/lib/libasync.js +++ b/src/lib/libasync.js @@ -641,7 +641,7 @@ addToLibrary({ const promise = new Promise(resolve => { Fibers.continuations[continuationId] = resolve; }); if (!Fibers.trampolineRunning) { - queueMicrotask(Fibers.trampoline); + Promise.resolve().then(Fibers.trampoline); } return promise; From 8d10a100401654a5531839ef0ff0b74d0a243b3c Mon Sep 17 00:00:00 2001 From: zb3 Date: Thu, 11 Sep 2025 15:27:59 +0200 Subject: [PATCH 3/4] Don't keep references to used continuations --- src/lib/libasync.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/lib/libasync.js b/src/lib/libasync.js index 64aac20f78b00..f8c45be74e2c1 100644 --- a/src/lib/libasync.js +++ b/src/lib/libasync.js @@ -566,7 +566,9 @@ addToLibrary({ #if ASYNCIFY_DEBUG dbg('ASYNCIFY/FIBER: resuming fiber', newFiber); #endif - Fibers.continuations[continuationId](); + const continuation = Fibers.continuations[continuationId]; + Fibers.continuations[continuationId] = null; + continuation(); } else { var entryPoint = {{{ makeGetValue('newFiber', C_STRUCTS.emscripten_fiber_s.entry, '*') }}}; From f28a33893607ed9e761fc08bc472a7972c6a5473 Mon Sep 17 00:00:00 2001 From: zb3 Date: Thu, 11 Sep 2025 15:49:12 +0200 Subject: [PATCH 4/4] Briefly explain the test --- test/test_core.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/test_core.py b/test/test_core.py index 224a67e2bb668..d6a51bb5bb34b 100644 --- a/test/test_core.py +++ b/test/test_core.py @@ -8409,6 +8409,8 @@ 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,),