Cloudflare добавила нативный Rust в Workers: таргет Emscripten для wasm-bindgen

28.09.2026

Cloudflare открыла первый публичный экспериментальный предпросмотр поддержки таргета wasm32-unknown-emscripten в wasm-bindgen. Это снимает ограничение на крейты, которым был нужен C, и позволяет запускать на Workers нативные приложения на Tokio — вплоть до Minecraft-сервера внутри Durable Object.

Что именно сделано

wasm-bindgen — открытый тулчейн, на котором держится весь Rust-код, исполняемый как WebAssembly, на Workers. Инициатором поддержки Emscripten выступила команда Google больше года назад: wasm-bindgen должен был собрать Rust-модуль и сгенерировать сопутствующий JavaScript, а линкер Emscripten (wasm-ld) — связать зависимости на C++. Проблема была в том, что оба тулчейна претендовали на роль «того, кто загружает JS и владеет итоговым выводом». После согласования они разошлись ролями, и теперь умеют работать вместе через настройку -sWASM_BINDGEN: Emscripten ведёт сборку и линковку, а wasm-bindgen отдаёт переносимые биндинги.

Работу вела в основном Cloudflare. Она потребовала патчей в низкоуровневых крейтах — libc, socket2, mio, — которые просто не знали про Emscripten. Правки в основном сводились к тому, чтобы добавить target_os = "emscripten" к существующим проверкам платформы.

Главная сложность: Tokio против однопоточного цикла событий

Worker однопоточный и живёт внутри JavaScript-цикла событий, а Tokio построен вокруг блокирующих операций, которые паркуют поток. Прочитать из сокета или вызвать epoll_wait, заблокировав общий JS-цикл, нельзя. Cloudflare пошла по двум путям.

Вариант 1: JSPI

JSPI (WebAssembly JavaScript Promise Integration) ложится на парковку Tokio почти один в один: блокирующий вызов Wasm может приостановить стек и вернуть управление циклу событий. Но Rust об этом не знает. Контекст рантайма Tokio хранится в thread-local, а переключение стека JSPI — не переключение потока, поэтому приостановленный и новый стеки делят один и тот же thread-local. Tokio видит, что рантайм уже «введён», и паникует. Чтобы поддержать реентерабельный JSPI, контекст приходится переключать на каждом входе, выходе, приостановке и возобновлении — фактически получается кооперативная многоточечность, где у каждого приостановленного стека свой контекст.

Вариант 2: LocalEventLoop

Более общий подход. Цикл рантайма Tokio делает две вещи: опрашивает готовые задачи, а потом ждёт. Если у хоста уже есть свой цикл событий, ждать внутри нельзя. Предложение LocalEventLoop делит цикл надвое: опрос готовых задач превращается в явную операцию drive(), которая выполняет одну пачку и возвращает управление, а ожидание заменяется пробуждением — рантайм сообщает хосту «есть работа», и хост вызывает drive(), когда готов.

Сигнал строится на обычном std::task::Waker, которым владеет хост, но трактуется не как «пора опросить эту future», а как «пора опросить сам рантайм». Так как Waker это Send + Sync без семантики исполнения, реализация сводится к уведомлению чужого цикла — контракт безопасен по построению: пробуждение из другого потока лишь ставит работу в очередь, а не входит в рантайм повторно.

Единственное, что остаётся невозможным, — ждать. block_on никуда не делся и выполняет future до упора по готовой работе, но там, где обычный рантайм запарковал бы поток, LocalEventLoop::block_on паникует: разбудить future изнутри вызова нечем, ведь ожидание принадлежит хосту.

Схема встраивается не только в Workers. Тот же цикл работает в главном цикле GTK, в насосе сообщений Win32 и в run loop Cocoa, а благодаря кооперативной семантике несколько таких рантаймов могут сосуществовать.

Сокеты и epoll

Сокеты упирались в Emscripten: там был poll() и эмуляция через WebSocket, но не было epoll_wait(), на котором построен I/O-драйвер Tokio через mio. Готовый мост уже существовал — node:net в слое совместимости с Node.js, который Workers тоже реализует. Cloudflare внесла в Emscripten более 40 пул-реквестов и получила слой -sNODERAWSOCKETS, по образцу уже существовавшего -sNODERAWFS. Под JSPI epoll_wait() просто приостанавливает стек, и драйвер работает как на нативной платформе; для LocalEventLoop готовность должна доходить до Waker из JS-колбэка, поэтому инженеры набросали предложение API emscripten_epoll_add_listener, которое пока не принято.

Minecraft-сервер в Durable Object

Проверкой таргета послужил Minecraft-сервер на Rust Pumpkin, который Дэн Лапид портировал за выходные. Он написан на Tokio и рассчитан на многоядерные машины: генерация мира идёт в отдельном пуле потоков, игровой тик и планировщик чанков — каждый в своём потоке. Внутри Durable Object ровно один поток, поэтому все эти потоки пришлось превратить в кооперативные задачи, а каждую задачу Rayon — в задачу Tokio, причём генерация мира теперь занимает по одному чанку за заход, чередуясь с сетевым вводом-выводом и тиками.

С сохранением вышло проще всего: Pumpkin пишет мир через обычный std::fs, а под опцией -sNODERAWFS вызовы файловой системы уходят в слой node:fs. Поскольку мост написан на JavaScript, его легко подменить — библиотека worker-fs-mount монтирует совместимую с node:fs файловую систему поверх хранилища Durable Object, где каждый файл становится строкой в его SQLite. Pumpkin не догадывается, что пишет не на диск, а после перезапуска объекта мир восстанавливается как обычно.

Сеть тоже не потребовала изменений в самом Pumpkin: соединения игроков приходят через TCP-вход Worker и перенаправляются в Durable Object, где handleAsNodeConnection() раздаёт сокеты слушателю net.Server.

Статус

Патчи и примеры доступны уже сейчас и имеют статус экспериментальных. Таргет пока пре-релизный, а поддержка net в Tokio на Emscripten не завершена: epoll, TCP, UDP и Unix-сокеты не работают.

Оригинал

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

Упоминаний: 1 · Просмотров: 4