Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 61 additions & 1 deletion src/lib/libasync.js
Original file line number Diff line number Diff line change
Expand Up @@ -525,6 +525,10 @@ addToLibrary({

$Fibers__deps: ['$Asyncify', 'emscripten_stack_set_limits', '$stackRestore'],
$Fibers: {
#if ASYNCIFY == 2
lastContinuationId: 0,
continuations: [],

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like we add to this but never remove anything.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you mean nullifying the old continuation references [commited] or was it about the fact that ids only grow but are never reclaimed? Ids work just like rewindId and I don't see callStackIdToFunc being cleared either.. (not that it's perfect)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I meant nullyfing. The overflow is a separate issue, but seeing at it goes 2^53 it's probably fine.

#endif
nextFiber: 0,
trampolineRunning: false,
trampoline() {
Expand Down Expand Up @@ -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) {
Expand All @@ -580,6 +610,7 @@ addToLibrary({
_asyncify_start_rewind(asyncifyData);
Asyncify.doRewind(asyncifyData);
}
#endif // ASYNCIFY == 1
},
},

Expand All @@ -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;

Expand Down Expand Up @@ -617,6 +676,7 @@ addToLibrary({
_asyncify_stop_rewind();
Asyncify.currData = null;
}
#endif // ASYNCIFY == 1
},
#else // ASYNCIFY
emscripten_sleep: () => {
Expand Down
19 changes: 16 additions & 3 deletions test/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We probably still want this?

@zb3 zb3 Sep 25, 2025

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought with_asyncify_and_jspi includes that check, take a look:

def with_asyncify_and_jspi(f):
assert callable(f)
@wraps(f)
def metafunc(self, jspi, *args, **kwargs):
if self.get_setting('WASM_ESM_INTEGRATION'):
self.skipTest('WASM_ESM_INTEGRATION is not compatible with ASYNCIFY')
if jspi:
self.set_setting('ASYNCIFY', 2)
self.require_jspi()
else:
self.set_setting('ASYNCIFY')
f(self, *args, **kwargs)
parameterize(metafunc, {'': (False,),
'jspi': (True,)})
return metafunc

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()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a brief comment explaining what the test is doing.

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
Expand Down
117 changes: 117 additions & 0 deletions test/test_fibers_setjmp.cpp
Original file line number Diff line number Diff line change
@@ -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 <stdio.h>
#include <stdlib.h>
#include <setjmp.h>
#include <emscripten/fiber.h>

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;
}