- 开发工具
【免费下载链接】wasm-bindgen
Facilitating high-level interactions between Wasm modules and JavaScript
导读
本篇文章以 wasm-bindgen 仓库中的examples/webrtc_datachannel示例为核心,逐行剖析如何用 Rust 编写 WebRTC DataChannel 代码,并通过 wasm-bindgen 编译为 Wasm 模块在浏览器中运行。你将掌握:Rust 侧如何创建RTCPeerConnection与RTCDataChannel、如何用Closure注册事件回调(onmessage/ondatachannel/onicecandidate)、如何用wasm-bindgen-futures的JsFuture完成 offer / answer 信令交换,以及如何用npm run serve一键本地构建运行整个示例。
一、示例总览:本地运行与文件结构
该示例的官方说明非常简洁(examples/webrtc_datachannel/README.md):
本地构建并运行该示例:
$ npm run serve然后在浏览器中访问
http://localhost:8080即可运行。
其核心思想是:在单个页面内创建两个RTCPeerConnection(pc1 与 pc2),通过 DataChannel 完成 P2P 消息往返,全部逻辑用 Rust 编写。页面本身无需任何 UI——打开 DevTools 控制台就能看到信令状态与消息日志。
仓库中该示例的文件结构如下:
- examples/webrtc_datachannel/src/lib.rs:Rust 源码,WebRTC 全部逻辑所在;
- examples/webrtc_datachannel/Cargo.toml:Rust 依赖与 web-sys feature 清单;
- examples/webrtc_datachannel/index.js:浏览器入口,动态导入编译产物
./pkg; - examples/webrtc_datachannel/index.html:页面模板,提示打开 DevTools 查看 Console;
- examples/webrtc_datachannel/package.json:npm 脚本,
build/serve; - examples/webrtc_datachannel/webpack.config.js:webpack + wasm-pack 构建配置。
二、Cargo.toml 依赖剖析:如何开启 WebRTC 相关 web-sys features
要让 WebRTC API 在 Rust 侧可用,必须在 web-sys 中显式启用对应的 feature。这是 wasm-bindgen 生态中 web-sys 的设计惯例:所有 API 都按 feature 门控,按需编译,避免 Wasm 包体积膨胀。
examples/webrtc_datachannel/Cargo.toml 中声明了三个关键依赖:
[dependencies] js-sys = { path = "../../crates/js-sys" } wasm-bindgen = { path = "../../" } wasm-bindgen-futures = { path = "../../crates/futures" }以及 web-sys 的 feature 清单:
[dependencies.web-sys] features = [ "MessageEvent", "RtcPeerConnection", "RtcSignalingState", "RtcSdpType", "RtcSessionDescriptionInit", "RtcPeerConnectionIceEvent", "RtcIceCandidate", "RtcDataChannel", "RtcDataChannelEvent", ] path = "../../crates/web-sys"RtcPeerConnection:RTCPeerConnection本身及信令方法(create_offer / create_answer / set_local_description 等);RtcDataChannel/RtcDataChannelEvent:DataChannel 及其事件类型;RtcSessionDescriptionInit/RtcSdpType/RtcSignalingState:offer / answer 信令描述与状态;RtcPeerConnectionIceEvent/RtcIceCandidate:ICE 候选事件;MessageEvent:DataChannel 收到消息时的事件载体。
这些类型对应的 Rust 绑定均可在 crates/web-sys/src/features/ 下找到(如gen_RtcPeerConnection.rs、gen_RtcDataChannel.rs等),它们由 WebIDL 自动生成,方法签名与浏览器 API 一一对应。
三、Rust 入口与 console 绑定
3.1 声明式引入 console 方法
Rust 侧通过extern "C"块 +#[wasm_bindgen(js_namespace = console)]引入 JS 全局对象console的log和warn方法(lib.rs):
#[wasm_bindgen] extern "C" { #[wasm_bindgen(js_namespace = console)] fn log(s: &str); #[wasm_bindgen(js_namespace = console)] fn warn(s: &str); }并配套两个宏,把 Rust 的format_args!格式化为字符串后输出:
macro_rules! console_log { ($($t:tt)*) => (log(&format_args!($($t)*).to_string())) } macro_rules! console_warn { ($($t:tt)*) => (warn(&format_args!($($t)*).to_string())) }3.2 #[wasm_bindgen(start)] 异步入口
整个示例的入口是标注了#[wasm_bindgen(start)]的async fn start() -> Result<(), JsValue>(lib.rs)。它会在 Wasm 模块初始化完成后自动执行,不需要 JS 侧手动调用;返回值Result<(), JsValue>使得过程中任何 WebRTC 调用失败都能向上传播(若入口出错,wasm-bindgen 会抛出对应 JS 异常)。
四、核心流程:pc1 ↔ pc2 的 DataChannel 建立与消息往返
整体流程(代码注释中已给出示意pc1 <=> pc2):
- 创建 pc1、pc2 两个
RTCPeerConnection; - 在 pc1 上创建 DataChannel(
dc1),并注册onmessage; - 在 pc2 上注册
ondatachannel,待协商完成后拿到dc2; - 交换 ICE candidate;
- pc1 生成 offer →
setLocalDescription→ pc2setRemoteDescription→ pc2 生成 answer →setLocalDescription→ pc1setRemoteDescription; - 连接建立后,
dc2发送 "Ping from pc2.dc!",dc1收到后回发 "Pong from pc1.dc!"。
4.1 创建 PeerConnection 与 DataChannel
let pc1 = RtcPeerConnection::new()?; console_log!("pc1 created: state {:?}", pc1.signaling_state()); let pc2 = RtcPeerConnection::new()?;RtcPeerConnection::new()对应浏览器中new RTCPeerConnection(),signaling_state()返回RtcSignalingState枚举(初始为Stable)。
在 pc1 上创建 DataChannel(lib.rs):
let dc1 = pc1.create_data_channel("my-data-channel"); console_log!("dc1 created: label {:?}", dc1.label());create_data_channel的 Rust 签名(gen_RtcPeerConnection.rs)为create_data_channel(this, label: &str) -> RtcDataChannel,对应 JS 的pc.createDataChannel(label);label()返回通道名称。注意:DataChannel 只能由发起方(offerer)创建,接收方通过ondatachannel事件获得通道对象。
4.2 用 Closure 注册 dc1 的 onmessage
web_sys的set_onmessage接收Option<&js_sys::Function>,而 Rust 闭包不能直接传入,必须通过wasm_bindgen::Closure包装并借用其引用:
let dc1_clone = dc1.clone(); let onmessage_callback = Closure::<dyn FnMut(_)>::new(move |ev: MessageEvent| { if let Some(message) = ev.data().as_string() { console_warn!("{:?}", message); dc1_clone.send_with_str("Pong from pc1.dc!").unwrap(); } }); dc1.set_onmessage(Some(onmessage_callback.as_ref().unchecked_ref())); onmessage_callback.forget();要点拆解:
ev.data().as_string():MessageEvent.data()是JsValue,通过as_string()尝试转为 RustString(仅当消息是字符串时成功);dc1_clone = dc1.clone():RtcDataChannel是引用计数类型(内部是JsValue克隆),move闭包内持有副本,避免借用冲突;unchecked_ref():把Closure内部函数引用转为JsValue再转为Function引用;其底层调用set_onmessage(this, value: Option<&Function>)(gen_RtcDataChannel.rs);onmessage_callback.forget():必须调用,否则闭包在函数结束时被 Drop,Closure会销毁对应的 JS 函数,导致回调失效;forget()将内存泄漏作为代价换取闭包永久存活(Web 页面生命周期内这是标准做法)。
4.3 在 pc2 上处理 ondatachannel
接收方 pc2 不主动创建通道,而是监听ondatachannel(lib.rs):
let ondatachannel_callback = Closure::<dyn FnMut(_)>::new(move |ev: RtcDataChannelEvent| { let dc2 = ev.channel(); console_log!("pc2.ondatachannel!: {:?}", dc2.label()); let onmessage_callback = Closure::<dyn FnMut(_)>::new(move |ev: MessageEvent| { if let Some(message) = ev.data().as_string() { console_warn!("{:?}", message); } }); dc2.set_onmessage(Some(onmessage_callback.as_ref().unchecked_ref())); onmessage_callback.forget(); let dc2_clone = dc2.clone(); let onopen_callback = Closure::<dyn FnMut()>::new(move || { dc2_clone.send_with_str("Ping from pc2.dc!").unwrap(); }); dc2.set_onopen(Some(onopen_callback.as_ref().unchecked_ref())); onopen_callback.forget(); }); pc2.set_ondatachannel(Some(ondatachannel_callback.as_ref().unchecked_ref())); ondatachannel_callback.forget();ev.channel()返回RtcDataChannel(此处为dc2);- 回调中又注册了两个嵌套闭包:
dc2.onmessage负责打印收到的消息;dc2.onopen在通道打开后立刻发送"Ping from pc2.dc!"—— 这正是消息往返的起点; - 注意闭包的嵌套使用:外层
ondatachannel_callback持有move捕获的变量,内层闭包再各自clone需要的数据并forget()。
4.4 交换 ICE candidate
真实网络中,ICE candidate 需要通过信令服务器转发;本示例是本地双连接,直接互发(lib.rs):
let pc2_clone = pc2.clone(); let onicecandidate_callback1 = Closure::<dyn FnMut(_)>::new(move |ev: RtcPeerConnectionIceEvent| { if let Some(candidate) = ev.candidate() { console_log!("pc1.onicecandidate: {:#?}", candidate.candidate()); let _ = pc2_clone.add_ice_candidate_with_opt_rtc_ice_candidate(Some(&candidate)); } }); pc1.set_onicecandidate(Some(onicecandidate_callback1.as_ref().unchecked_ref())); onicecandidate_callback1.forget();对称地,pc2 的onicecandidate把候选回传给 pc1。ev.candidate()返回Option<RtcIceCandidate>(候选收集过程中可能为None),candidate()方法取 SDP 字符串;add_ice_candidate_with_opt_rtc_ice_candidate是带Option参数的重载绑定(gen_RtcPeerConnection.rs),对应 JS 的addIceCandidate()。
五、信令流程:用 JsFuture 把 Promise 变成 Rust async
WebRTC 的信令方法(createOffer/createAnswer/setLocalDescription/setRemoteDescription)在浏览器中返回Promise。在 Rust 侧,通过wasm_bindgen_futures::JsFuture::from(promise)把 JS Promise 转为 RustFuture,配合.await以 async/await 风格编排流程(lib.rs)。
5.1 提取 offer 的 SDP
let offer = JsFuture::from(pc1.create_offer()).await?; let offer_sdp = Reflect::get(&offer, &JsValue::from_str("sdp"))? .as_string() .unwrap(); console_log!("pc1: offer {:?}", offer_sdp);create_offer()返回的 Promise resolve 后得到的是一个 RTCSessionDescription 对象,因此用js_sys::Reflect::get读取其sdp字段(Rust 侧没有专门的类型持有该 Promise 解析值,这是通用做法)。注意:此例中直接使用Reflect,而 WebIDL 绑定中也有RtcSessionDescription类型;对sdp字段的读取必须依赖js-sys的动态反射能力。
5.2 pc1:setLocalDescription(offer)
let offer_obj = RtcSessionDescriptionInit::new(RtcSdpType::Offer); offer_obj.set_sdp(&offer_sdp); let sld_promise = pc1.set_local_description(&offer_obj); JsFuture::from(sld_promise).await?; console_log!("pc1: state {:?}", pc1.signaling_state());RtcSessionDescriptionInit是构造 SDP 描述的对象(对应 JS 的RTCSessionDescriptionInit),RtcSdpType::Offer与RtcSdpType::Answer是枚举类型;set_sdp填充 SDP 字符串。信令状态从Stable过渡到HaveLocalOffer。
5.3 pc2:setRemoteDescription(offer) → createAnswer → setLocalDescription(answer)
let offer_obj = RtcSessionDescriptionInit::new(RtcSdpType::Offer); offer_obj.set_sdp(&offer_sdp); let srd_promise = pc2.set_remote_description(&offer_obj); JsFuture::from(srd_promise).await?; let answer = JsFuture::from(pc2.create_answer()).await?; let answer_sdp = Reflect::get(&answer, &JsValue::from_str("sdp"))? .as_string() .unwrap(); let answer_obj = RtcSessionDescriptionInit::new(RtcSdpType::Answer); answer_obj.set_sdp(&answer_sdp); let sld_promise = pc2.set_local_description(&answer_obj); JsFuture::from(sld_promise).await?;pc2 收到 offer 后状态变为HaveRemoteOffer,生成 answer 后变为Stable。
5.4 pc1:setRemoteDescription(answer)
let answer_obj = RtcSessionDescriptionInit::new(RtcSdpType::Answer); answer_obj.set_sdp(&answer_sdp); let srd_promise = pc1.set_remote_description(&answer_obj); JsFuture::from(srd_promise).await?; console_log!("pc1: state {:?}", pc1.signaling_state());至此协商完成,ICE 连接建立,dc2的onopen触发并发送 "Ping",dc1.onmessage收到后回发 "Pong",往返完成,全程可在 DevTools Console 看到日志。
六、前端工程与构建链路
6.1 index.js:动态导入编译产物
examples/webrtc_datachannel/index.js 是整个浏览器入口:
window.addEventListener('load', async () => { await import('./pkg'); });await import('./pkg')动态加载 wasm-pack 生成的 ES 模块(内部含.wasm二进制),导入时即触发#[wasm_bindgen(start)]入口。index.html只提示打开 DevTools 查看 Console(index.html)。
6.2 webpack 配置:wasm-pack 插件
examples/webrtc_datachannel/webpack.config.js 使用@wasm-tool/wasm-pack-plugin,将crateDirectory指向当前目录,自动调用 wasm-pack 编译 Rust → Wasm:
new WasmPackPlugin({ crateDirectory: __dirname }),输出目录为../dist/webrtc_datachannel,mode: 'development',并开启experiments.asyncWebAssembly: true以支持异步 Wasm 加载。
6.3 npm 脚本与本地服务
examples/webrtc_datachannel/package.json:
"scripts": { "build": "webpack", "serve": "webpack serve" }npm run build:一次编译,产物落入dist/webrtc_datachannel;npm run serve:启动 webpack-dev-server(默认http://localhost:8080),即 README 中推荐的运行方式。
依赖版本通过 pnpm catalog 统一管理("catalog:"写法),需要先在仓库根目录安装依赖。
6.4 自动化测试佐证:Playwright 端到端
examples 仓库配有 examples/playwright.spec.ts,会遍历所有含package.json的示例目录自动执行构建与浏览器验证:测试中npm run build后通过 Playwright 访问dist/<dir>/index.html,并把控制台error视为测试失败(msg.type() === 'error'直接抛错)。这意味 webrtc_datachannel 这类示例必须在构建与运行阶段不产生任何 console error 才能通过 CI——即本示例演示的完整 WebRTC 流程(含 ICE、信令、DataChannel 消息往返)是经过仓库级验证的真实可用代码。
七、关键 API 对照表
| 浏览器 API(JS) | wasm-bindgen 绑定(Rust) | 源码位置 |
|---|---|---|
new RTCPeerConnection() | RtcPeerConnection::new() -> Result<RtcPeerConnection, JsValue> | gen_RtcPeerConnection.rs |
pc.createDataChannel(label) | pc.create_data_channel(&str) -> RtcDataChannel | 同上#L776 |
pc.ondatachannel = fn | pc.set_ondatachannel(Option<&Function>) | 同上#L438 |
pc.onicecandidate = fn | pc.set_onicecandidate(Option<&Function>) | 同上 |
pc.addIceCandidate(cand) | pc.add_ice_candidate_with_opt_rtc_ice_candidate(Option<&RtcIceCandidate>) | 同上#L485 |
dc.label | dc.label() -> String | gen_RtcDataChannel.rs#L26 |
dc.onmessage = fn | dc.set_onmessage(Option<&Function>) | 同上#L164 |
dc.send(str) | dc.send_with_str(&str) -> Result<(), JsValue> | 同上#L218 |
new RTCSessionDescriptionInit({type, sdp}) | RtcSessionDescriptionInit::new(RtcSdpType)+set_sdp(&str) | gen_RtcSessionDescriptionInit.rs |
以上所有方法签名均来自 crates/web-sys/src/features/ 下自动生成的绑定源码,可放心作为 API 参考。
八、常见问题与注意事项
forget()忘记调用:Closure未forget()会在作用域结束时 Drop 并释放 JS 函数,事件回调立即失效。示例中所有闭包(含嵌套闭包)都调用了forget()。- DataChannel 必须在 offerer 侧创建:只有 pc1 调用了
create_data_channel,pc2 完全依赖ondatachannel被动接收,这是 WebRTC 规范约束。 Reflect::get解析 Promise 结果:create_offer()/create_answer()resolve 的对象没有现成 Rust 类型,需要js_sys::Reflect::get(&obj, &JsValue::from_str("sdp"))动态取字段;若使用RtcSessionDescription类型则可用其sdp()getter。- 网络环境限制:本示例在本地单页内完成双连接,ICE 不经过真实网络;若需跨设备通信,需自行实现信令服务器转发 SDP 与 ICE candidate。
- feature 门控:漏配任一 web-sys feature 会导致对应 API 编译失败,这是 web-sys 按需编译机制的典型表现。
总结
webrtc_datachannel示例是 wasm-bindgen 生态中完整展示浏览器实时通信能力的最小可运行范例:Rust 编写的全部 WebRTC 逻辑、Closure事件模型、JsFuture的 Promise 桥接、web-sys feature 配置与 webpack + wasm-pack 构建链路一应俱全。阅读完本文后,你可以基于 examples/webrtc_datachannel/src/lib.rs 直接改造出真实场景下的 DataChannel 应用(如聊天、文件分块传输、游戏同步),只需将本地双连接替换为通过信令服务器交换 SDP / ICE 的远程双端即可。
- 开发工具
【免费下载链接】wasm-bindgen
Facilitating high-level interactions between Wasm modules and JavaScript
相关推荐
wasm-bindgen WebRTC DataChannel 实战:在浏览器中实现 Rust ↔ JavaScript 点对点数据通道
wasm bindgen WebRTC DataChannel 实战:在浏览器中实现 Rust ↔ JavaScript 点对点数据通道 导读 本文围绕 was
开发工具LangChain4j 内存向量存储 InMemoryEmbeddingStore 完全指南:原型开发、持久化与源码剖析
LangChain4j 内存向量存储 InMemoryEmbeddingStore 完全指南:原型开发、持久化与源码剖析 本指南围绕 LangChain4j 提
开发工具wasm-bindgen WebGL 示例深度解析:从 Rust 到浏览器绘制一个三角形
wasm bindgen WebGL 示例深度解析:从 Rust 到浏览器绘制一个三角形 本文以 wasm bindgen 仓库中的 webgl 示例 http
开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考