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
3 changes: 3 additions & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
----------------
Expand Down
27 changes: 15 additions & 12 deletions site/source/docs/api_reference/fiber.h.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ fiber.h
co-operative threads of execution. The `fiber.h
<https://github.com/emscripten-core/emscripten/blob/main/system/include/emscripten/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
Expand Down Expand Up @@ -53,16 +53,16 @@ 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

Opaque pointer, passed as-is to :c:member:`emscripten_fiber_t.entry`.

.. 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

Expand Down Expand Up @@ -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.

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

Expand All @@ -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.
101 changes: 90 additions & 11 deletions src/lib/libasync.js
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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, '*') }}};

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

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'm not sure I like this dependency on the fiber struct's address. The docs state:

This structure represents a Fiber context continuation. The runtime does not keep references to these objects, they only contain information needed to perform the context switch. The switch operation updates some of the contents, however.

and that is true for the asyncify version. So it's possible to, e.g. realloc() an array of fibers without breaking anything. Perhaps you can fix this by reusing the rewind_id field of asyncify_data_t (embedded into emscripten_fiber_t), e.g. allocate an integer handle for each resolve and associate that instead of the address.

What happens when a fiber is discarded and never resumed though? Is there a zombie entry stuck in the map then?

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) => {

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.

Does this mean the ``entryPoint` must return a Promise? Is that guaranteed?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

The third arg is promising = true so this will always return a promise. Added a comment.

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
Expand Down Expand Up @@ -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');
Expand Down
18 changes: 13 additions & 5 deletions system/include/emscripten/fiber.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions system/lib/libc/emscripten_fiber.c
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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;
}
28 changes: 25 additions & 3 deletions test/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 <stdio.h>
#include <emscripten/fiber.h>

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):
Expand Down
32 changes: 31 additions & 1 deletion test/test_fibers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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;

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

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.

Why do we need this new test case? Were we missing coverage of this?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Yeah, the existing test only ever swapped back and forth between child fibers and the main fiber. There was no coverage for swapping directly between two child fibers without going through main. Added a comment to clarify this.


return 0;
}
Loading