Reddit

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

새 wasm-bindgen Emscripten 타깃으로 Workers에서 네이티브 Rust 지원

Cloudflare가 wasm-bindgen의 Emscripten 타깃을 실험적으로 지원해 Rust 라이브러리와 Tokio 애플리케이션을 Workers에서 실행하는 방법을 공개했습니다. Tokio의 이벤트 루프 연동과 소켓 브리지를 구현해 Durable Object 안에서 영속 멀티플레이 Minecraft 서버를 구동했습니다.

AI 요약

Cloudflare가 Rust WebAssembly 애플리케이션을 Workers에서 실행하는 새 실험 기능을 공개했습니다. wasm-bindgen의 Rust 타깃에 Emscripten의 wasm32-unknown-emscripten을 지원합니다. 이 조합으로 기존 Rust 라이브러리와 Tokio 기반 애플리케이션을 활용하고, Emscripten의 파일 시스템·타이머·소켓 가상화 기능을 Workers의 Node.js 호환 API와 연결합니다. 공개된 패치와 예제는 아직 실험 단계입니다.

wasm-bindgen과 Emscripten 결합

Emscripten과 wasm-bindgen은 각각 WebAssembly 모듈을 로드하고 JavaScript 코드를 생성하는 도구라, 기존에는 함께 쓰기 어려웠습니다. Google 팀은 Emscripten이 빌드를 주도하고 모듈과 JavaScript를 생성하되, wasm-bindgen은 Emscripten 라이브러리에 포함할 수 있는 형태로 바인딩 코드를 내보내는 방식을 제안했습니다. 양쪽 프로젝트가 서로를 포함하는 통합 테스트를 유지하도록 조정한 결과, 새 -sWASM_BINDGEN 설정으로 두 바인딩 계층을 함께 쓸 수 있게 됐습니다. Emscripten이 빌드하는 C++ 코드에 wasm-bindgen으로 만든 Rust 정적 코드를 연결하거나, Rust 컴파일러로 만든 wasm-bindgen 애플리케이션을 Emscripten 타깃으로 빌드하는 흐름을 지원합니다.

Rust 라이브러리와 Tokio 런타임

초기 테스트에서는 저수준 시스템 라이브러리를 포함해 많은 Rust 라이브러리가 별도 수정 없이 동작했습니다. 일부 라이브러리는 Emscripten 타깃을 플랫폼 조건에 추가해야 했고, libc, socket2, Mio 등이 그 사례입니다. 더 큰 작업은 Tokio 지원이었습니다. Workers는 JavaScript 이벤트 루프 안에서 단일 스레드로 실행되지만, 일반 Tokio 런타임은 소켓이나 타이머를 기다릴 때 스레드를 멈춥니다. 이런 대기는 공유 이벤트 루프를 막으므로 두 실행 모델을 그대로 결합할 수 없습니다.

Cloudflare는 두 가지 접근법을 개발했습니다. 첫째, WebAssembly JavaScript Promise Integration(JSPI)은 동기식으로 보이는 WebAssembly 호출을 멈추고 JavaScript 이벤트 루프에 제어를 돌려줍니다. 다만 스택 전환은 스레드 전환이 아니므로 Rust의 스레드 로컬 Tokio 런타임 문맥이 새 호출과 기존 호출 사이에 공유됩니다. 재진입 시 런타임이 이미 진입한 상태로 인식돼 패닉이 날 수 있습니다. Cloudflare의 실험용 Tokio 패치는 JSPI의 진입·종료·중단·재개 때 스레드 로컬 문맥을 교체하는 방식으로 이 문제를 다룹니다. 설계 확정과 상류 반영은 Tokio 및 Emscripten 팀과 진행 중입니다.

둘째, Tokio에 호스트 이벤트 루프를 위한 LocalEventLoop 런타임을 제안했습니다. 일반 런타임은 준비된 작업을 실행한 뒤 스레드를 대기시키지만, 이 방식은 실행 부분을 drive() 호출로 나누고 대기 대신 호스트가 소유한 Waker로 알립니다. 소켓 읽기를 기다리는 작업은 호스트에 제어를 돌려주고, 소켓이 준비되면 호스트가 런타임을 다시 구동합니다. 따라서 Workers의 이벤트 루프를 막지 않습니다. GTK, Win32, Cocoa 같은 네이티브 이벤트 루프에도 적용할 수 있도록 설계했으며, 이 런타임에서는 대기할 곳이 없으므로 block_on이 대기 상황에서 패닉을 일으킵니다.

Emscripten 소켓과 Workers 연결

Tokio의 네트워크 기능은 Emscripten이 poll()과 WebSocket 에뮬레이션만 지원하고 Tokio의 I/O 드라이버가 의존하는 epoll_wait()을 지원하지 않아 남은 과제였습니다. Cloudflare는 Workers의 기존 Node.js 호환 계층에 node:net API가 있다는 점을 활용했습니다. Emscripten에 40개가 넘는 pull request를 보내 -sNODERAWSOCKETS 옵션을 추가했고, 이를 통해 Emscripten 애플리케이션에서 TCP·UDP·Unix 소켓과 epoll을 사용할 수 있게 했습니다. Workers도 같은 node:net API를 제공하므로 별도 소켓 API를 구현하지 않고 연결됩니다. JSPI 방식에서는 epoll_wait()이 스택을 중단해 준비 상태를 기다립니다. LocalEventLoop 방식에서는 JavaScript 콜백이 Waker를 깨우고, 다음 drive()가 타임아웃 0의 epoll_wait()으로 이벤트를 수집합니다.

Durable Object에서 실행한 Minecraft 서버

Cloudflare 엔지니어 Dan Lapid는 Rust Minecraft 서버 Pumpkin을 Durable Object에 포팅했습니다. Pumpkin은 Tokio를 사용하며 원래는 월드 생성용 스레드 풀과 게임 틱·청크 스케줄러용 OS 스레드를 실행합니다. Durable Object에는 스레드가 하나뿐이므로 틱과 청크 스케줄러를 비동기 작업으로 바꾸고, Rayon 작업도 Tokio 작업으로 옮겼습니다. 월드 생성은 청크 단위로 이벤트 루프에 작업을 돌려주며 네트워크 처리와 게임 틱 사이에 실행합니다.

저장은 Pumpkin의 일반 std::fs 호출을 Emscripten의 -sNODERAWFS가 Workers의 node:fs 계층으로 전달합니다. worker-fs-mount와 durable-object-fs를 결합해 파일을 Durable Object의 SQLite 저장소에 기록합니다. Pumpkin은 파일이 디스크가 아닌 데이터베이스에 저장된다는 사실을 알지 못하며, 객체가 재시작해도 같은 월드에서 이어서 실행합니다. 접속은 Workers TCP ingress로 들어와 handleAsNodeConnection()을 거쳐 Durable Object 안의 net.Server로 전달됩니다. Emscripten의 소켓 계층이 TcpListener를 구현하므로 Pumpkin 코드를 바꾸지 않고 플레이어 연결을 처리합니다. Cloudflare는 이 사례로 영속 저장과 멀티플레이를 갖춘 Rust 서버가 Workers에서 동작하는 과정을 보여줬습니다.

원문: Cloudflare Blog / 번역·요약: Trawling