Add JSPI lifecycle hooks (<emscripten/jspi.h>) - #27698
Open
guybedford wants to merge 2 commits into
Open
Conversation
guybedford
force-pushed
the
jspi-hooks
branch
2 times, most recently
from
September 11, 2026 19:16
4255fca to
68c8134
Compare
Under -sJSPI every call to a promising export starts a fiber that may be suspended while its suspending imports await, with other fibers running on the same thread in between. Libraries with activation-affine state need to know when that happens. This adds the experimental JSPI_HOOKS setting and <emscripten/jspi.h>: jspi_register(fn, mask) registers a hook for JSPI_ENTER, JSPI_EXIT, JSPI_SUSPEND and JSPI_RESUME events. Each hook has its own per-fiber token: NULL at the first event it sees for a fiber, then whatever it returned at that fiber's previous event, so per-fiber state needs no lookup. EXIT and RESUME also report whether the export or import completed with an exception. The hooks run inside the fiber's own wasm frames, immediately before/after the boundary call, through wrappers that the new binaryen --jspi-hooks pass places around every promising export and suspending import at link time (a JS wrapper only observes the transition a microtask later, when another fiber may already have run). The pass delivers the events to the __jspi_enter/exit/suspend/resume exports provided by the runtime (libjspi), forwarding the i64 token returned by the "before" hook of a pair to the "after" hook through a wasm local; the runtime's token is a pointer to its fiber record. The wrapped import set is exactly the set the JS wraps in WebAssembly.Suspending, including __async JS library functions. The runtime keeps one record per live fiber, holding the hooks' tokens and whoever entered or last resumed the fiber, which becomes current again when the fiber leaves at a suspension or exit, so user JS may call WebAssembly.promising on a wrapped export directly. With the hooks enabled, function pointers made promising from JS (dynCall with promising=true, makeDynCall, embind async, the pthread entry point) go through per-signature __jspi_dyncall_<sig> trampoline exports generated by the pass for the signatures in the table, so no fiber runs without its hooks. On hello-world + emscripten_sleep at -O2 the hooks cost about 800 bytes of wasm and 130 bytes of JS. Independently of the setting, invoke_* imports are no longer treated as suspending under JSPI, since JSPI cannot suspend across their JS frame.
guybedford
force-pushed
the
jspi-hooks
branch
from
September 11, 2026 19:54
68c8134 to
5e7c6bc
Compare
…ping The hooks pass emits its wrappers in the module's exception handling flavor and refuses a module carrying both. With WASM_LEGACY_EXCEPTIONS=0 the module may still contain legacy instructions from prebuilt inputs, so run --translate-to-exnref first.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This implements
<emscripten/jspi.h>, lifecycle hooks for-sJSPIfibers, backed by a Binaryen--jspi-hookspass posted in WebAssembly/binaryen#9102.The general idea here is that many systems need to interoperate on the JSPI context switching model and so there are two ways to do that:
For example, wasm-bindgen just released support for JSPI, and we now have the very real problem that wasm-bindgen JSPI does not interoperate with Emscripten JSPI.
This first PR just integrates a runtime hooks layer that allows C bindings to add event handlers for the JSPI lifecycle hooks in a sound way - enter/exit/suspend/resume. It handles exceptions, supports passing a token through the hook lifecycle of
enter - (suspend - return)* - exit, and is gated behind a new experimental-sJSPI_HOOKS.The runtime API takes the following shape in
emscripten/jspi.h:JSPI_ENTER,JSPI_EXIT,JSPI_SUSPEND,JSPI_RESUME0is returned on success,-1if no hooks API is present (no-sJSPI_HOOKSprovided), and-2if unable to register the hook (currently limited to a maximum of 64 hooks).When built without
-sJSPI_HOOKS, the register function is stubbed out and always returns-1.The
jspi_hookcallback takes the following form:Where
This then lays the foundation for a follow-on
-sREENTRANT_JSPIsupport for shadow stack switching.Made with AI assistance under my review