拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

wasm-bindgen 中使用 Serde 序列化任意数据并在 Rust 与 JavaScript 之间传递(serde-wasm-bindgen 实战指南)

wasm-bindgen 中使用 Serde 序列化任意数据并在 Rust 与 JavaScript 之间传递(serde-wasm-bindgen 实战指南)
  • 开发工具

【免费下载链接】wasm-bindgen

Facilitating high-level interactions between Wasm modules and JavaScript

项目地址:https://gitcode.com/gh_mirrors/wa/wasm-bindgen
点击查看免费下载

在 Rust 编写的 WebAssembly 模块与 JavaScript 之间传递数据,传统上受限于 wasm ABI 能够表达的类型:字符串、数值、布尔值以及JsValue本身。而当你需要跨越边界传递HashMap、嵌套Vec、数组、Option乃至自定义结构体时,常规的#[wasm_bindgen]导出就无能为力了。本指南以 guide/src/reference/arbitrary-data-with-serde.md 为核心,系统讲解如何借助serde-wasm-bindgen把任意 Rust 数据类型序列化为JsValue传给 JavaScript,再把 JavaScript 对象反序列化回 Rust 类型,并对比了基于 JSON 的gloo-utils替代方案。读完本文,你将掌握两种在 wasm-bindgen 项目中打通复杂数据边界的完整方案,并了解它们各自的性能特性与取舍。

为什么需要 Serde:wasm ABI 的天然限制

wasm-bindgen通过生成胶水代码把 Rust 函数导出给 JavaScript,但其核心机制是 wasm 的导入/导出函数签名——参数与返回值必须落在有限的 ABI 类型集合内(数字、布尔、字符串、JsValue、借用切片等)。这意味着像下面这个结构体,无法直接通过#[wasm_bindgen]导出到 JavaScript:

use serde::{Serialize, Deserialize}; use std::collections::HashMap; #[derive(Serialize, Deserialize)] pub struct Example { pub field1: HashMap<u32, String>, pub field2: Vec<Vec<f32>>, pub field3: [f32; 4], }

HashMap、嵌套Vec、定长数组等类型都不在 wasm ABI 的"可裸传"集合中。但这一切型都实现了 Serde 的Serialize/Deserializetrait,因此可以借助 Serde 生态将它们编码为 JavaScript 原生数据结构(Map、Array、Object、Number、String等),再以单个JsValue的形式跨越边界。这就是serde-wasm-bindgen所做的事情。

注意:参与序列化的自定义结构体不需要标注#[wasm_bindgen]宏,它只是一个普通的 Rust 类型,仅在 Rust 一侧存在。真正暴露给 JS 的是包装了JsValue转换的导出函数。

第一步:添加依赖

在Cargo.toml中同时加入两个 crate:serde本身(需要开启derive特性以便使用#[derive(Serialize, Deserialize)])和serde-wasm-bindgen:

[dependencies] serde = { version = "1.0", features = ["derive"] } serde-wasm-bindgen = "0.4"

serde的derive特性会引入serde_derive,为你的类型生成 trait 实现;serde-wasm-bindgen则提供把T: Serialize转换为JsValue、把JsValue转换回T: Deserialize的两个核心函数。

第二步:为类型派生Serialize与Deserialize

给需要跨界的类型加上#[derive(Serialize, Deserialize)]。Serde 的派生宏会对所有字段逐一生成序列化/反序列化逻辑,因此每个成员的类型也必须实现这两个 trait。内置类型(数字、String、Vec、数组、HashMap、Option、元组等)天然满足;自定义嵌套类型同样派生即可。

以文档中的Example为例,它同时包含HashMap<u32, String>、Vec<Vec<f32>>与[f32; 4]三类"ABI 不友好"的成员,但全部满足 Serde 要求:

use serde::{Serialize, Deserialize}; #[derive(Serialize, Deserialize)] pub struct Example { pub field1: HashMap<u32, String>, pub field2: Vec<Vec<f32>>, pub field3: [f32; 4], }

仓库内亦有同类实践可供参考:crates/typescript-tests/src/typescript_type.rs 中,TextStyle结构体同时标注了#[wasm_bindgen]与#[derive(Serialize, Deserialize)],通过serde_wasm_bindgen::from_value把从 JavaScript 传入的接口对象直接转换成 Rust 结构。

第三步:用serde_wasm_bindgen::to_value发送到 JavaScript

Rust 侧导出函数构造数据后,调用serde_wasm_bindgen::to_value(&example)得到Result<JsValue, Error>,unwrap()后把JsValue作为返回值交给 JavaScript:

use wasm_bindgen::prelude::*; use std::collections::HashMap; use serde::{Serialize, Deserialize}; #[derive(Serialize, Deserialize)] pub struct Example { pub field1: HashMap<u32, String>, pub field2: Vec<Vec<f32>>, pub field3: [f32; 4], } #[wasm_bindgen] pub fn send_example_to_js() -> JsValue { let mut field1 = HashMap::new(); field1.insert(0, String::from("ex")); let example = Example { field1, field2: vec![vec![1., 2.], vec![3., 4.]], field3: [1., 2., 3., 4.] }; serde_wasm_bindgen::to_value(&example).unwrap() }

调用成功后,JavaScript 拿到的JsValue是一个普通对象,其中field1是Map、field2是成员为数字数组的Array、field3是数字Array。序列化失败的场景(例如类型不可序列化)会返回Err,此时需要根据具体错误处理,而不是盲目unwrap。

第四步:用serde_wasm_bindgen::from_value从 JavaScript 接收

反向路径同样简单:导出函数接收一个JsValue参数,用serde_wasm_bindgen::from_value(val)反序列化回目标类型:

#[wasm_bindgen] pub fn receive_example_from_js(val: JsValue) { let example: Example = serde_wasm_bindgen::from_value(val).unwrap(); // ... 使用 example }

这里的类型注解是必须的,因为from_value是泛型函数,需要借助目标类型推断Deserialize实现。如果 JavaScript 传入的值结构与目标类型不匹配(例如缺少字段、类型不符),from_value会返回Err。

仓库测试中可以看到完整的"出/入"双向验证:在 tests/wasm/js_objects.rs 的serde测试(feature = "serde-serialize"门控)里,Rust 侧先构造含Option<SerdeBar>、嵌套结构体的SerdeFoo序列化为JsValue,交予 JavaScript 侧校验(见 tests/wasm/js_objects.js,verify_serde用deepStrictEqual断言收到的对象形状),再接收 JS 返回的对象反序列化回SerdeFoo并逐字段断言。这个用例同时印证了Option、嵌套结构体以及undefined无法反序列化为i32(ok()为None)等边界行为。

JavaScript 侧用法:拿到对象后自由操作再回传

由于serde-wasm-bindgen生成的是 JavaScript 原生数据结构,JavaScript 侧可以像操作普通对象一样直接读取、修改,再传回 wasm:

import { send_example_to_js, receive_example_from_js } from "example"; // 从 wasm 获取 example 对象。 let example = send_example_to_js(); // 在 Vec<Vec<f32>> 末尾追加一个 Vec 元素。 example.field2.push([5, 6]); // 把修改后的对象传回 wasm。 receive_example_from_js(example);

注意这里field2是真正的Array,所以可以直接push——修改后的数据回传 wasm 时,from_value会按Vec<Vec<f32>>重新解析。整个往返无需任何额外桥接代码。

另一种方案:基于 JSON 的gloo-utils扩展

serde-wasm-bindgen直接逐个操作 JavaScript 值,因此在 Rust 与 JavaScript 之间会产生大量来回调用,某些场景下可能偏慢。替代思路是:把值先序列化成 JSON 字符串,在另一端解析。浏览器内置的 JSON 实现通常很快,所以这种方式在部分场景下能超过serde-wasm-bindgen的性能;但它只支持可被 JSON 表达的类型,会丢掉serde-wasm-bindgen支持的一些重要类型,例如Map、Set以及 ArrayBuffer(二进制缓冲区)等。

该方案由gloo_utils的JsValueSerdeExt扩展 trait 提供,在Cargo.toml中开启其serde特性:

[dependencies] gloo-utils = { version = "0.1", features = ["serde"] }

Rust 侧用法几乎与serde-wasm-bindgen同构,只是把to_value/from_value换成扩展方法:

use gloo_utils::format::JsValueSerdeExt; #[wasm_bindgen] pub fn send_example_to_js() -> JsValue { let mut field1 = HashMap::new(); field1.insert(0, String::from("ex")); let example = Example { field1, field2: vec![vec![1., 2.], vec![3., 4.]], field3: [1., 2., 3., 4.] }; JsValue::from_serde(&example).unwrap() } #[wasm_bindgen] pub fn receive_example_from_js(val: JsValue) { let example: Example = val.into_serde().unwrap(); // ... 使用 example }

gloo-utils的 JSON 方案在 wasm-bindgen 仓库中也有长期测试覆盖:tests/wasm/js_objects.rs中的serde测试(受serde-serializefeature 门控)使用的正是JsValue::from_serde与JsValue::into_serde,而 tests/wasm/js_objects.js 以deepStrictEqual校验了往返对象的一致性。这为读者提供了可直接对照的参考实现。

两种方案如何取舍

需要澄清的是,JSON 方案并非永远更快——它的实际速度介于serde-wasm-bindgen的0.2x 到 2x之间,具体取决于 JavaScript 运行时以及所传递的值本身;同时 JSON 方案通常带来更大的代码体积(需要携带serde_json相关的序列化/反序列化逻辑)。结论是:不要凭直觉选型,请针对自己的数据结构、目标浏览器/运行时分别做性能剖析(profile)。若你的数据中包含Map、Set、ArrayBuffer 等 JSON 无法表达的类型,则只能选择serde-wasm-bindgen。

历史背景:为什么from_serde/into_serde不在 wasm-bindgen 里了

在 wasm-bindgen 的早期版本中,基于 JSON 的 Serde 支持(JsValue::from_serde与JsValue::into_serde)曾内置在 wasm-bindgen 自身。但这样做强制引入了对serde_json的依赖,进而带来一个现实问题:在serde_json的某些特性与其它 crate 的特性组合下,serde_json会与wasm-bindgen形成循环依赖(circular dependency),这在 Rust 中是非法的,导致用户代码编译失败。为此,这些方法被抽取到gloo-utils中,以扩展 trait(JsValueSerdeExt)的形式提供,wasm-bindgen 内的原始方法则被弃用(deprecated)。这也是为什么文档与上述测试代码中会见到#[allow(deprecated)]标注(参见 tests/wasm/js_objects.rs)——仓库自身的回归测试仍在验证这一遗留 API,但新代码应当优先使用gloo-utils或serde-wasm-bindgen。

仓库中的更多实践参考

  • crates/typescript-tests/src/typescript_type.rs:用serde_wasm_bindgen::from_value把 TypeScript 接口对象直接反序列化为 Rust 结构体的构造器模式;
  • crates/typescript-tests/Cargo.toml:实际工程中serde+serde-wasm-bindgen的依赖写法;
  • examples/raytrace-parallel/src/lib.rs:在并行光线追踪示例中,从 JavaScript 传入的对象经serde_wasm_bindgen::from_value还原为 Rust 数据结构;
  • tests/wasm/js_objects.rs 与 tests/wasm/js_objects.js:双向序列化/反序列化的完整回归测试。

小结

在 wasm-bindgen 项目中传递任意复杂数据,有两条成熟路线:serde-wasm-bindgen直接操作 JavaScript 值,支持Map、Set、ArrayBuffer 等 JSON 之外的丰富类型,适合数据形状复杂、类型多样或需要二进制数据的场景;gloo-utils的 JSON 方案则借助浏览器高效的 JSON 解析,在简单 JSON 类型上有望获得更优速度与更小的运行时开销,但类型覆盖面窄、代码体积更大。无论选择哪条路线,都建议先用真实数据在自己的目标运行时上做基准测试,再决定最终方案。

  • 开发工具

【免费下载链接】wasm-bindgen

Facilitating high-level interactions between Wasm modules and JavaScript

项目地址:https://gitcode.com/gh_mirrors/wa/wasm-bindgen
点击查看免费下载
上一篇:Reference 项目 Lua 5.4 速查表:从基础语法到表、元表与文件 IO 的完整实战指南
下一篇:tiny11builder:一条脚本把 6GB 的 Windows 11 安装映像压成精简版

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

返回列表