Skip to content

Add wasm32-unknown-emscripten target support#1969

Draft
guybedford wants to merge 1 commit into
tokio-rs:masterfrom
guybedford:emscripten
Draft

Add wasm32-unknown-emscripten target support#1969
guybedford wants to merge 1 commit into
tokio-rs:masterfrom
guybedford:emscripten

Conversation

@guybedford

@guybedford guybedford commented Jul 11, 2026

Copy link
Copy Markdown

Draft PR for #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, only requiring one additional Emscripten-specific API explained below.

Once emscripten-core/emscripten#27207 lands, Emscripten exposes a real epoll backed by its runtime event loop, so the existing Linux epoll selector can reused rather than adding a new backend.

The readiness model works under different modes - direct blocking epoll_wait is fully supported under Emscripten's JSPI or pthread proxy modes, but when threading support is not available in browsers/node, an alternative new API may be used instead - emscripten_epoll_set_callback to support a single thread cooperative event loop mode. Instead of calling epoll_wait, a thread can register a persistent callback on the epoll, which is delivered by the JS event loop back to that thread possibly as soon as the next host tick after the stack unwinds. This allows the epoll model to work exactly the same, but without blocking in a hosted single-threaded JS event loop. Events keep re-firing while the set stays ready, and level-triggering and maxevents overflow drain exactly as a blocking epoll_wait loop would. It is just another consumer of the same ready list concept, supporting all of EPOLLET/EPOLLONESHOT/EPOLLEXCLUSIVE/maxevents. A NULL callback can be used to unregister.

For Mio, to support non-JSPI / thread use cases with this new emscripten_epoll_set_callback results in a new optional Emscripten-only API - Poll::new_with_callback(capacity, closure), which maps directly to this callback for the lifetime of the Poll. It boxes the user closure plus a persistent Events buffer; a small extern "C-unwind" trampoline refills that buffer from the delivered ready set on each tick and invokes the closure. The callback holds no borrows across the call, so the closure may freely re-enter mio to register/deregister sources, and it is disarmed (then freed) on drop.

Patch Sets

This PR will remain a draft while it relies on the following upstream patches:

In addition Rust nightly is needed for two additional patches only landed recently.

Test status

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

Tests use JSPI to be able to support the harness integration, but can work without JSPI when integrating with the callback model - a dedicated emscripten test is included for these.

Early feedback on the approach very much welcome.

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-waker cfg lists gain emscripten.

Because emscripten cannot block in `epoll_wait` without JSPI/ASYNCIFY, two
non-blocking readiness paths are added alongside the normal `Poll::poll`:

* `Poll::new_with_callback` arms a persistent callback on the epoll fd via the
  runtime's `emscripten_epoll_set_callback`, delivering ready events on a fresh
  host tick whenever the set makes progress. The callback state is boxed, owned
  by the arming selector, and disarmed on drop; it may freely re-enter mio to
  (de)register sources.
* `Registry::poll_ready` does a zero-timeout drain of the same epoll set as a
  readiness probe where a blocking wait is impossible.

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 suite spawns OS threads (socket-peer test harnesses), so std is rebuilt
with atomics via -Zbuild-std and linked -pthread with -sPROXY_TO_PTHREAD so the
main thread can block; JSPI (-sJSPI) provides the return-to-host suspension for
blocking reads/writes, and NODERAWFS/NODERAWSOCKETS back the filesystem and
sockets with node's. This needs nightly + rust-src and a JSPI-capable Node
(26+, or 22 with --experimental-wasm-jspi). No custom target spec is required:
nightly now 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,
so the examples cannot be linked with the socket/thread runtime.

Temporary, until the dependencies land upstream:

* Cargo.toml patches libc to guybedford/libc#emscripten for the emscripten
  epoll/pthread externs (rust-lang/libc#5270).
* the CI job builds against the guybedford/emscripten `cf` fork, which carries
  the epoll callback (emscripten-core/emscripten#27207), AF_UNIX pathname
  stream sockets, and multicast getsockopt patches this target depends on.

Suite result: 148 passed, 0 failed, 3 ignored under Node - green on CI.
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.

1 participant