WASM 与 JS 互操作中的异步模型Promise 和 Future 的桥梁怎么搭一、当 Rust 的 async 遇上 JS 的 Promise用 Rust 写 WASM 模块最让我感到别扭的地方就是异步模型的不匹配。Rust 用的是 async/await Future 这套体系JS 用的是 Promise async/await。表面上await这个关键字两边都有但底层的调度机制完全不同。Rust 的 Future 是惰性的需要被 poll 才会执行JS 的 Promise 是热启动的创建后立即开始执行。这种差异带来了一个实际问题Rust 编译成的 WASM 不能直接await一个 JS 的 Promise。如果你在 Rust 的 async 函数里想调用一个 JS 的异步 API比如fetch必须通过某种机制把 Promise 转成 Rust 能理解的 Future或者反过来。wasm-bindgen和wasm-bindgen-futures就是做这个转换的桥梁。这篇文章聊聊它们的工作原理和具体用法。二、Promise 和 Future 之间的转换过程要理解这个桥梁是怎么工作的先看看从 Rust 调 JS Promise 时的完整时序sequenceDiagram participant Rust as Rust async fn participant Bridge as wasm-bindgen-futures participant JS as JavaScript Promise participant EventLoop as JS 事件循环 Rust-Bridge: .await 一个 JsFuture Bridge-JS: 调用 JS 函数得到 Promise JS-EventLoop: Promise 进入 pending 状态 Bridge-Bridge: 注册 .then() 回调 Bridge-Rust: Future 返回 Poll::Pending Note over Rust: Rust 挂起控制权交给 JS EventLoop-EventLoop: 继续处理其他事件 EventLoop--JS: Promise resolve JS-Bridge: 触发 .then() 回调 Bridge-Rust: wake() 通知 Future 就绪 Rust-Bridge: 再次 poll Bridge-Rust: Poll::Ready(结果) Note over Rust: Rust 恢复执行这个流程的关键是当 Rust 的 Future 还没就绪时返回Poll::Pending控制权会还给 JS 的事件循环。等到 JS 那边 Promise resolve 了再通过回调通知 Rust 侧的 Future 可以继续执行了。三、代码实战3.1 从 Rust 调用 JS 的异步 API最常见的场景是在 Rust 的 WASM 模块里调用浏览器 API比如fetchuse wasm_bindgen::prelude::*; use wasm_bindgen_futures::JsFuture; use web_sys::{window, Request, RequestInit, Response}; /// 在 Rust 的 async 函数中调用 JS 的 fetch API pub async fn fetch_json(url: str) - ResultJsValue, JsValue { // 1. 创建 RequestInit 配置对象 let mut opts RequestInit::new(); opts.method(GET); opts.mode(web_sys::RequestMode::Cors); // 2. 创建 Request 对象 let request Request::new_with_str_and_init(url, opts)?; // 3. 调用 window.fetch() —— 这会返回一个 JS Promise let window window().ok_or(无法获取 window 对象)?; let promise window.fetch_with_request(request); // 4. 将 JS Promise 转换为 Rust Future // JsFuture::from() 是关键它把 Promise 包装成一个实现了 Future trait 的类型 let response JsFuture::from(promise).await?; // 5. 解析 Response 对象 let response: Response response.dyn_into()?; let json_promise response.json()?; // response.json() 也返回 Promise let json JsFuture::from(json_promise).await?; Ok(json) } // 在 WASM 入口中使用 #[wasm_bindgen] pub async fn load_user_data() - ResultJsValue, JsValue { // 直接在 Rust 中 await不需要手动处理 Promise let data fetch_json(https://api.example.com/user/42).await?; // 解析 JSON 数据 let name js_sys::Reflect::get(data, JsValue::from_str(name))?; console::log_1(format!(用户名: {}, name.as_string().unwrap_or_default()).into()); Ok(data) }JsFuture::from(promise)是核心。它在内部做了几件事把 JS Promise 注册到浏览器的微任务队列给 Promise 挂上.then()和.catch()回调返回一个实现了Futuretrait 的 Rust 对象当 Promise 完成时回调会触发 Rust Future 的wake()让它可以被重新 poll3.2 从 JS 调用 Rust 的异步函数反过来JS 也可以调用 Rust 的异步函数use wasm_bindgen::prelude::*; use wasm_bindgen_futures::spawn_local; /// 一个耗时的 Rust 异步计算函数 #[wasm_bindgen] pub async fn heavy_computation(input: u32) - Resultu32, JsValue { // 模拟耗时操作 let mut result input; for i in 0..1000 { result result.wrapping_mul(i 1).wrapping_add(i); // 主动 yield让浏览器有时间处理 UI 事件 if i % 100 0 { // 使用微任务 yield避免长时间阻塞主线程 wasm_bindgen_futures::JsFuture::from( js_sys::Promise::resolve(JsValue::NULL) ).await?; } } Ok(result) } /// 在 Rust 侧启动异步任务不阻塞 JS 的主线程 #[wasm_bindgen] pub fn start_background_task() { // spawn_local 在浏览器的主线程上启动异步任务 // 任务会自动被浏览器的微任务队列调度 spawn_local(async { match heavy_computation(42).await { Ok(result) { console::log_1(format!(计算结果: {}, result).into()); } Err(e) { console::error_1(format!(计算失败: {:?}, e).into()); } } }); }JS 侧直接调用import init, { heavy_computation, start_background_task } from ./pkg/my_wasm.js; async function main() { await init(); // 方式1: 直接 await Rust 的异步函数 // #[wasm_bindgen] 标记的 async fn 会被自动包装为返回 Promise const result await heavy_computation(42); console.log(直接调用结果:, result); // 方式2: 不 await让 Rust 在后台运行 start_background_task(); // 这行会立即执行不会等 background task 完成 console.log(后台任务已启动主线程继续执行); } main();3.3 同时 await 多个 PromiseRust 的并发能力在 WASM 中也适用use wasm_bindgen_futures::JsFuture; use web_sys::window; /// 并发调用多个 API等所有结果都返回后再继续 pub async fn fetch_multiple() - ResultVecJsValue, JsValue { let window window().ok_or(无法获取 window)?; // 创建三个并发的 fetch 请求 let p1 window.fetch_with_str(https://api.example.com/users); let p2 window.fetch_with_str(https://api.example.com/posts); let p3 window.fetch_with_str(https://api.example.com/comments); // 使用 tokio::join! —— 在 WASM 中表现为并发而非并行 // 注意WASM 目前不支持真正的多线程除非使用 Web Workers let (r1, r2, r3) tokio::join!( JsFuture::from(p1), JsFuture::from(p2), JsFuture::from(p3), ); Ok(vec![r1?, r2?, r3?]) }3.4 使用 gloo-timers 处理延时WASM 中不能使用tokio::time::sleep因为 Tokio 的定时器依赖操作系统而要用浏览器原生的setTimeoutuse gloo_timers::future::TimeoutFuture; /// 在 WASM 中使用异步延时 pub async fn delayed_greeting(name: str) { console::log_1(3秒后问好....into()); // 使用浏览器的 setTimeout不阻塞主线程 TimeoutFuture::new(3000).await; console::log_1(format!(你好, {}!, name).into()); }四、边界与注意事项WASM 单线程模型的限制。目前主流的 WASM 运行时包括浏览器中的都是单线程的SharedArrayBuffer 和 Web Workers 除外。这意味着tokio::join!虽然可以让多个 Future 并发执行但它们是交替进行的不是真正并行的。如果一个 Future 里有纯 CPU 计算它会阻塞其他 Future。不能在 WASM 中阻塞等待。std::thread::sleep和其他阻塞操作在 WASM 中会冻住整个浏览器页面。所有的等待操作都必须通过异步实现或者使用gloo-timers这类浏览器 API 的封装。宏任务 vs 微任务。JS 的事件循环区分宏任务setTimeout、I/O和微任务Promise、queueMicrotask。JsFuture使用的是微任务队列这通常没问题但如果有大量并发 Promise可能需要关注微任务队列的堆积。文件系统访问。WASM 默认没有文件系统访问权限。如果需要读写文件必须通过 JS 桥接层使用浏览器的 File API 或 IndexedDB。异步桥接还有一个性能边界JsFuture::from()本身有固定的转换开销大约每次调用几微秒。如果在一个紧密循环里反复调用 JS 异步函数转换开销会堆积。高频场景下可以批量收集请求一次join!多个 Future减少桥接次数。微优化积累起来能省不少总耗时。还有一个序列化边界wasm-bindgen的JsValue和 Rust 类型互转时serde_wasm_bindgen::from_value对大对象1MB的解析会很慢。如果数据量超过这个阈值考虑用SharedArrayBuffer做零拷贝传输。五、总结Rust WASM 和 JS 的异步桥接核心就靠两个 cratewasm-bindgen定义桥接接口和wasm-bindgen-futuresPromise ↔ Future 转换。大多数情况下你只需要把 JS 返回的 Promise 包一层JsFuture::from()然后正常.await就行。反过来#[wasm_bindgen]标记的 Rust async 函数会自动在 JS 侧暴露为返回 Promise 的函数。转换过程虽然是自动的但理解背后的机制特别是单线程事件循环模型对排查性能问题很有帮助。实际开发中我踩过的最大坑是在 WASM 里试图用tokio::time::sleep——这在非 WASM 环境下完全正常但在浏览器里直接 panic。凡是涉及延时的操作一律用gloo-timers或其他浏览器 API 封装。