-
Notifications
You must be signed in to change notification settings - Fork 3.5k
Support fiber API with JSPI #27638
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Support fiber API with JSPI #27638
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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() { | ||
|
|
@@ -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, '*') }}}; | ||
|
|
||
|
|
@@ -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; | ||
|
|
||
|
|
@@ -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); | ||
| 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) => { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does this mean the ``entryPoint` must return a Promise? Is that guaranteed?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The third arg is |
||
| 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 | ||
|
|
@@ -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'); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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) { | ||
|
|
@@ -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; | ||
|
|
||
|
|
@@ -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); | ||
|
|
@@ -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); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
| } | ||
There was a problem hiding this comment.
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:
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 therewind_idfield ofasyncify_data_t(embedded intoemscripten_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?