Skip to content

feat: JSPI support for wasm32-unknown-emscripten - #1

Open
guybedford wants to merge 72 commits into
emscripten-targetfrom
emscripten-jspi
Open

guybedford wants to merge 72 commits into
emscripten-targetfrom
emscripten-jspi

Conversation

@guybedford

@guybedford guybedford commented Jul 15, 2026

Copy link
Copy Markdown
Owner

Motivation

Supporting the wasm32-unknown-emscripten platform.

Solution

This extends and is based to tokio-rs#8281, the single-threaded wasm32-unknown-emscripten target, to support JavaScript Promise Integration (JSPI)-based suspension.

Under a single-threaded build, Emscripten shares the host event loop with JavaScript, throwing on any blocking operations.

With JSPI, operations may suspend the Wasm execution on a JS promise allowing the runtime to be suspended but reentrant. For this reason, this is not like a wake, but instead multiple of these parked JSPI blocks can coexist and be woken in any order. When JSPI is enabled, and a block_on future would have to suspend (a timer deadline, an external wake, or a deferred yield_now's host turn), the calling Wasm stack parks on the host loop via JSPI-specific parking and resumes on the wake. Runtime::block_on, Handle::block_on, and the blocking_* sync APIs all behave as on native. The scheduler, task system, and time driver are all core Tokio. The Emscripten-specific aspect is just the hosted event loop drive loop and the JSPI park primitive over Emscripten's promise API, along with timer/keepalive glue. The parked stacks live in an Emscripten member of the existing runtime::context thread-local and no new public API is added.

spawn_blocking remains unsupported. Since tokio::fs and the stdio types do rely on it, the internal blocking shim completes their std calls inline, which is possible since Emscripten's filesystem syscalls are synchronous and there is nothing to offload. net is currently not included as it depends on unreleased Emscripten epoll work. When that is ready this PR can either be updated with that.

This PR does not include net functionality yet, as that depends on the Mio PR in tokio-rs/mio#1969. But extending this approach to full socket epolls is straightforward on top of this base as the next follow-on once the Mio PR lands.

Tests run in Node.js with two modes -

  1. The JSPI suite tests include features rt, time, sync, macros, fs, io-util, io-std, test-util as some emscripten-specific tests. There are only a couple of documented ignores (dup(), link() MEMFS limits, on_thread_park). - 744 passed, 0 failed, 12 ignored

  2. The non-JSPI tests remain limited pending deeper host event loop runtime work, a separate follow-on. - 4 passing, 0 failed.

@guybedford
guybedford force-pushed the emscripten-jspi branch 4 times, most recently from 6121ff9 to 3258304 Compare July 18, 2026 03:34
@guybedford
guybedford force-pushed the emscripten-jspi branch 2 times, most recently from bb7a461 to 23060f5 Compare August 26, 2026 01:05
@guybedford
guybedford force-pushed the emscripten-jspi branch 3 times, most recently from c83349a to 64c8614 Compare August 26, 2026 02:03
fly1d and others added 4 commits August 27, 2026 11:14
`github.event.pull_request.base.ref` is empty for push events, so the workspace semver check runs on `tokio-1.*.x` branches. Also check `github.ref_name` to preserve the intended release-branch exclusion while leaving pull request behavior unchanged.

Fixes: tokio-rs#8389
Per bytecodealliance/wasmtime#13558, Wasmtime v46.0.1
was the last release to support `wasm32-wasip1-threads`, so we use that for the
WASIp1 testing.

For WASIp2, we should be able to use any recent version of Wasmtime, but we pin
to a specific version anyway to avoid surprises.

(cherry picked from commit 5760ccd)
@guybedford
guybedford force-pushed the emscripten-jspi branch 2 times, most recently from b5e76c3 to 995f5ea Compare September 14, 2026 16:41
fallintoplace and others added 9 commits September 16, 2026 11:30
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4 to 7.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](actions/setup-node@v4...v7)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [mymindstorm/setup-emsdk](https://github.com/mymindstorm/setup-emsdk) from 14 to 16.
- [Release notes](https://github.com/mymindstorm/setup-emsdk/releases)
- [Commits](emscripten-core/setup-emsdk@v14...v16)

---
updated-dependencies:
- dependency-name: mymindstorm/setup-emsdk
  dependency-version: '16'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Alice Ryhl <aliceryhl@google.com>
The action moved from mymindstorm/setup-emsdk to the emscripten-core org.
Follow-on to tokio-rs#8281, supporting JSPI as an alternative approach to
suspension.

A `context::jspi` module provides the `__asyncjs__` suspension import
timer, the only suspending import, as well as a feature check for JSPI
being used. The park leaf suspends the whole Wasm stack on a host timer,
so the host loop runs while Tokio waits and resumes it when the timer
fires.

The scheduler, task system and time driver are the canonical ones,
tokio-macros is untouched, and no public API is added.

The scheduler's `event_interval` maintenance park arrives as a
zero-duration park and resumes through a host immediate, so a busy
scheduler still gives the host loop a turn, the role the non-blocking I/O
poll plays on native: the immediate it schedules next lands in the
following event loop iteration, which begins by running expired timers.
Without JSPI the zero-duration park is a no-op, as on native.
`setTimeout(0)` also works but is clamped to a millisecond, and a
microtask queue does not work at all, since those drain before the loop
advances.

The runtime stays entered while parked, as on native: a `block_on` from
another promising activation on the same thread during the park panics
as a nested runtime. Driving the runtime from several activations is a
follow-on.

Coverage expands from one run of the suite in which a timed wait blocked
the thread, plus a single pthread test file, to three lanes:

* non-JSPI: 4 passed, 0 failed
* JSPI: 788 passed, 0 failed, 10 ignored
* pthreads (`-pthread`): 762 passed, 0 failed, 10 ignored

In the process, the pthreads test suite is ungated as well, and changed
to share the blocking shim, since Emscripten has a sync-only FS.

Real-timer tests for Emscripten are consolidated into the single
`rt_emscripten_jspi.rs` file.

* `spawn_blocking` remains unsupported in non-pthread builds
* `net` (epoll, over tokio-rs/mio#1969) remains the follow-up
* Without `-sJSPI`, a non-zero wait now panics rather than blocking the
  thread through `std::thread::sleep`.
* A wait with no deadline panics in either mode. There is no reactor
  here, so host timers are the only mid-park wake source and nothing
  could ever deliver the wake.

Refs: tokio-rs#8281
Moves the JSPI primitives to runtime::jspi so the thread parker behind
blocking_recv and friends suspends in sync-only builds as well. The park
and scheduler-context state now restore on unwind, and a stale JS park
slot can no longer remove its successor's entry.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.