Skip to content

CLOUDFLARE Tier 1

Read original ↗

Supporting native Rust in Workers with the new Emscripten target for wasm-bindgen

First public experimental preview of first-class support for the Emscripten wasm32-unknown-emscripten Rust compiler target on the wasm-bindgen toolchain — the goal being to run native Rust code, including Tokio-based async applications, directly on Cloudflare Workers rather than only the wasm32-unknown-unknown target that today's workers-rs uses. The effort was first initiated by Google over a year ago and then reviewed/supported by the Cloudflare engineers who co-maintain wasm-bindgen.

Summary

Two toolchains that each "assume they are in charge of loading and interacting with JavaScript" — Emscripten (native-code → Wasm, initially by Mozilla, now maintained by Google) and wasm-bindgen (Rust↔JS bindings) — were made to cooperate via a new -sWASM_BINDGEN setting: Emscripten drives the build, loads the Wasm module, and provides the companion JS, while wasm-bindgen emits a smaller portable version of its bindings that slots into Emscripten's library system. Because Workers supports Web Platform APIs + Node.js compatibility, Emscripten's Node.js compilation flags let it fully virtualize native platform features (timers, filesystem, sockets) on top of Workers' existing Node.js APIs. The hard part was Tokio: Workers are single-threaded, hosted inside a JS event loop, but Tokio's async is built around blocking-via-thread-parking — a blocking epoll_wait/socket read cannot block the shared JS event loop. Cloudflare implemented both reconciliation approaches (JSPI and a new LocalEventLoop Tokio runtime), contributed the socket/epoll bridge to Emscripten as -sNODERAWSOCKETS, and demonstrated the whole stack by running a Rust-native Minecraft server (Pumpkin) inside a Durable Object with real TCP sockets and TCP ingress.

Key takeaways

  1. New Rust target wasm32-unknown-emscripten in wasm-bindgen. Two directions now interoperate under -sWASM_BINDGEN: (a) C++ Emscripten builds can link static wasm-bindgen Rust code (both binding layers coexist); (b) Rust apps using wasm-bindgen can build for the Emscripten target (both binding layers coexist). Required changes had to land in both projects and both teams had to agree to maintain integration tests depending on the other. (Source: wasm-bindgen Emscripten docs)

  2. Emscripten already supports target_family = unix in Rust, so many libraries — even low-level systems ones — worked out of the box. The few that needed patches (libc, socket2, Mio) mostly just needed target_os = "emscripten" added to existing platform gates in place of Wasm gates. Patches were infrequent, mostly trivial, and Rust maintainers were receptive. (Source: patterns/upstream-the-fix)

  3. Tokio is the hard dependency. Workers are single-threaded inside a JS event loop; Tokio async is designed around blocking supported via threaded parking semantics. "The two models are clearly not compatible with each other — a blocking operation such as a pending socket read or epoll wait cannot block the shared JS event loop." Two options were pursued: WebAssembly JavaScript Promise Integration (JSPI) or modifying Tokio to support event-loop runtime integration. Cloudflare contributed full Tokio support patchsets upstream; the first wasm32-unknown-emscripten target-support patch already landed upstream in Tokio.

  4. JSPI maps onto Tokio's parking semantics — a blocking Wasm call suspends the Wasm stack on a synchronous op and returns control to the JS event loop, exactly like a park. The subtlety: JSPI can enter a new Wasm stack while an earlier one is suspended, but Rust doesn't know its stack was switched. Tokio's runtime context is a thread-local, and a JSPI stack switch is not a thread switch, so suspended + new stacks share the same thread-local runtime context → the runtime thinks it's still entered and panics. Fully reentrant JSPI therefore requires swapping the thread-local context on each JSPI enter, exit, suspend, and resume — "in effect this is cooperative time-multiplexed threading, with each suspended stack carrying its own runtime context." Implemented + verified in the pre-release patchset; upstreaming ongoing.

  5. LocalEventLoop: split Tokio's loop so a host event loop drives it. A Tokio runtime does two things in a loop: (1) poll tasks until nothing is ready, then (2) wait — the thread parks in the I/O driver until a socket is readable / a timer fires / another thread wakes it. When the host already owns an event loop, Tokio's loop can't run inside it without blocking the host's. The fix: split the loop — (1) becomes an explicit drive() that runs one batch of ready tasks and returns; (2) is replaced by a wake — instead of parking, the runtime tells the host it has work and the host calls drive() when ready. Built with a standard std::task::Waker the host owns: instead of signaling "poll a future soon," it signals "the runtime itself should be driven soon." Everything that would unpark a native runtime's thread (a spawn, a cross-thread wake, a socket becoming readable, a timer expiring) wakes the host instead. Because a Waker is Send + Sync and carries no execution semantics, "the contract is safe by construction: a wake arriving from another thread, from a host callback, or even during a drive, queues work rather than re-entering the runtime." The one thing you cannot do is wait: block_on still runs a future as far as ready work carries it, but where a normal runtime would park, LocalEventLoop::block_on panics (nothing inside the call could wake the future — its wait belongs to the host). Same architecture embeds into a GTK main loop, a Win32 message pump, or a Cocoa run loop, and any number of these runtimes can co-exist due to cooperative execution semantics. API: Builder::build_local_event_loop(..., host_waker) + el.spawn_local(...) (returns immediately) + el.drive().

  6. Sockets/epoll bridge via the Node.js compat layer, contributed as -sNODERAWSOCKETS. With both runtime approaches working, most of Tokio's test custom bridge, Cloudflare reused the one it already has: the node:net API in its Node.js compatibility layer. Emscripten already had -sNODERAWFS to bridge into Node.js FS APIs, so the same approach gave a sockets bridge with no custom APIs on either side. Cloudflare contributed this in over 40 pull requests to Emscripten, now the -sNODERAWSOCKETS layer — epoll + TCP + UDP + Unix sockets for Emscripten apps in Node.js, and, because Workers implements the same node:net API, on Workers too. Under JSPI, epoll_wait() simply suspends the stack until readiness; for LocalEventLoop, readiness reaches the Waker from a JS callback via a drafted emscripten_epoll_add_listener API, and the next drive() collects events with a zero-timeout epoll_wait() so Tokio's existing I/O driver runs unchanged.

  7. Proof of concept: a Rust-native Minecraft server (Pumpkin) inside a Durable Object. Dan Lapid ported Pumpkin — a Tokio-based, multi-core-designed Rust Minecraft server — to run inside a Durable Object over a single weekend. A DO has exactly one thread, so Pumpkin's dedicated threads (world-gen thread pool, game-tick loop, chunk scheduler) became cooperative tasks on the event loop: the tick loop + chunk scheduler became async tasks, each Rayon job became a Tokio task, and world generation runs on the event loop itself (one turn per chunk, interleaved with network I/O + game ticks). Persistence: Pumpkin writes its world through ordinary std::fs; under Emscripten's -sNODERAWFS, FS calls forward to the node:fs Workers compat layer, and Dan's worker-fs-mount library mounts a node:fs-compatible FS whose durable-object-fs backend stores files as rows in the DO's SQLite storage, written synchronously and committed with the DO's transaction — "Pumpkin itself has no idea it isn't on disk, and a restarted object boots straight from the same world." Networking: each player's connection arrives through Workers TCP ingress at the Worker's connect() handler, forwarded into the DO, where handleAsNodeConnection() from cloudflare:node dispatches the socket to a net.Server listening on that port inside the object (a new TCP counterpart of handleAsNodeRequest()); -sNODERAWSOCKETS implements TcpListener on top of net.Server, so the server accepts players exactly as it would on Linux, and the returned promise tells the object when the last player left so it can save and shut down.

  8. Availability: experimental patchsets + example applications for web, Node.js, and Workers are available today; the Tokio patchsets are required directly in the Rust Workers Tokio examples pending further upstream integration. Feedback solicited via GitHub + #rust-on-workers on Cloudflare's Discord.

Systems / concepts / patterns extracted

  • Systems: Emscripten (target + -sWASM_BINDGEN / -sNODERAWSOCKETS / -sNODERAWFS / Node.js flags), wasm-bindgen (wasm32-unknown-emscripten target), Tokio (JSPI reentrancy + LocalEventLoop), WebAssembly (JSPI), Cloudflare Workers (native-Rust host + connect() TCP ingress + cloudflare:node), Durable Objects (single-thread host for Pumpkin; SQLite-backed FS), Node.js (node:net / node:fs compat as the bridge substrate), workers-rs (Emscripten examples), Mio (Tokio's epoll-based I/O driver — recorded as prose). Pumpkin, Rayon, worker-fs-mount, durable-object-fs, GTK / Win32 / Cocoa run loops recorded as prose.
  • Concepts: backpressure-adjacent runtime wake/park semantics. Recorded as prose + tags (single-source, not minted as pages per the taxonomy gate): event-loop-runtime-integration, host-driven-runtime-drive, cooperative-time-multiplexed-threading, thread-local-context-swap-on-stack-switch, wake-instead-of-park, node-compat-layer-as-syscall-bridge, single-threaded-host.
  • Patterns: patterns/upstream-contribution-parallel-to-in-house-integration (Tokio/libc/socket2/Mio target patches + -sNODERAWSOCKETS upstream while the Rust Workers examples ship on the PR branches), patterns/upstream-the-fix (40+ Emscripten PRs + trivial target-gate patches to shared Rust crates).

Operational numbers / caveats

  • 40+ pull requests contributed to Emscripten for the -sNODERAWSOCKETS layer.
  • First wasm32-unknown-emscripten target-support patch already landed upstream in Tokio; full Tokio patchsets still under upstream review.
  • Pumpkin DO port done over a single weekend (Dan Lapid).
  • Experimental / pre-release throughout: JSPI reentrancy design + LocalEventLoop design are being finalized/upstreamed in collaboration with the Tokio + Emscripten teams; the Rust Workers Tokio examples require the patchsets directly.
  • Not a benchmark post — no throughput/latency numbers; the claim is "significantly improved library compatibility for Rust Workers" demonstrated qualitatively.

Source

Last updated · 766 distilled / 2,225 read