Skip to content

Add wasm32-unknown-emscripten target support - #1969

Open
guybedford wants to merge 3 commits into
tokio-rs:masterfrom
guybedford:emscripten
Open

guybedford wants to merge 3 commits into
tokio-rs:masterfrom
guybedford:emscripten

Conversation

@guybedford

@guybedford guybedford commented Jul 11, 2026

Copy link
Copy Markdown

Resolves #642.

Adds wasm32-unknown-emscripten as a target for mio, with a CI job running the suite under Node. Opening as a draft for now since it depends on unreleased patchsets for Emscripten and Rust's libc.

This lays the groundwork for Tokio support for Emscripten. Initially I did not plan to PR Mio, but in discussion with @Darksonn and @Noah-Kennedy it was suggested to use Mio if possible, and this has worked out well in my opinion.

With emscripten-core/emscripten#27207 landed, Emscripten now exposes a real epoll, so the existing Linux epoll selector can reused rather than adding a new backend.

Patch Sets

The only remaining dependency needed to land this is libc@0.2.190, which for now is pinned to its 0.2 branch.

This PR required the following upstream patches, now all merged:

Test status

The tests run under -pthread -sPROXY_TO_PTHREAD in Emscripten.

In order to support the blocking accept, it was necessary to refactor the test suite to support non-blocking sockets via a util::accept / util::read abstraction.

Tests run directly on the Emscripten Node.js build with NODERAWSOCKETS and NODERAWFS to provide a transparent runner without further harness configuration being necessary.

Comment thread src/poll.rs Outdated
@guybedford

guybedford commented Jul 12, 2026 via email

Copy link
Copy Markdown
Author

@Darksonn

Copy link
Copy Markdown
Member

Why do you want the callback mechanism on the Tokio side rather than just storing them in this buffer until the next time Tokio calls poll? I'm not sure we can safely invoke wakers from the callback anyway.

@Darksonn Darksonn left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It looks like you were able to reduce this down to merely adjusting cfgs, which is great, thanks!

Comment thread README.md Outdated

@Thomasdezeeuw Thomasdezeeuw left a comment

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.

I'm not sure what to do about the missing public api (UnixStream::pair and UnixDatagram). Removing them, as this API does, is one option, we can also include them, but always return an unsupported error.

What do you think @Darksonn?

Comment thread src/lib.rs Outdated
Comment thread tests/tcp_stream.rs Outdated
Comment thread tests/udp_socket.rs Outdated
@Darksonn

Copy link
Copy Markdown
Member

My suggestion would be to remove the unsupported APIs. IMO it's no different from the fact that UnixDatagram also does not exist on the windows target.

@guybedford

Copy link
Copy Markdown
Author

My suggestion would be to remove the unsupported APIs. IMO it's no different from the fact that UnixDatagram also does not exist on the windows target.

I've gone ahead and implemented this.

@guybedford
guybedford force-pushed the emscripten branch 2 times, most recently from 876f031 to a62c9e4 Compare August 19, 2026 00:17
@guybedford

Copy link
Copy Markdown
Author

To give a progress update here - Emscripten landed support for epoll yesterday in emscripten-core/emscripten#27207 🥳

This PR is now just awaiting two more PRs expected to land this week (libc and Emscripten as per the description checklist), at which point it'll be ready for final review.

In the mean time, the code remains feature complete, so reviews continue to be very much welcome.

guybedford added a commit to guybedford/tokio that referenced this pull request Sep 6, 2026
Follow-on to tokio-rs#8281 to support real async under Emscripten pthreads and
JSPI.

For JSPI, a `runtime::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.

For pthreads it actually just works naturally, with the test suite change
to use `-sPROXY_TO_PTHREAD`. Emscripten runs filesystem operations on the
main thread, so `tokio::fs` deadlocks under a runtime that parks it.

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 greedy
self-waking task cannot starve host timers: the immediate it schedules
next lands in the following event loop iteration, which begins by running
expired timers. `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.

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: 763 passed, 0 failed, 10 ignored
* pthreads (`-pthread -sPROXY_TO_PTHREAD`): 762 passed, 0 failed, 10 ignored

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 timed wait now panics rather than blocking the
  thread through `std::thread::sleep`. This is the timer behaviour change
  the Wasm docs already anticipate, and freezing the host loop is not a
  useful way to wait on this target.
* 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.
* JSPI can support re-entrancy through stack saving and restoring, but
  that is not implemented here, so this is sound only under non-reentrant
  JSPI currently.

Refs: tokio-rs#8281
guybedford added a commit to guybedford/tokio that referenced this pull request Sep 7, 2026
Follow-on to tokio-rs#8281 to support real async under Emscripten pthreads and
JSPI.

For JSPI, a `runtime::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.

For pthreads it works naturally, with the test suite change to use
`-sPROXY_TO_PTHREAD`. Emscripten runs filesystem operations on the
main thread, so `tokio::fs` deadlocks under a runtime that parks it.

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.
`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. Without JSPI the zero-duration park is a no-op, as on native.

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: 763 passed, 0 failed, 10 ignored
* pthreads (`-pthread -sPROXY_TO_PTHREAD`): 762 passed, 0 failed, 10 ignored

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 timed wait now panics rather than blocking the
  thread through `std::thread::sleep`. This is the timer behaviour change
  the Wasm docs already anticipate, and freezing the host loop is not a
  useful way to wait on this target.
* 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.
* JSPI can support re-entrancy through stack saving and restoring, but
  that is not implemented here, so this is sound only under non-reentrant
  JSPI currently.

Refs: tokio-rs#8281
guybedford added a commit to guybedford/tokio that referenced this pull request Sep 7, 2026
Follow-on to tokio-rs#8281 to support real async under Emscripten pthreads and
JSPI.

For JSPI, a `runtime::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.

For pthreads it works naturally. Emscripten's filesystem is synchronous,
so `tokio::fs` and `io-std` keep using the inline blocking shim there
too, and the pthread lane needs no `-sPROXY_TO_PTHREAD`.

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.
`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. Without JSPI the zero-duration park is a no-op, as on native.

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: 763 passed, 0 failed, 10 ignored
* pthreads (`-pthread`): 762 passed, 0 failed, 10 ignored

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 timed wait now panics rather than blocking the
  thread through `std::thread::sleep`. This is the timer behaviour change
  the Wasm docs already anticipate, and freezing the host loop is not a
  useful way to wait on this target.
* 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.
* JSPI can support re-entrancy through stack saving and restoring, but
  that is not implemented here, so this is sound only under non-reentrant
  JSPI currently.

Refs: tokio-rs#8281
guybedford added a commit to guybedford/tokio that referenced this pull request Sep 7, 2026
…nd pthreads

Follow-on to tokio-rs#8281. That PR enabled the `wasm32-unknown-emscripten`
target, but a timed wait on the single-threaded runtime blocked the
thread through `std::thread::sleep`, and the pthread lane covered a
single test file.

For JSPI, a `runtime::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.

For pthreads nothing in the runtime changes: the native condvar parker
already works over `Atomics.wait`. The pthread CI lane now runs the full
suite. Emscripten's filesystem is synchronous, so `tokio::fs` and
`io-std` use the inline blocking shim in pthread builds too, which is
what lets that lane run without `-sPROXY_TO_PTHREAD`.

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.
`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. Without JSPI the zero-duration park is a no-op, as on native.

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: 763 passed, 0 failed, 10 ignored
* pthreads (`-pthread`): 762 passed, 0 failed, 10 ignored

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 timed wait now panics rather than blocking the
  thread through `std::thread::sleep`. This is the timer behaviour change
  the Wasm docs already anticipate, and freezing the host loop is not a
  useful way to wait on this target.
* 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.
* JSPI can support re-entrancy through stack saving and restoring, but
  that is not implemented here, so this is sound only under non-reentrant
  JSPI currently.

Refs: tokio-rs#8281
guybedford added a commit to guybedford/tokio that referenced this pull request Sep 7, 2026
Follow-on to tokio-rs#8281, supporting JSPI as an alternative approach to
suspension.

A `runtime::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.

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: 763 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.
* JSPI can support re-entrancy through stack saving and restoring, but
  that is not implemented here, so this is sound only under non-reentrant
  JSPI currently.

Refs: tokio-rs#8281
@guybedford
guybedford force-pushed the emscripten branch 3 times, most recently from 9b373cb to 7a0702f Compare September 8, 2026 23:02
guybedford added a commit to guybedford/tokio that referenced this pull request Sep 9, 2026
Follow-on to tokio-rs#8285, enabling `tokio::net` on Emscripten over mio's
epoll selector and Node's raw sockets (`-sNODERAWSOCKETS`).

The I/O driver is the native one. Under JSPI, Emscripten's `epoll_wait`
is a blocking wait that suspends on the host event loop, resuming on
readiness (or the mio waker pipe) or the deadline, so `park` and
`park_timeout` need nothing target-specific. The one addition is for
the scheduler's zero-duration maintenance park: a zero-timeout
`epoll_wait` is a synchronous probe, and the host loop is the only
producer of readiness, so the driver yields one host turn first, as the
zero-duration `ParkThread` park already does. Without JSPI a real wait
panics rather than spinning, matching the existing park semantics.
Under pthreads with `-sPROXY_TO_PTHREAD` the wait blocks on the worker
as on native, with no target-specific code at all.

`ScheduledIo::set_readiness` moves from `fetch_update` to an explicit
`compare_exchange_weak` loop: Rust 1.99 deprecates `fetch_update`, and
the MSRV predates `try_update`.

TcpStream/TcpListener/UdpSocket, stream `AF_UNIX` sockets, `lookup_host`
and `AsyncFd` work as on native. Gated where Node lacks the primitive:
datagram `AF_UNIX` (`UnixDatagram`, `UnixSocket::new_datagram`),
`socketpair(2)` (`UnixStream::pair`), and `SO_PEERCRED` (`peer_cred`
reports unsupported). Name resolution goes through Emscripten's
synchronous `getaddrinfo`, which maps hostnames to synthetic addresses,
so the `localhost` tests are ignored on this target.

CI adds `net` to both emscripten test runs on the released emsdk: JSPI
(815 passed, 0 failed, 19 ignored) and pthreads, which regains
`-sPROXY_TO_PTHREAD` (818 passed, 0 failed, 19 ignored). The JSPI run
uses Rust beta until 1.99 is stable, which is where `OwnedFd::try_clone`
(mio's registry handle) gains Emscripten support. TEMPORARY: `mio` comes
from tokio-rs/mio#1969 until released.
guybedford added a commit to guybedford/tokio that referenced this pull request Sep 9, 2026
Follow-on to tokio-rs#8285, enabling `tokio::net` on Emscripten over mio's
epoll selector and Node's raw sockets (`-sNODERAWSOCKETS`).

The I/O driver is the native one. Under JSPI, Emscripten's `epoll_wait`
is a blocking wait that suspends on the host event loop, resuming on
readiness (or the mio waker pipe) or the deadline, so `park` and
`park_timeout` need nothing target-specific. The one addition is for
the scheduler's zero-duration maintenance park: a zero-timeout
`epoll_wait` is a synchronous probe, and the host loop is the only
producer of readiness, so the driver yields one host turn first, as the
zero-duration `ParkThread` park already does. Without JSPI a real wait
panics rather than spinning, matching the existing park semantics.
Under pthreads with `-sPROXY_TO_PTHREAD` the wait blocks on the worker
as on native, with no target-specific code at all.

TcpStream/TcpListener/UdpSocket, stream `AF_UNIX` sockets, `lookup_host`
and `AsyncFd` work as on native. Gated where Node lacks the primitive:
datagram `AF_UNIX` (`UnixDatagram`, `UnixSocket::new_datagram`),
`socketpair(2)` (`UnixStream::pair`), and `SO_PEERCRED` (`peer_cred`
reports unsupported). Name resolution goes through Emscripten's
synchronous `getaddrinfo`, which maps hostnames to synthetic addresses,
so the `localhost` tests are ignored on this target.

CI adds `net` to both emscripten test runs on the released emsdk: JSPI
(815 passed, 0 failed, 19 ignored) and pthreads, which regains
`-sPROXY_TO_PTHREAD` (818 passed, 0 failed, 19 ignored). The JSPI run
uses Rust beta until 1.99 is stable, which is where `OwnedFd::try_clone`
(mio's registry handle) gains Emscripten support. TEMPORARY: `mio` comes
from tokio-rs/mio#1969 until released.
guybedford added a commit to guybedford/rust-workers-minecraft that referenced this pull request Sep 11, 2026
One perpetual `#[wasm_bindgen(jspi)]` export builds a current-thread
runtime and `block_on`s the whole server lifetime; every park suspends the
Wasm stack on `epoll_wait`, so the hosted runtime adapter is gone. Pumpkin
binds its stock `TcpListener` on 25565 inside the Durable Object's port
table, and the object routes each inbound socket to it with
`handleAsNodeConnection`, replacing the injected-stream entry point,
wasm-streams, and the workers-rs dependency. `stop` cancels the server and
the run promise settling is the checkpoint signal.

Toolchain: Rust beta, emscripten main frontend over an emsdk backend,
wasm-bindgen 0.2.128 CLI via `-sWASM_BINDGEN`, exnref exception handling
throughout, tokio `emscripten-epoll`, mio tokio-rs/mio#1969, libc
`libc-0.2`. rustc needs a larger compile-thread stack for pumpkin-data.

Requires a workerd with per-Durable-Object port tables and `net.Server`
inbound routing (`MINIFLARE_WORKERD_PATH`).
guybedford added a commit to guybedford/rust-workers-minecraft that referenced this pull request Sep 11, 2026
One perpetual `#[wasm_bindgen(jspi)]` export builds a current-thread
runtime and `block_on`s the whole server lifetime; every park suspends the
Wasm stack on `epoll_wait`, so the hosted runtime adapter is gone. Pumpkin
binds its stock `TcpListener` on 25565 inside the Durable Object's port
table, and the object routes each inbound socket to it with
`handleAsNodeConnection`, replacing the injected-stream entry point,
wasm-streams, and the workers-rs dependency. `stop` cancels the server and
the run promise settling is the checkpoint signal.

Toolchain: Rust beta, emscripten main frontend paired with the matching
emscripten-releases backend through emsdk, wasm-bindgen 0.2.128 CLI via
`-sWASM_BINDGEN`, exnref exception handling throughout, tokio
`emscripten-epoll`, mio tokio-rs/mio#1969, libc `libc-0.2`. Setup
provisions all of it under .work/ (or reuses EMSDK/EMSCRIPTEN). rustc needs
a larger compile-thread stack for pumpkin-data. The wasm-bindgen and
workers-rs patches and the CLI lockfile are gone; the Pumpkin patch drops
the injected-stream entry point.

Requires a workerd with per-Durable-Object port tables and `net.Server`
inbound routing (`MINIFLARE_WORKERD_PATH`). CI moves to Linux.
guybedford added a commit to guybedford/rust-workers-minecraft that referenced this pull request Sep 11, 2026
One perpetual `#[wasm_bindgen(jspi)]` export builds a current-thread
runtime and `block_on`s the whole server lifetime; every park suspends the
Wasm stack on `epoll_wait`, so the hosted runtime adapter is gone. Pumpkin
binds its stock `TcpListener` on 25565 inside the Durable Object's port
table, and the object routes each inbound socket to it with
`handleAsNodeConnection`, replacing the injected-stream entry point,
wasm-streams, and the workers-rs dependency. `stop` cancels the server and
the run promise settling is the checkpoint signal.

Toolchain: Rust beta, the emscripten 6.0.9 release frontend with a
backport of emscripten-core/emscripten#27208 over the Homebrew or emsdk
6.0.9 backend, wasm-bindgen 0.2.128 CLI via `-sWASM_BINDGEN`, exnref
exception handling throughout, tokio `emscripten-epoll`, mio
tokio-rs/mio#1969, libc `libc-0.2`. Setup provisions the sources, backend
and CLI under .work/. rustc needs a larger compile-thread stack for
pumpkin-data. The wasm-bindgen and workers-rs patches and the CLI lockfile
are gone; the Pumpkin patch drops the injected-stream entry point.

Requires a workerd with per-Durable-Object port tables and `net.Server`
inbound routing (`MINIFLARE_WORKERD_PATH`). CI moves to Linux.
guybedford added a commit to guybedford/tokio that referenced this pull request Sep 11, 2026
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.

A park leaves the runtime. Under single-threaded JS semantics, parked and
"another activation is running" are the same state, so a sibling
promising activation may call `block_on` on the same thread while one is
suspended. `enter_runtime` records a `Snapshot` of the context it was
entered from (what it and `set_scheduler` write: entered flag, rng,
current handle and depth, scheduler pointer) and its guard restores the
previous one; `jspi::suspended` wraps the suspending call, restoring the
entry snapshot around it and its own after, on unwind too: a JS error out
of the import (`SuspendError` from a non-promising activation) unwinds
the runtime's guards cleanly rather than leaving the thread half-left.
The record is part of the snapshot so nesting through the host composes.
A suspension Tokio does not issue, such as a suspending import called
from task code, keeps the runtime entered, so a sibling `block_on` during
it panics as nested, and nested `block_on` from Rust is unchanged.

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: 791 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. Its sibling-activation tests are driven
from a `--js-library` shim, since rustc internalizes a test binary's
`__em_js__` statics. The `interleaved_suspended_runtimes` test is
ignored: Emscripten shares one shadow stack between promising
activations, so a runtime doing work while a sibling is suspended
overwrites the sibling's frames until the toolchain switches stacks.

* `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.
* `task::local::CURRENT` is not part of the swapped context; a `LocalSet`
  entered in a suspended sibling would be visible to a resumed activation
  once interleaving is possible.

Refs: tokio-rs#8281
guybedford added a commit to guybedford/tokio that referenced this pull request Sep 11, 2026
Follow-on to tokio-rs#8285, enabling `tokio::net` on Emscripten over mio's
epoll selector and Node's raw sockets (`-sNODERAWSOCKETS`).

The I/O driver is the native one. Under JSPI, Emscripten's `epoll_wait`
is a blocking wait that suspends on the host event loop, resuming on
readiness (or the mio waker pipe) or the deadline, so `park` and
`park_timeout` need nothing target-specific beyond `jspi::io_wait`,
which makes the wait a park in the JSPI sense: it leaves the runtime
around the `epoll_wait` as the timer park does, so sibling activations
may drive their own runtime while it is suspended. It also handles the
scheduler's zero-duration maintenance park: a zero-timeout `epoll_wait`
is a synchronous probe, and the host loop is the only producer of
readiness, so the driver yields one host turn first, as the
zero-duration `ParkThread` park already does. Without JSPI a real wait
panics rather than spinning, matching the existing park semantics.
Under pthreads with `-sPROXY_TO_PTHREAD` the wait blocks on the worker
as on native, with no target-specific code at all.

A wait with no deadline is a real `epoll_wait` that a socket could wake,
so it suspends rather than panicking as the reactor-less park does; the
`rt_emscripten_block_on` test of that panic is gated off `net`.

TcpStream/TcpListener/UdpSocket, stream `AF_UNIX` sockets, `lookup_host`
and `AsyncFd` work as on native. Gated where Node lacks the primitive:
datagram `AF_UNIX` (`UnixDatagram`, `UnixSocket::new_datagram`),
`socketpair(2)` (`UnixStream::pair`), and `SO_PEERCRED` (`peer_cred`
reports unsupported). Name resolution goes through Emscripten's
synchronous `getaddrinfo`, which maps hostnames to synthetic addresses,
so the `localhost` tests are ignored on this target.

CI adds `net` to both emscripten test runs on the released emsdk: JSPI
(846 passed, 0 failed, 19 ignored) and pthreads, which regains
`-sPROXY_TO_PTHREAD` (818 passed, 0 failed, 19 ignored). The JSPI lane
links a `--pre-js` that fails a test binary whose `main` never returns:
a park that suspends with no wake source leaves Node's event loop to
drain and the process to exit 0, which cargo would otherwise take as
success. The JSPI run uses Rust beta until 1.99 is stable, which is
where `OwnedFd::try_clone` (mio's registry handle) gains Emscripten
support. TEMPORARY: `mio` comes from tokio-rs/mio#1969 until released,
which in turn takes `libc` from the `libc-0.2` branch for its emscripten
epoll bindings (rust-lang/libc#5427).
guybedford added a commit to guybedford/tokio that referenced this pull request Sep 11, 2026
Follow-on to tokio-rs#8285, enabling `tokio::net` on Emscripten over mio's
epoll selector and Node's raw sockets (`-sNODERAWSOCKETS`).

The I/O driver is the native one. Under JSPI, Emscripten's `epoll_wait`
is a blocking wait that suspends on the host event loop, resuming on
readiness (or the mio waker pipe) or the deadline, so `park` and
`park_timeout` need nothing target-specific beyond `jspi::io_wait`,
which makes the wait a park in the JSPI sense: it leaves the runtime
around the `epoll_wait` as the timer park does, so sibling activations
may drive their own runtime while it is suspended. It also handles the
scheduler's zero-duration maintenance park: a zero-timeout `epoll_wait`
is a synchronous probe, and the host loop is the only producer of
readiness, so the driver yields one host turn first, as the
zero-duration `ParkThread` park already does. Without JSPI a real wait
panics rather than spinning, matching the existing park semantics.
Under pthreads with `-sPROXY_TO_PTHREAD` the wait blocks on the worker
as on native, with no target-specific code at all.

A wait with no deadline is a real `epoll_wait` that a socket could wake,
so it suspends rather than panicking as the reactor-less park does; the
`rt_emscripten_block_on` test of that panic is gated off `net`.

TcpStream/TcpListener/UdpSocket, stream `AF_UNIX` sockets, `lookup_host`
and `AsyncFd` work as on native. Gated where Node lacks the primitive:
datagram `AF_UNIX` (`UnixDatagram`, `UnixSocket::new_datagram`),
`socketpair(2)` (`UnixStream::pair`), and `SO_PEERCRED` (`peer_cred`
reports unsupported). Name resolution goes through Emscripten's
synchronous `getaddrinfo`, which maps hostnames to synthetic addresses,
so the `localhost` tests are ignored on this target.

CI adds `net` to both emscripten test runs on the released emsdk: JSPI
(846 passed, 0 failed, 19 ignored) and pthreads, which regains
`-sPROXY_TO_PTHREAD` (818 passed, 0 failed, 19 ignored). The JSPI lane
links a `--pre-js` that fails a test binary whose `main` never returns:
a park that suspends with no wake source leaves Node's event loop to
drain and the process to exit 0, which cargo would otherwise take as
success. The JSPI run uses Rust beta until 1.99 is stable, which is
where `OwnedFd::try_clone` (mio's registry handle) gains Emscripten
support. TEMPORARY: `mio` comes from tokio-rs/mio#1969 until released,
which in turn takes `libc` from the `libc-0.2` branch for its emscripten
epoll bindings (rust-lang/libc#5427).
guybedford added a commit to guybedford/tokio that referenced this pull request Sep 11, 2026
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.

A park leaves the runtime. Under single-threaded JS semantics, parked and
"another activation is running" are the same state, so a sibling
promising activation may call `block_on` on the same thread while one is
suspended. `enter_runtime` records a `Snapshot` of the context it was
entered from (what it and `set_scheduler` write: entered flag, rng,
current handle and depth, scheduler pointer) and its guard restores the
previous one; `jspi::suspended` wraps the suspending call, restoring the
entry snapshot around it and its own after, on unwind too: a JS error out
of the import (`SuspendError` from a non-promising activation) unwinds
the runtime's guards cleanly rather than leaving the thread half-left.
The record is part of the snapshot so nesting through the host composes.
A suspension Tokio does not issue, such as a suspending import called
from task code, keeps the runtime entered, so a sibling `block_on` during
it panics as nested, and nested `block_on` from Rust is unchanged.

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: 791 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. Its sibling-activation tests are driven
from a `--js-library` shim, since rustc internalizes a test binary's
`__em_js__` statics. The `interleaved_suspended_runtimes` test is
ignored: Emscripten shares one shadow stack between promising
activations, so a runtime doing work while a sibling is suspended
overwrites the sibling's frames until the toolchain switches stacks.

* `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.
* `task::local::CURRENT` is not part of the swapped context; a `LocalSet`
  entered in a suspended sibling would be visible to a resumed activation
  once interleaving is possible.

Refs: tokio-rs#8281
guybedford added a commit to guybedford/rust-workers-minecraft that referenced this pull request Sep 11, 2026
One perpetual `#[wasm_bindgen(jspi)]` export builds a current-thread
runtime and `block_on`s the whole server lifetime; every park suspends the
Wasm stack on `epoll_wait`, so the hosted runtime adapter is gone. Pumpkin
binds its stock `TcpListener` on 25565 inside the Durable Object's port
table, and the object routes each inbound socket to it with
`handleAsNodeConnection`, replacing the injected-stream entry point,
wasm-streams, and the workers-rs dependency. `stop` cancels the server and
the run promise settling is the checkpoint signal. `-sREENTRANT_JSPI`
gives each activation its own shadow stack, so other entries into the
module while the server is suspended cannot clobber its frames.

Toolchain: Rust beta; emscripten main plus the JSPI hooks, reentrant JSPI
and epoll listener PRs, with the paired emscripten-releases LLVM and the
jspi-hooks Binaryen branch built by setup; wasm-bindgen 0.2.128 CLI via
`-sWASM_BINDGEN`; exnref exception handling throughout; tokio
`emscripten-epoll`, mio tokio-rs/mio#1969, libc `libc-0.2`. rustc needs a
larger compile-thread stack for pumpkin-data. The wasm-bindgen and
workers-rs patches and the CLI lockfile are gone; the Pumpkin patch drops
the injected-stream entry point.

Requires a workerd with per-Durable-Object port tables and `net.Server`
inbound routing (`MINIFLARE_WORKERD_PATH`). CI moves to Linux.
@guybedford

Copy link
Copy Markdown
Author

@Darksonn thanks, I've pushed up a change to fix it.

guybedford added a commit to guybedford/tokio that referenced this pull request Sep 14, 2026
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
guybedford added a commit to guybedford/tokio that referenced this pull request Sep 14, 2026
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
guybedford added a commit to guybedford/tokio that referenced this pull request Sep 14, 2026
Follow-on to tokio-rs#8285, enabling `tokio::net` on Emscripten over mio's
epoll selector and Node's raw sockets (`-sNODERAWSOCKETS`).

The I/O driver is the native one. Under JSPI, Emscripten's `epoll_wait`
is a blocking wait that suspends on the host event loop, resuming on
readiness (or the mio waker pipe) or the deadline, so `park` and
`park_timeout` need nothing target-specific beyond `jspi::io_wait`,
which handles the scheduler's zero-duration maintenance park: a
zero-timeout `epoll_wait` is a synchronous probe, and the host loop is
the only producer of readiness, so the driver yields one host turn
first, as the zero-duration `ParkThread` park already does. Without JSPI
a real wait panics rather than spinning, matching the existing park
semantics.
Under pthreads with `-sPROXY_TO_PTHREAD` the wait blocks on the worker
as on native, with no target-specific code at all.

A wait with no deadline is a real `epoll_wait` that a socket could wake,
so it suspends rather than panicking as the reactor-less park does; the
`rt_emscripten_block_on` test of that panic is gated off `net`.

TcpStream/TcpListener/UdpSocket, stream `AF_UNIX` sockets, `lookup_host`
and `AsyncFd` work as on native. Gated where Node lacks the primitive:
datagram `AF_UNIX` (`UnixDatagram`, `UnixSocket::new_datagram`),
`socketpair(2)` (`UnixStream::pair`), and `SO_PEERCRED` (`peer_cred`
reports unsupported). Name resolution goes through Emscripten's
synchronous `getaddrinfo`, which maps hostnames to synthetic addresses,
so the `localhost` tests are ignored on this target.

CI adds `net` to both emscripten test runs on the released emsdk: JSPI
(846 passed, 0 failed, 19 ignored) and pthreads, which regains
`-sPROXY_TO_PTHREAD` (818 passed, 0 failed, 19 ignored). The JSPI lane
links a `--pre-js` that fails a test binary whose `main` never returns:
a park that suspends with no wake source leaves Node's event loop to
drain and the process to exit 0, which cargo would otherwise take as
success. The JSPI run uses Rust beta until 1.99 is stable, which is
where `OwnedFd::try_clone` (mio's registry handle) gains Emscripten
support. TEMPORARY: `mio` comes from tokio-rs/mio#1969 until released,
which in turn takes `libc` from the `libc-0.2` branch for its emscripten
epoll bindings (rust-lang/libc#5427).
@guybedford
guybedford marked this pull request as ready for review September 15, 2026 17:01
Adds `wasm32-unknown-emscripten` as a target for mio, plus a CI job that runs
the suite under Node. Resolves tokio-rs#642.

Emscripten exposes a real epoll backed by its runtime event loop, so the
existing Linux epoll selector is reused rather than adding a new backend. The
wasm `compile_error!` guard is relaxed to let emscripten through, and the
`epoll`/`eventfd`/pipe cfg lists gain emscripten.

AF_UNIX support is stream-only: emscripten's node-backed sockets have no
datagram primitive, so `UnixDatagram` and the `socketpair`-based helpers are
not compiled there. Sockets set `O_NONBLOCK` via `fcntl` since emscripten's
`socket(2)` silently strips `SOCK_NONBLOCK`/`SOCK_CLOEXEC`.

The test peers are std sockets on helper threads, so std is rebuilt with
atomics via -Zbuild-std and linked -pthread with -sPROXY_TO_PTHREAD, and
NODERAWFS/NODERAWSOCKETS back the filesystem and sockets with node's.
Emscripten sockets never block - a call that would block returns EAGAIN - so
the peers' blocking `accept`/`read` go through `util::accept`/`util::read`,
which on emscripten wait for readiness with `poll(2)` (which does park a
pthread) and retry; elsewhere they are the plain std calls. This runs on a
released emsdk (6.0.9) with no emscripten patches. Nightly + rust-src are
needed for -Zbuild-std; no custom target spec is required since nightly emits
the __main_argc_argv entry point (rust-lang/rust#158937). Doctests are skipped
on this target: rustdoc does not apply the emcc link args.

Temporary, until released: Cargo.toml takes libc from the `libc-0.2` branch
for the emscripten epoll bindings (rust-lang/libc#5427).

Suite result: 147 passed, 0 failed, 3 ignored under Node. The one
emscripten-specific ignore is `tcp_stream::raw_fd` (`getsockname` after a
non-blocking connect can transiently report an unbound local address).
@guybedford

Copy link
Copy Markdown
Author

This PR is now ready for review, and no longer draft. All upstream patchsets have now landed.

We are just waiting for the libc@0.2.190 release further.

In the mean time, reviews are very welcome. //cc @Darksonn

Comment thread src/sys/unix/tcp.rs
Comment thread .github/workflows/ci.yml Outdated
Comment thread tests/util/mod.rs
// and the caller is expected to wait for readiness itself. `poll(2)` does
// block on a pthread, which is where these peers run.
#[cfg(target_os = "emscripten")]
pub fn accept<L: Accept + AsRawFd>(listener: &L) -> io::Result<L::Conn> {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Aren't we using JSPI precisely to make blocking syscalls such as this one possible without special code such as this?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

This Mio implementation doesn't use JSPI, as that requires extra ceremony per the Tokio PR. Instead pthreads uses a JS worker to support blocking calls which gets things to mostly just work, and avoiding the need for the blocking JSPI Emscripten PR.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Still, if you're using pthreads, why can't we just use a blocking call here instead of looping on a non-blocking call?

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.

I'm also not a fan of all this special code for testing. I would prefer if we can do without it.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I had an Emscripten PR to do just that open for two months here - emscripten-core/emscripten#27342.

I just closed it to use this approach instead, because there was resistance to supporting a dual path sync/async function upstream. Accept behaves sync without jspi/pthreads, and async under jspi/pthreads, but Emscripten doesn't have a way to switch on those modes, which is what my PR added. Otherwise you need two variants of every blocking function in JS - a sync version and an async version. All difficult sells for Emscripten.

So my test refactor here was done to ensure this PR can land sooner (as soon as the next libc release).

Ideally we could just not use blocking sockets in the test suite here and everything would work out. I haven't really never needed support for blocking accept yet in test applications.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I've posted a new attempt towards this in emscripten-core/emscripten#27724.

Depending on progress, we can decide if we want this PR to block on that or not.

I do think ideally we should land this since it only affects the tests, and do the refactoring as a follow-on, now that it has tracking PR again.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think if emscripten's goal is to get C programs to work verbatim without changes, you'll need these blocking calls to work. I would be ok with doing it temporarily if we can fix it in future emscripten, but it's Thomas's call.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Certianly, we can see how progress goes with that - but I would strong suggest that once libc@0.2.190 is released, we shouldn't hold back the patchset on it if it is still pending.

Comment thread tests/util/mod.rs
// and the caller is expected to wait for readiness itself. `poll(2)` does
// block on a pthread, which is where these peers run.
#[cfg(target_os = "emscripten")]
pub fn accept<L: Accept + AsRawFd>(listener: &L) -> io::Result<L::Conn> {

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.

I'm also not a fan of all this special code for testing. I would prefer if we can do without it.

Comment thread Cargo.toml

[target.'cfg(any(unix, target_os = "hermit", target_os = "wasi"))'.dependencies]
libc = "0.2.183"
libc = { git = "https://github.com/rust-lang/libc", branch = "libc-0.2" }

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.

Blocking: this needs a released version (I assume this is just a matter of time).

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Yes, we are just waiting on 0.2.190.

Comment thread .github/workflows/ci.yml
guybedford added a commit to wasm-bindgen/wasm-bindgen that referenced this pull request Sep 16, 2026
tokio-rs/tokio#8484 replaces EventLoopRuntime with EventLoop /
LocalEventLoop: spawn_local queues only, drive runs one batch, and the
root's outcome is read from its JoinHandle. The ambient and isolated
schedulers now spawn the root plus a sibling task that awaits it and
delivers the result, keeping the synchronous first poll (drive outside a
runtime context) and panic -> JoinError semantics. The isolated event
loop's drop is deferred to a microtask, since a runtime cannot be dropped
from inside its own drive.

The event loop builders are gated on tokio's net feature, so mio is
patched to its emscripten branch (tokio-rs/mio#1969).
guybedford added a commit to wasm-bindgen/wasm-bindgen that referenced this pull request Sep 16, 2026
tokio-rs/tokio#8484 replaces EventLoopRuntime with EventLoop /
LocalEventLoop: spawn_local queues only, drive runs one batch, and the
root's outcome is read from its JoinHandle. The ambient and isolated
schedulers now spawn the root plus a sibling task that awaits it and
delivers the result, keeping the synchronous first poll (drive outside a
runtime context) and panic -> JoinError semantics. The isolated event
loop's drop is deferred to a microtask, since a runtime cannot be dropped
from inside its own drive.

The event loop builders are gated on tokio's net feature, so mio is
patched to its emscripten branch (tokio-rs/mio#1969).
guybedford added a commit to cloudflare/workers-rs that referenced this pull request Sep 16, 2026
tokio-rs/tokio#8484 replaces EventLoopRuntime with EventLoop /
LocalEventLoop; the example takes tokio from its branch and mio from its
emscripten branch (tokio-rs/mio#1969). The wasm-bindgen submodule moves to
the rebuilt emscripten stack: main plus the reinit fix (#5332), legalized
export names and the tokio attribute (#5334) adapted to LocalEventLoop,
without the emscripten JSPI support. worker's schedule_isolated use is
unchanged.
guybedford added a commit to cloudflare/workers-rs that referenced this pull request Sep 16, 2026
tokio-rs/tokio#8484 replaces EventLoopRuntime with EventLoop /
LocalEventLoop; the example takes tokio from its branch and mio from its
emscripten branch (tokio-rs/mio#1969). The wasm-bindgen submodule moves to
the rebuilt emscripten stack: main plus the reinit fix (#5332), legalized
export names and the tokio attribute (#5334) adapted to LocalEventLoop,
without the emscripten JSPI support. worker's schedule_isolated use is
unchanged.
guybedford added a commit to guybedford/tokio that referenced this pull request Sep 18, 2026
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
guybedford added a commit to guybedford/tokio that referenced this pull request Sep 18, 2026
Follow-on to tokio-rs#8285, enabling `tokio::net` on Emscripten over mio's
epoll selector and Node's raw sockets (`-sNODERAWSOCKETS`).

The I/O driver is the native one. Under JSPI, Emscripten's `epoll_wait`
is a blocking wait that suspends on the host event loop, resuming on
readiness (or the mio waker pipe) or the deadline, so `park` and
`park_timeout` need nothing target-specific beyond `jspi::io_wait`,
which handles the scheduler's zero-duration maintenance park: a
zero-timeout `epoll_wait` is a synchronous probe, and the host loop is
the only producer of readiness, so the driver yields one host turn
first, as the zero-duration `ParkThread` park already does. Without JSPI
a real wait panics rather than spinning, matching the existing park
semantics.
Under pthreads with `-sPROXY_TO_PTHREAD` the wait blocks on the worker
as on native, with no target-specific code at all.

A wait with no deadline is a real `epoll_wait` that a socket could wake,
so it suspends rather than panicking as the reactor-less park does; the
`rt_emscripten_block_on` test of that panic is gated off `net`.

TcpStream/TcpListener/UdpSocket, stream `AF_UNIX` sockets, `lookup_host`
and `AsyncFd` work as on native. Gated where Node lacks the primitive:
datagram `AF_UNIX` (`UnixDatagram`, `UnixSocket::new_datagram`),
`socketpair(2)` (`UnixStream::pair`), and `SO_PEERCRED` (`peer_cred`
reports unsupported). Name resolution goes through Emscripten's
synchronous `getaddrinfo`, which maps hostnames to synthetic addresses,
so the `localhost` tests are ignored on this target.

CI adds `net` to both emscripten test runs on the released emsdk: JSPI
(846 passed, 0 failed, 19 ignored) and pthreads, which regains
`-sPROXY_TO_PTHREAD` (818 passed, 0 failed, 19 ignored). The JSPI lane
links a `--pre-js` that fails a test binary whose `main` never returns:
a park that suspends with no wake source leaves Node's event loop to
drain and the process to exit 0, which cargo would otherwise take as
success. The JSPI run uses Rust beta until 1.99 is stable, which is
where `OwnedFd::try_clone` (mio's registry handle) gains Emscripten
support. TEMPORARY: `mio` comes from tokio-rs/mio#1969 until released,
which in turn takes `libc` from the `libc-0.2` branch for its emscripten
epoll bindings (rust-lang/libc#5427).
guybedford added a commit to wasm-bindgen/wasm-bindgen that referenced this pull request Sep 18, 2026
tokio-rs/tokio#8484 replaces EventLoopRuntime with EventLoop /
LocalEventLoop: spawn_local queues only, drive runs one batch, and the
root's outcome is read from its JoinHandle. The ambient and isolated
schedulers now spawn the root plus a sibling task that awaits it and
delivers the result, keeping the synchronous first poll (drive outside a
runtime context) and panic -> JoinError semantics. The isolated event
loop's drop is deferred to a microtask, since a runtime cannot be dropped
from inside its own drive.

The event loop builders are gated on tokio's net feature, so mio is
patched to its emscripten branch (tokio-rs/mio#1969).
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.

Unable to build when targeting wasm32-unknown-emscripten platform

3 participants