写过八年 async/await,今天发现 Rust 代码在浏览器里根本跑不起来——wasm-bindgen 把异步这座桥的账全算清楚了

写过八年 async/await,今天发现 Rust 代码在浏览器里根本跑不起来——wasm-bindgen 把异步这座桥的账全算清楚了

在 Node.js 里写惯 async/await 的前端工程师,第一次把 Rust 编译成 WebAssembly 的时候,大概都会遇到同一个问题:Tokio 呢?我的 #[tokio::main] 呢?

浏览器不是服务器。wasm-bindgen 就是这座桥,但它的异步模型和你想的不一样。

问题:Tokio 在浏览器里找不到

写过 Rust 服务端的都知道,#[tokio::main] 负责帮你启动一个多线程异步运行时。Tokio 会创建一个事件循环,调度 Future 在线程池里执行。

但 WebAssembly 没有线程池。或者说,浏览器根本不会给你暴露操作系统级别的线程池 API 给 JavaScript——直到 Web Workers,但那又完全是另一套模型。

你试试这段代码:

“`rust
use wasm_bindgen_futures::spawn_local;
use js_sys::Promise;
use wasm_bindgen::prelude::*;

[wasm_bindgen]

pub async fn run_task() {
let result = some_async_operation().await;
result
}
“`

编译,浏览器加载,然后——你的异步任务不会自动运行,因为没有运行时。

这就是 wasm-bindgen 要解决的核心问题。

wasm-bindgen 怎么接异步

wasm-bindgen 给 JavaScript 和 Rust 之间架了两条异步通道:一条是 Future → Promise,一条是 Promise → Future。

Rust → JS:spawn_local

wasm_bindgen_futures::spawn_local 把一个 Rust Future 丢进浏览器的微任务队列:

“`rust
use wasm_bindgen_futures::spawn_local;

[wasm_bindgen]

pub fn start_worker() {
spawn_local(async {
let data = fetch_data().await;
update_ui(&data);
});
}
“`

spawn_local 没有多线程,任务直接在当前线程跑完。但它正确接入了 JavaScript 的事件循环——await 会让出控制权,JavaScript 可以在等待期间渲染 UI、处理点击事件。

这和 Node.js 里 async 的行为本质相同,只是没有 Tokio 的 work-stealing 调度。

JS → Rust:Future 和 JsFuture

如果你想让 JavaScript 调用一个返回 Future 的 Rust 函数:

“`rust
use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::future_to_promise;

[wasm_bindgen]

pub fn rust_async_function() -> Promise {
future_to_promise(async {
let result = some_async_rust_call().await?;
Ok(result.into())
})
}
“`

JavaScript 那边拿到的是一个标准 Promise,可以 await,可以 .then(),和调用任何异步 JS 函数没有区别。

实战:Rust 异步代码进浏览器的最小示例

“`rust
use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::{spawn_local, future_to_promise};
use js_sys::Promise;

[wasm_bindgen]

pub struct DataProcessor {
buffer: Vec<f32>,
}

[wasm_bindgen]

impl DataProcessor {

[wasm_bindgen(constructor)]

pub fn new() -> Self {
    DataProcessor { buffer: vec![] }
}

# [wasm_bindgen]
pub fn process(&self, input: &[f32]) -> Promise {
    let input_vec = input.to_vec();
    future_to_promise(async move {
        let result = process_data_async(input_vec).await;
        Ok(result.into())
    })
}

# [wasm_bindgen]
pub fn start_background_task(&self) {
    let buffer = self.buffer.clone();
    spawn_local(async move {
        loop {
            wasm_bindgen_futures::sleep(std::time::Duration::from_secs(1)).await;
            if should_stop() {
                break;
            }
        }
    });
}

}

async fn process_data_async(data: Vec<f32>) -> Vec<f32> {
data.iter().map(|x| x * 2.0).collect()
}

fn should_stop() -> bool {
false
}
“`

JavaScript 那边的调用:

“`javascript
import init, { DataProcessor } from ‘./pkg/my_wasm.js’;

await init();

const processor = new DataProcessor();
const result = await processor.process(new Float32Array([1, 2, 3, 4]));
console.log(result); // Float32Array [2, 4, 6, 8]
“`

wasm-bindgen 的异步限制:没有 Tokio

这里要清醒一件事:wasm-bindgen 的异步是浏览器兼容版,不是全功能版。

没有 Tokio: 你不能用 tokio::spawn、tokio::join!、tokio::select!。Tokio 依赖操作系统线程池,浏览器 Wasm 环境不支持。

没有 async traits: async fn 在 trait 里暂时还不是稳定的,impl Future 的模式在 wasm-bindgen 里要额外处理。

单线程调度: spawn_local 只在当前线程执行,不会有 Tokio 那样的多核并行。如果你的任务计算密集,浏览器主线程会被卡住。

wasm-bindgen 自己的运行时: wasm-bindgen-futures 提供了一个基础的 Future 轮询机制,接入浏览器的 queueMicrotask。这个运行时是单线程的,足够处理 IO 密集任务,不适合 CPU 密集型并行。

什么时候该在 Wasm 里用异步

适合的场景:

  • 包装 JavaScript API 调用(fetch、WebSocket、FileReader)
  • 数据处理管道,分段让出控制权给 UI 渲染
  • 和 JavaScript 事件循环配合的后台计算

不适合的场景:

  • CPU 密集型计算(要分片,用 requestAnimationFrame 配合)
  • 需要多线程并行的任务(考虑 Web Workers,但那是另一个 runtime)
  • 复杂的异步流控(select! 那套直接移植会碰壁)

迁移思路

如果你有一个 Rust 服务端异步库想搬到浏览器:

  1. 移除所有 Tokio 依赖,改用 std::future
  2. 把 tokio::spawn 换成 spawn_local
  3. 把 tokio::time::sleep 换成 wasm_bindgen_futures::sleep(基于 setTimeout)
  4. 把 channel 换成 js_sys::MessageChannel 或者直接共享 Arc<Mutex<T>>

这四个替换把大多数 IO 型的异步代码搬进浏览器是够用的。

下一步

wasm-bindgen 是 wasm-pack 的核心,如果你的 Rust+Wasm 项目还没用过 wasm-pack,从这个命令开始:

“`bash
cargo install wasm-pack
wasm-pack build –target web
“`

然后把生成的 JS 包装模块直接 import 进你的前端项目。

异步这座桥没有想象中那么难,只是你得接受浏览器不是服务器这个事实。

评论区

0 条评论

登录后可评论。

铁锈·Rust工具链 14 阅读