Article summary — original

Cloudflare Introduces Experimental Emscripten Target for wasm-bindgen and Rust Workers Cloudflare announced an experimental preview supporting the Emscripten wasm32-unknown-emscripten compiler target in wasm-bindgen and Cloudflare Workers. Developed alongside Google and open-source maintainers, this integration enables native Rust applications, including Tokio-based network services, to run within JavaScript and Workers environments. wasm-bindgen and Emscripten now interoperate cooperatively using the new -sWASM_BINDGEN flag, bridging Rust and C++ dependencies with companion JavaScript. Cloudflare mapped Tokio to single-threaded Workers using two approaches: WebAssembly JSPI context switching and a proposed LocalEventLoop design. Over 40 pull requests to Emscripten established -sNODERAWSOCKETS, enabling epoll and raw socket support atop Node.js compatibility APIs in Workers. A multiplayer Rust Minecraft server (Pumpkin) was demonstrated running inside a Durable Object using TCP ingress and SQLite-backed storage virtualization. Toolchain Interoperability Between wasm-bindgen and Emscripten The integration originated from Google's Portable Toolchains team, which needed wasm-bindgen's JavaScript bindings alongside Emscripten's C++ linking capabilities. Historically, both toolchains attempted to manage module loading and companion JavaScript independently. Collaborating with Cloudflare engineers, the teams coordinated a workflow where Emscripten drives the build and wasm-bindgen supplies companion bindings directly into Emscripten's library system under the -sWASM_BINDGEN configuration. Adapting Tokio for Single-Threaded Event Loops Because Workers execute single-threaded within a JavaScript event loop, standard Tokio blocking threads cannot be used directly. To bridge this, Cloudflare pursued two strategies. The first utilizes WebAssembly JavaScript Promise Integration (JSPI), which suspends stacks on blocking calls; handling reentrant calls required cooperative time-multiplexed context swapping of thread-local state. The second strategy introduces a LocalEventLoop architecture to Tokio, which replaces thread parking with host wakers, allowing the host runtime to interleave work by invoking explicit batch drive operations. Bridging Sockets via Emscripten and Node.js APIs Tokio's networking subsystem relies on epoll_wait via mio, which Emscripten previously lacked. Cloudflare contributed more than 40 pull requests to introduce the -sNODERAWSOCKETS flag in Emscripten. This implementation virtualizes epoll, TCP, UDP, and Unix sockets on top of the node:net compatibility APIs shared by Node.js and Cloudflare Workers, allowing Tokio's native network drivers to operate without bespoke platform APIs. Minecraft Server Demonstration in Durable Objects To demonstrate the target's compatibility, engineer Dan Lapid ported Pumpkin, a multi-threaded Rust Minecraft server, to run inside a single-threaded Durable Object. Thread pools were transformed into cooperative Tokio tasks. World persistence was routed through -sNODERAWFS into worker-fs-mount to store world files in the object's transactional SQLite database, while incoming TCP connections were bridged into the runtime's network listener through Workers TCP ingress and -sNODERAWSOCKETS.

Article summary — zh

Cloudflare 发布 wasm-bindgen 的 Emscripten 目标支持,使原生 Rust 及 Tokio 应用可在 Workers 运行 Cloudflare 宣布为其 Rust Workers 和 wasm-bindgen 工具链实验性支持 wasm32-unknown-emscripten 编译目标。该方案由谷歌与 Cloudflare 协同推进,解决了原生 Rust、C++ 依赖与 JavaScript 交互的冲突,并让基于 Tokio 的原生异步应用无需复杂改动即可在单线程事件循环环境中运行。 wasm-bindgen 与 Emscripten 实现了互操作,由 Emscripten 驱动构建并借助 -sWASM_BINDGEN 配置内嵌 wasm-bindgen 生成的轻量 JS 绑定 Cloudflare 为 Tokio 适配了 WebAssembly JSPI 与 LocalEventLoop 两种在单线程 JS 事件循环中的集成运行模式 通过向 Emscripten 贡献超过 40 个 PR 引入 -sNODERAWSOCKETS 编译选项,打通了基于 node:net 的 epoll 与网络套接字支持 通过一个在单线程 Durable Object 中运行的多人 Minecraft 原生 Rust 服务器(Pumpkin),验证了文件系统虚拟化与 TCP 入口能力 wasm-bindgen 与 Emscripten 的工具链协同 此前谷歌内部尝试将 Rust WebAssembly 工具链与 C++ 结合时,发现 wasm-bindgen 与 Emscripten 均默认由自身全权掌控 JavaScript 生成和 Wasm 输出,导致两者无法共存。谷歌 Portable Toolchains 团队与 Cloudflare 工程师合作制定了协作方案:由 Emscripten 继续主导构建、加载 Wasm 模块并提供配套 JS,同时 wasm-bindgen 产出格式兼容的轻便绑定直接集成到 Emscripten 库体系中。最终双方在 -sWASM_BINDGEN 配置下实现了无缝互操作。 适配单线程事件循环:JSPI 与 LocalEventLoop 针对 Workers 单线程事件循环与 Tokio 原生阻塞/休眠语义不兼容的问题,团队探索了两种方案。其一是利用 WebAssembly JavaScript Promise Integration(JSPI)挂起 Wasm 堆栈并交回控制权,但由于 Rust 内部不感知栈切换,必须在出入和恢复时精细置换线程本地上下文以防运行时状态混乱。其二则是设计通用的 LocalEventLoop,将原本会导致线程挂起的等待操作替换为主机通告唤醒(wake),由外部宿主事件循环批量调用 drive() 推进任务,确保主循环绝不发生同步阻塞。 网络抽象与 -sNODERAWSOCKETS 的引入 由于 Emscripten 原生仅支持 poll 和 WebSocket 仿真,缺少 Tokio I/O 驱动(mio)依赖的 epoll_wait,网络套接字(TCP/UDP/Unix)成为最后的技术短板。Cloudflare 利用 Workers 与 Node.js 兼容层中现有的 node:net 接口,向 Emscripten 提交了 40 多个 PR,开创了 -sNODERAWSOCKETS 编译选项。该机制在 JSPI 下可直接挂起栈,在 LocalEventLoop 模式下则通过新设计的 emscripten_epoll_add_listener 回调触发唤醒并执行零超时的 epoll 收集。 实战验证:在 Durable Object 运行 Minecraft 服务器 作为能力验证,开发者在单线程 Durable Object 内成功部署了多核设计的 Rust 原生 Minecraft 服务器 Pumpkin。原本分配在独立操作系统线程上的世界生成、游戏时钟和 Rayon 任务被改造为交错执行的 Tokio 异步任务;基于 std::fs 的文件写入通过 -sNODRAWFS 映射到 Durable Object 的 SQLite 数据库中;网络层则结合 Workers TCP 入口与 handleAsNodeConnection API 派发给 net.Server,让原生服务端无需重写底层通信即可接待实际玩家连接。

Saved original article

Today we’re announcing the first public experimental preview of a feature to better support native Rust code and even Tokio-based applications just running natively on Workers: first-class support for the Emscripten wasm32-unknown-emscripten Rust compiler target on the [wasm-bindgen](https://github.com/wasm-bindgen/wasm-bindgen) open source toolchain and Cloudflare’s Rust Workers. wasm-bindgen is the open source toolchain powering Rust-based WebAssembly applications on our V8-based Workers Runtime. Enabling the Emscripten target for wasm-bindgen has been a long-term effort, first initiated by Google over a year ago, and then further reviewed and supported by the Cloudflare engineers maintaining wasm-bindgen. While still in pre-release, we’re excited to share the new workflow possibilities this work enables in running native wasm-bindgen Rust applications with Emscripten on the web, [Node.js](http://node.js), and on Cloudflare’s global Workers platform. In testing we’ve been able to see significantly improved library compatibility for Rust Workers. To illustrate the sort of capabilities supported, we were able to get a Rust-native Minecraft server (Pumpkin) running inside of a Durable Object with TCP ingress, using real TCP sockets via Tokio. See the end of this post for a full description of this port. [Emscripten](https://emscripten.org/) is an open-source WebAssembly compiler toolchain initially created by Mozilla and currently maintained by Google engineers, which makes it possible to run and bridge native code with the web platform, including supporting and virtualizing platform features such as timers, file system operations, sockets, and other native functionality. Since Cloudflare Workers supports Web Platform APIs and Node.js compatibility, we are able to support Emscripten on Workers using its Node.js compilation flags, fully virtualizing native platform features such as timers, file system operations, and sockets on top of our existing Node.js APIs. And with our work on Tokio support, Rust code building on top of Tokio’s async runtime ecosystem can now also be fully integrated into JavaScript-based host environments with this Emscripten target. We’ve made these current experimental patchsets available with example applications to try out today, including: Mitch Foley on Google’s Portable Toolchains team first encountered the need for Rust WebAssembly toolchains to interoperate with C++ when another internal team at Google was interested in using wasm-bindgen to interface with their JavaScript. The goal was for wasm-bindgen to drive the build and produce the companion JavaScript, while Emscripten’s linker (wasm-ld) would be able to link in any C++ dependencies needed. Internally, Google uses Emscripten in C++ codebases to generate JavaScript along with the rest of their applications, allowing the C++ and JS to interact with one another. While Emscripten was capable of linking in Rust code as a dependency, the thing it couldn’t do was provide a robust binding system between Rust and JavaScript like wasm-bindgen does. The problem was both tools assume they are in charge of loading and interacting with JavaScript and generating the final JS and Wasm output. Because of this conflict, Google internal users would have to commit to using one or the other toolchain and never both, and adding a second set of tooling would have doubled the support surface for Google’s Portable Toolchains team. Mitch and his colleague Yifan Yang crafted a plan to have them work cooperatively: Emscripten would continue to drive the build, load the Wasm module, and provide the companion JS, while wasm-bindgen would produce a smaller portable version of its JavaScript bindings in a format that could be directly included in Emscripten’s library system. This wasn’t just a technical problem — both Emscripten and the wasm-bindgen maintainers had to support this plan and be willing to maintain integration tests that depended on the other. With feedback from Google’s Portable Toolchains team, Google’s Wasm Tools team, and Cloudflare engineers, this finally resulted in the required changes landing in both projects. As a result of these efforts, the wasm-bindgen and Emscripten toolchains now have seamless interoperability under the new [`-sWASM_BINDGEN` configuration](https://emscripten.org/docs/tools_reference/settings_reference.html#wasm-bindgen): See the [wasm-bindgen Emscripten documentation](https://wasm-bindgen.github.io/wasm-bindgen/reference/emscripten.html) page for more information about using this target. After landing the `wasm32-unknown-emscripten` target support in wasm-bindgen, early prototypes by Cloudflare engineers demonstrated clear success for supporting this target on Cloudflare Workers. Many libraries worked out of the box, even including low-level systems libraries, since Emscripten already supports the `target_family = unix` in Rust. Some low-level systems libraries that were unaware of Emscripten required patching, for example `libc`, `socket2`, and `Mio`. Even for these low-level libraries, patches primarily involved adding the Emscripten target to the existing platform gates, for example to explicitly allow `target_os = “emscripten”,` in place of Wasm platform gates. Overall we posted these target support patches fairly infrequently, and they were mostly trivial when needed, with the Rust library maintainers very receptive to reviewing the support. That said, one of the critical foundational libraries that needed significant support was Tokio. Cloudflare Workers are single-threaded and hosted within a JS event loop, while Tokio async is designed around blocking operations being supported through 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. To get around this, we had two options: WebAssembly JavaScript Promise Integration or to modify Tokio to support event loop runtime integration. To support Tokio building for Emscripten, we have contributed full Tokio support patchsets that are being reviewed upstream, with the first target support patch for `wasm32-unknown-emscripten` already landed upstream in Tokio. With these patchsets, we’ve been able to fully support both approaches in Cloudflare Workers. In our Rust Workers Tokio examples, these patches are required directly pending further integration upstream. WebAssembly JavaScript Promise Integration (JSPI) maps directly onto Tokio’s existing parking semantics. This is because JSPI allows a blocking Wasm call to suspend the WebAssembly stack on a synchronous operation and return control to the JS event loop, exactly as one would expect of a park. But when a new Wasm call is made while an earlier one is suspended, JSPI doesn’t break: it instead supports having a new WebAssembly stack being entered while arbitrary existing stacks are suspended at the same time. The problem here, though, is that Rust itself isn’t aware that its stack is being switched out from under it. Tokio’s runtime context is tracked via a thread local, but a JSPI stack switch is not a thread switch, so the suspended and new stack still share the same thread-local runtime context. As a result, the Tokio runtime context still thinks it is in the parked context when a new Wasm call enters, and then panics because the runtime is already entered. Supporting fully reentrant JSPI therefore requires careful thread local handling to split context between JSPI context switches. To support this, the thread-local context itself must be swapped 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. With our pre-release Tokio patchset, we were able to implement and verify this model. Finalizing the design and upstreaming it is ongoing in collaboration with the Tokio and Emscripten teams. The other runtime approach is full event loop integration. Event loops are of course a common paradigm in native UI applications, so the idea of supporting an event loop runtime for Tokio was certainly not something completely unfamiliar to the maintainers in discussions we had around the Emscripten target support. The question was rather how to design an event loop for Tokio in such a way that it could work across native applications (including Windows and macOS), as well as for WebAssembly embeddings in JavaScript hosts. If we could design a general `LocalEventLoop` runtime for Tokio, we could solve this problem more generally. A Tokio runtime does two things in a loop: (1) it polls tasks until nothing is ready, and then (2) it waits. The wait is what makes it a runtime rather than a library: the thread parks inside the I/O driver until a socket becomes readable, a timer expires, or another thread wakes it. But when the host already has its own event loop, Tokio's loop cannot run inside it without blocking the host's. The way around this is to split Tokio's loop in two with both parts able to integrate with the host: (1) becomes an explicit `drive()` operation that runs one batch of ready tasks and returns, and (2) is replaced by a wake, so that instead of parking, the runtime tells the host it has work and the host calls `drive()` when it is ready to. Our proposed LocalEventLoop design for Tokio is a LocalRuntime whose wait has been replaced by a wake. It is built with a standard `std::task::Waker` that the host owns, which instead of being used to signal that a future should be polled soon, is used by the Tokio runtime to signal that the runtime itself should be driven soon. Consider the example of a socket read under a regular Tokio runtime: This would correspond to the following call diagram: Instead, with `LocalEventLoop` we can write: Here, `spawn_local` returns immediately, with the host event loop taking responsibility for driving the spawned task to completion via `el.drive()` calls from the host. When stream data is unavailable, the runtime simply returns, handing back control to the host. Once the socket becomes readable, Tokio uses the `host_waker` to signal that the runtime needs driving. This `LocalEventLoop` flow corresponds to the following call diagram: Everything that would have unparked a native runtime's thread wakes the host instead: a spawn, a task woken from another thread, a socket becoming readable, a timer expiring. Because a `Waker` is `Send + Sync` and carries no execution semantics, its implementation is just a notification to the host's event loop. This makes the contract 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 drive that follows runs on the owning thread with nothing else on the stack. The one thing you cannot do is wait. `block_on` still exists and runs a future as far as ready work carries it, but where a normal runtime would park, `LocalEventLoop::block_on` panics instead. This is because nothing could wake that future from inside the call since its wait belongs to the host. With this design, the host event loop is never blocked, and interleaves its own work with Tokio's, one batch at a time. The same architecture embeds into a GTK main loop, a Win32 message pump, or a Cocoa run loop. In addition, any number of these runtimes can co-exist due to their cooperative execution semantics. With both Tokio runtime integration approaches fleshed out for the Rust Emscripten target, we were then able to support most of the Tokio test suite, with one major subsystem still unsupported — the `net` feature. This includes its sockets APIs: TCP, UDP, and Unix sockets. The reason for this was that Emscripten only supported `poll()` and a WebSocket emulation layer, but not `epoll_wait()`, on which Tokio’s I/O driver is built (via mio). For Cloudflare Workers, we wanted to be able to fully integrate with our TCP sockets APIs, including upcoming inbound TCP. To do this, we would need to build a bridge between Emscripten’s virtualization layer and our own sockets API layer. Instead of having to build this bridge ourselves, we realized we already have one: the `node:net` API we support in our Node.js compatibility layer. Emscripten had an `-sNODERAWFS` mode to bridge natively into Node.js FS APIs, so the same approach could give us a sockets bridge without Emscripten or Cloudflare Workers needing to implement any custom APIs on either side. We contributed this work in over 40 pull requests to Emscripten, which is now the [-`sNODERAWSOCKETS` layer](https://emscripten.org/docs/tools_reference/settings_reference.html#noderawsockets) compilation option, enabling support for epoll, TCP, UDP, and Unix sockets for Emscripten applications in Node.js — and, because Workers implements the same `node:net` API, on Workers itself as well. Under JSPI, Emscripten's `epoll_wait()` simply suspends the stack until readiness, so Tokio's I/O driver works as on native. For `LocalEventLoop`, readiness instead had to reach the Waker from a JS callback. To support this, we drafted an Emscripten proposal for a new `emscripten_epoll_add_listener` API to associate a callback on an epoll’s ready events. The next `drive()` then collects these events with a zero-timeout `epoll_wait()`, so Tokio's existing I/O driver can be used unchanged. To test out this new target, the challenge was raised to see if a Minecraft server could be deployed to Workers. Dan Lapid then implemented a [Pumpkin Minecraft server running on a Durable Object](https://github.com/danlapid/rust-workers-minecraft) over a single weekend. [Pumpkin](https://pumpkinmc.org/) is a Rust Minecraft server built on Tokio and designed for multi-core machines. World generation runs on a dedicated thread pool, while the game tick loop and chunk scheduler each run on their own OS threads. Inside a Durable Object there is exactly one thread, so getting Pumpkin to run there meant turning those threads into cooperative tasks on the event loop. Leaning on the Tokio integration, the tick loop and chunk scheduler became async tasks and each Rayon job became a Tokio task. World generation then runs on the event loop itself, taking one turn per chunk and interleaving with network I/O and game ticks rather than blocking them. For persistence, Pumpkin writes its world through ordinary `std::fs`. Under Emscripten’s `-sNODERAWFS` option file system calls get forwarded to the `node:fs` Node.js compatibility layer for Workers. Since this bridge is just JavaScript, it is easy to swap out. Dan’s [worker-fs-mount](https://github.com/danlapid/worker-fs-mount) library enables mounting a `node:fs`-compatible file system with its `durable-object-fs` backend, which stores files as rows in the Durable Object's SQLite storage. With this integrated, every file Pumpkin saves becomes a row in the object's database, written synchronously and committed with the Durable Object's transaction. Pumpkin itself has no idea it isn't on disk, and a restarted object boots straight from the same world. Networking needed no changes in Pumpkin either. Each player's connection arrives through Workers TCP ingress at the Worker's `connect()` handler, which forwards it into the Durable Object. There, `handleAsNodeConnection()` from `cloudflare:node` dispatches the socket to a `net.Server` listening on that port inside the object - a new TCP counterpart of `handleAsNodeRequest()`. Emscripten’s `-sNODERAWSOCKETS` backend 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 has left, so it can save and shut down. Running a fully persistent Minecraft server inside a Durable Object with multiplayer support demonstrates the level of native compatibility that is possible with this new Emscripten target. We’re excited to see what native Rust applications you can bring to the platform. We’re making these full patchsets and workflows [available today](https://github.com/cloudflare/workers-rs/tree/main/examples/emscripten) for experimental use. Improving wasm-bindgen’s support for modern WebAssembly standards for all users is part of our ongoing commitment to the Rust and Wasm ecosystem. We welcome all contributions and feedback — find us on GitHub and in the #rust-on-workers channel on Cloudflare’s [Discord](https://discord.gg/cloudflaredev).