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 @@ -29,6 +29,9 @@ See docs/process.md for more on how version tagging works.
performed when the linker inputs carry the wasm-bindgen Emscripten marker
section, so `-sWASM_BINDGEN` can safely be passed to non-wasm-bindgen builds.
(#27208)
- 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 (under JSPI, only rewind_id is used).

.. 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.
114 changes: 103 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,73 @@ addToLibrary({
Asyncify.doRewind(asyncifyData);
}
},
#elif ASYNCIFY == 2
fiberResolvers: new Map(),
nextFiberId: 0,

allocateFiberId() {
do {
// Keep IDs positive and non-zero (fits in signed i32 rewind_id, with 0 reserved).
Fibers.nextFiberId = (Fibers.nextFiberId + 1) & 0x7fffffff || 1;
} while (Fibers.fiberResolvers.has(Fibers.nextFiberId));
return Fibers.nextFiberId;
},

swap(oldFiber, newFiber) {
return new Promise((resolve) => {
var oldId = Fibers.allocateFiberId();
{{{ makeSetValue('oldFiber', C_STRUCTS.emscripten_fiber_s.asyncify_data + C_STRUCTS.asyncify_data_s.rewind_id, 'oldId', 'i32') }}};
Fibers.fiberResolvers.set(oldId, 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) => {
Comment thread
brendandahl marked this conversation as resolved.
abort(String(e));
});
} else {
var newId = {{{ makeGetValue('newFiber', C_STRUCTS.emscripten_fiber_s.asyncify_data + C_STRUCTS.asyncify_data_s.rewind_id, 'i32') }}};
var resume = Fibers.fiberResolvers.get(newId);
#if ASSERTIONS
assert(resume, `fiber ${newFiber} (id ${newId}) is not suspended`);
#endif
#if ASYNCIFY_DEBUG
dbg(`ASYNCIFY/FIBER: resume fiber ${newFiber} (id ${newId})`);
#endif
Fibers.fiberResolvers.delete(newId);
{{{ makeSetValue('newFiber', C_STRUCTS.emscripten_fiber_s.asyncify_data + C_STRUCTS.asyncify_data_s.rewind_id, 0, 'i32') }}};
resume(newFiber);
}
});
},
#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 +682,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', '*') }}};

var resumedFiber = await Fibers.swap(oldFiber, newFiber);

Fibers.restoreStack(resumedFiber);
},
#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 (under JSPI, only rewind_id is used). */
} 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
6 changes: 4 additions & 2 deletions system/lib/libc/emscripten_fiber.c
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ 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;
fiber->asyncify_data.rewind_id = 0;
}

void emscripten_fiber_init_from_current_context(
Expand All @@ -34,5 +35,6 @@ 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;
fiber->asyncify_data.rewind_id = 0;
}
28 changes: 25 additions & 3 deletions test/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -8450,11 +8450,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-*\nmove-342-*\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
Loading
Loading