《Rust Wasm 探索之旅:从入门到实践》系列一:当 Rust 遇见 WebAssembly:Wasm 与 Rust 生态初探(入门篇)
《Rust Wasm 探索之旅:从入门到实践》系列二:Rust+Wasm利器:用wasm-pack引爆前端性能!
《Rust Wasm 探索之旅:从入门到实践》系列三:Rust & WASM 之
wasm-bindgen
基础:让 Rust 与 JavaScript 无缝对话《Rust Wasm 探索之旅:从入门到实践》系列四:Rust & WASM 之
wasm-bindgen
进阶:解锁 Rust 与 JS 的复杂数据交互秘籍
Rust & WASM 之 wasm-bindgen
进阶:解锁 Rust 与 JS 的复杂数据交互秘籍
一 引言:突破基础,掌握高级数据交互
在上一篇文章中,我们探讨了如何用 wasm-bindgen
在 Rust 和 JavaScript 之间传递基本数据类型。但当面对结构体、集合类型等复杂数据时,这些基础的方法就显得力不从心了。
今天,就让我们一起踏上探索wasm-bindgen
进阶用法的旅程,带你掌握 Rust 与 JS 复杂数据交互的进阶技巧,让两种语言的协作如丝般顺滑。
二 Rust 结构体到 JavaScript 类的完美转换
(一)结构体定义与导出
wasm-bindgen
为 Rust 结构体提供了无缝的 JavaScript 类生成能力。
// lib.rs
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub struct User {
// 私有字段需要getter/setter
name: String,
// 公开字段,这样 JS 才能访问
pub age: u32,
}
#[wasm_bindgen]
impl User {
// 构造函数
#[wasm_bindgen(constructor)]
pub fn new(name: String, age: u32) -> User {
User { name, age }
}
// 方法
pub fn greet(&self) -> String {
format!("Hello, my name is {} and I'm {} years old.", self.name, self.age)
}
// getter
#[wasm_bindgen(getter)]
pub fn name(&self) -> String {
self.name.clone()
}
// setter
#[wasm_bindgen(setter)]
pub fn set_name(&mut self, name: String) {
self.name = name;
}
}
关键点解析:
#[wasm_bindgen]
:标记这个struct
和impl
块需要暴露给 JS。#[wasm_bindgen(constructor)]
:将new
函数指定为 JS 类的构造函数。- 字段可见性:默认情况下,Rust 的私有字段在 JS 中无法直接访问。那么可以选择将字段设为
pub
,或者提供 getter/setter 方法。 内存管理:JS对象持有Rust内存指针,需手动释放。
(二)在 JavaScript 中使用导出的结构体
编译后,就可以在 JS 中像使用普通类一样操作
User
:
// index.js
import init, { User } from './pkg/your_project_name.js';
async function run() {
await init();
// 使用构造函数创建实例
const user = new User('Alice', 28);
// 访问公共字段
console.log('Age:', user.age); // 输出: Age: 28
// 调用方法
console.log(user.greet()); // 输出: Hello, my name is Alice and I'm 28 years old.
// 使用 getter/setter
console.log('Name:', user.name()); // 输出: Name: Alice
user.set_name('Bob');
console.log(user.greet()); // 输出: Hello, my name is Bob and I'm 28 years old.
// 别忘了释放内存!
user.free();
}
run();
注意: wasm-bindgen
生成的 JS 对象持有一个指向 Rust 内存的指针。当不再需要这个对象时,最好调用 free()
方法来释放 Rust 分配的内存,避免内存泄漏。
三 Rust 接收 JavaScript 对象
(一)灵活但类型不安全的JsValue方案
JsValue
是一个万能类型,可以表示任何 JavaScript 值(对象、数组、字符串、数字等)。当不知道或不关心 JS 对象的具体结构时,它非常有用。
// lib.rs
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
extern "C" {
// 导入 JS 的 console.log
#[wasm_bindgen(js_namespace = console)]
fn log(s: &str);
}
#[wasm_bindgen]
pub fn process_js_object(obj: &JsValue) {
// 你可以使用 serde_wasm_bindgen 将其反序列化为 Rust 结构体
// 我们稍后会详细讲
log(&format!("Received JS value: {:?}", obj));
if let Ok(name) = obj.get("name")?.as_string() {
log(&format!("Received JS value: {}", name));
}
if let Ok(age) = obj.get("age")?.as_f64() {
log(&format!("Received JS value: {}", age));
}
}
// index.js
import init, { process_js_object } from './pkg/your_project_name.js';
async function run() {
await init();
const myObject = {
id: 101,
data: 'some payload',
nested: { a: 1 },
name: 'Tom',
age: 20,
};
process_js_object(myObject); // 在浏览器的 console 中会看到输出
}
run();
这里通过obj.get("name")
和obj.get("age")
来获取对象中的属性值,并使用as_string
和as_f64
方法将其转换为 Rust 中的类型 。
(二)定义特定类型的安全方案
如果 JS 对象的结构是固定的,我们可以使用 #[wasm_bindgen]
来定义一个类型,专门用来接收它。
// lib.rs
use wasm_bindgen::prelude::*;
// 使用 `typescript_type` 来告诉 wasm-bindgen 对应的 TS 类型
#[wasm_bindgen(typescript_type = "MyJsObject")]
pub extern "C" {
// 定义一个类型来映射 JS 对象
#[wasm_bindgen(extends = js_sys::Object)]
#[derive(Debug, Clone)]
type MyJsObject;
// 定义 getter 方法来访问属性
#[wasm_bindgen(method, getter)]
fn id(this: &MyJsObject) -> u32;
#[wasm_bindgen(method, getter)]
fn data(this: &MyJsObject) -> String;
}
#[wasm_bindgen]
pub fn process_typed_object(obj: &MyJsObject) {
// 现在可以安全地访问属性了!
log(&format!("Received typed object with id: {} and data: '{}'", obj.id(), obj.data()));
}
// index.js
import init, { process_typed_object } from './pkg/your_project_name.js';
async function run() {
await init();
const myObject = {
id: 101,
data: 'some payload',
};
process_typed_object(myObject); // 在浏览器的 console 中会看到输出
}
run();
这种方式虽然代码多一点,但换来了编译时的类型安全检查,是更健壮的做法。
(三)serde
+ serde-wasm-bindgen
:终极解决方案
手动处理类型转换太繁琐?serde
(Rust 序列化标准库)+ serde-wasm-bindgen
可自动完成 Rust 结构体与 JS 对象的双向转换,兼顾灵活与安全。
serde
是 Rust 生态中用于序列化和反序列化的标准库。serde-wasm-bindgen
则是连接 serde
和 wasm-bindgen
的桥梁,可以自动将 Rust 结构体和 JsValue
进行相互转换。
# Cargo.toml
[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde-wasm-bindgen = "0.6"
结构体加上 #[derive(Serialize, Deserialize)]
,然后将函数的参数和返回值类型从具体结构体改为 JsValue
即可。
// lib.rs
use wasm_bindgen::prelude::*;
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize)]
pub struct ComplexData {
id: u32,
name: String,
tags: Vec<String>,
active: bool,
}
// 接收 JS 对象,自动反序列化为 Rust 结构体
#[wasm_bindgen]
pub fn process_data_with_serde(val: JsValue) -> Result<JsValue, JsValue> {
// 1. JsValue -> Rust struct
let data: ComplexData = serde_wasm_bindgen::from_value(val)?;
// ... 在 Rust 中对 data 进行各种复杂操作 ...
println!("Processed in Rust: {:?}", data.name);
// 2. Rust struct -> JsValue
let processed_data = ComplexData {
id: data.id + 100,
..data // 使用 struct update 语法
};
Ok(serde_wasm_bindgen::to_value(&processed_data)?)
}
JS 代码变得异常简洁,只需要传递和接收普通的 JS 对象!
// index.js
import init, { process_data_with_serde } from './pkg/your_project_name.js';
async function run() {
await init();
const myData = {
id: 1,
name: 'Wasm-Bindgen',
tags: ['rust', 'webassembly', 'serde'],
active: true
};
try {
const result = process_data_with_serde(myData);
console.log('Result from Rust:', result);
// 输出: {id: 101, name: 'Wasm-Bindgen', tags: ['rust', 'webassembly', 'serde'], active: true}
} catch (error) {
console.error('Error from Rust:', error);
}
}
run();
serde-wasm-bindgen
几乎抹平了两种语言间数据结构的差异,强烈推荐!
四 serde 与 wasm-bindgen 的梦幻联动
(一)serde 与序列化反序列化
serde
是一个极为强大的序列化和反序列化框架,它就像是一座桥梁,连接着 Rust 数据结构与各种存储或传输格式,如 JSON、YAML、CBOR 等。serde
通过提供Serialize
和Deserialize
这两个核心 trait,让我们可以轻松地将 Rust 数据类型转换为其他格式,或者从其他格式还原为 Rust 数据类型 。
(二)serde-wasm-bindgen 的集成
serde-wasm-bindgen
则是将serde
与wasm-bindgen
完美集成的一个库,它使得 Rust 和 JavaScript 之间复杂数据的序列化和反序列化变得更加高效和便捷。
首先,在Cargo.toml
文件中添加依赖:
[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde-wasm-bindgen = "0.2"
假设有一个包含多个Point
的Points
结构体,并且希望在 Rust 和 JavaScript 之间传递它:
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize, Debug)]
pub struct Points {
pub points: Vec<Point>,
}
在 Rust 中,我们可以使用serde-wasm-bindgen
提供的函数来进行序列化和反序列化。
use wasm_bindgen::prelude::*;
use serde_wasm_bindgen::to_value;
#[wasm_bindgen]
pub fn serialize_points(points: &Points) -> Result<JsValue, JsValue> {
// 将Rust结构序列化为JsValue
to_value(points).map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
}
#[wasm_bindgen]
pub fn deserialize_points(value: JsValue) -> Result<Points, JsValue> {
from_value(value).map_err(|e| {
JsValue::from_str(&format!("反序列化失败: {}", e))
})
}
在 JavaScript 中接收并反序列化:
import init, { serialize_points } = from './pkg/your_package_name.js';
async function main() {
await init();
let points = { points: [{ x: 1, y: 2 }, { x: 3, y: 4 }] };
let serialized = serialize_points(points);
let deserialized = deserialize_points(serialized);
console.log('Deserialized points:', deserialized);
}
main();
通过这样的方式,可以方便地在 Rust 和 JavaScript 之间传递复杂的数据结构,并且利用serde
强大的序列化和反序列化能力,确保数据的准确传输和处理 。
五 集合类型的跨语言传递
(一)传递数组和向量
在 Rust 和 JavaScript 之间传递数组和Vec
类型是很常见的需求。在 Rust 中,Vec
是动态数组,非常灵活。
当从 JavaScript 传递数组到 Rust 时,我们可以利用wasm-bindgen
提供的工具。
import init, { sum_of_array: sumOfArray } from './pkg/your_package_name.js';
async function main() {
await init();
let numbers = [1, 2, 3, 4, 5];
let result = sumOfArray(numbers);
console.log(`Sum of array: ${result}`);
}
main();
在 Rust 中实现sum_of_array
函数:
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn sum_of_array(arr: Vec<i32>) -> i32 {
arr.into_iter().sum()
}
这里,wasm-bindgen
会自动将 JavaScript 数组转换为 Rust 的Vec<i32>
类型,可以直接在 Rust 中对其进行操作。
反过来,将 Rust 的Vec
传递给 JavaScript 也很简单。比如在 Rust 中生成一个包含斐波那契数列的Vec
,并传递给 JavaScript:
#[wasm_bindgen]
pub fn fibonacci(n: u32) -> Vec<u32> {
let mut fib = vec![0, 1];
for i in 2..n {
let next = fib[i - 1] + fib[i - 2];
fib.push(next);
}
fib
}
在 JavaScript 中接收并使用这个Vec
:
import init, { fibonacci } from './pkg/your_package_name.js';
async function main() {
await init();
let fibSeq = fibonacci(10);
console.log('Fibonacci sequence:', fibSeq);
}
main();
(二)传递哈希图(HashMap)
HashMap
是 Rust 中常用的键值对集合类型,在与 JavaScript 交互时,我们需要注意键值类型的转换。由于 JavaScript 对象本质上也是键值对结构,我们可以利用这一点来实现HashMap
的传递 。
假设 Rust 中有一个HashMap
,存储着学生的姓名和成绩,把这个HashMap
传递给 JavaScript:
use std::collections::HashMap;
use wasm_bindgen::prelude::*;
use serde::{Serialize, Deserialize};
use serde_wasm_bindgen::to_value;
#[derive(Serialize, Deserialize, Debug)]
struct StudentScores {
scores: HashMap<String, u32>,
}
#[wasm_bindgen]
pub fn get_student_scores() -> Result<JsValue, JsValue> {
let mut scores = HashMap::new();
scores.insert("Alice".to_string(), 95);
scores.insert("Bob".to_string(), 88);
let student_scores = StudentScores { scores };
to_value(&student_scores)
}
在 JavaScript 中接收并解析这个HashMap
:
import init, { get_student_scores } from './pkg/your_package_name.js';
async function main() {
await init();
let scores = get_student_scores();
console.log('Student scores:', scores.scores);
}
main();
通过serde-wasm-bindgen
的序列化和反序列化,我们可以方便地在 Rust 和 JavaScript 之间传递HashMap
类型的数据,确保数据的完整性和正确性 。
六 所有权和生命周期:跨语言边界的思考
当数据跨越 Wasm
边界时,内存管理和所有权变得尤为重要。
(一)所有权转移
在 Rust 与 JavaScript 通过wasm-bindgen
交互时,所有权转移是一个需要特别关注的概念。Rust 的所有权系统是其内存安全的基石,当数据在 Rust 和 JavaScript 之间传递时,所有权的规则依然适用 。
- 所有权转移:当 JS 将一个对象(如数组)传递给 Rust 的
Vec<T>
时,wasm-bindgen
会在 Wasm 内存中创建一个新的Vec
,数据被复制过来。JS 端的原始数据不受影响。 - 借用 (
&T
,&mut T
):当使用借用时,通常意味着数据不会被复制,而是直接操作 JS 内存中的数据(或其在 Wasm 内存中的一个临时表示)。这通常更高效,但也更复杂。 #[wasm_bindgen(start)]
:当 Wasm 模块加载时,这个函数会被调用。适合做一些初始化工作。- 手动内存管理:对于通过
#[wasm_bindgen]
导出的结构体,如例子中的User
,它的内存是由 Rust 的分配器管理的。当 JS 不再需要它时,调用user.free()
是一个好习惯。
简单原则: 优先使用 serde-wasm-bindgen
,它能处理好大部分内存细节。对于高性能场景或需要精细控制的场合,再考虑手动管理和借用。
(二)生命周期管理
在 Wasm 交互中,数据的生命周期管理同样至关重要。由于 WebAssembly 运行在浏览器环境中,与传统的 Rust 程序运行环境有所不同,需要特别注意避免悬空指针和内存泄漏等问题 。
假设我们有一个 Rust 函数,返回一个指向内部数据的引用:
#[wasm_bindgen]
pub fn get_internal_data() -> &'static str {
"This is internal data"
}
这里使用'static
生命周期标注,表示这个字符串字面量的生命周期是整个程序的生命周期,这样就确保了在任何时候返回的引用都是有效的 。
在实际开发中,还可以利用 Rust 的智能指针,如Rc
(引用计数)和Arc
(原子引用计数),来管理数据的生命周期。
七 可选参数和默认值的巧妙处理
(一)在 Rust 中定义
在 Rust 中,虽然不像一些其他语言(如 Python)那样直接支持在函数定义中设置参数默认值,但我们可以通过Option
枚举来实现类似的效果。
// lib.rs
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn greet_with_options(name: String, age: Option<u32>) {
match age {
Some(a) => log(&format!("Hello, {}! You are {} years old.", name, a)),
None => log(&format!("Hello, {}! Your age is a secret.", name)),
}
}
(二)在 JavaScript 中调用
在 JavaScript 中调用这个带有可选参数的 Rust 函数时,我们可以根据需要传递第二个参数,也可以不传递:
import init, { greet_with_options } from './pkg/your_package_name.js';
async function main() {
await init();
greet_with_options('Alice', 30); // "Hello, Alice! You are 30 years old."
greet_with_options('Bob', undefined); // "Hello, Bob! Your age is a secret."
greet_with_options('Charlie', null); // "Hello, Charlie! Your age is a secret."
}
main();
八 总结
今天我们一起探索了 wasm-bindgen
的强大功能,从导出 Rust 结构体到利用 serde
实现无缝的复杂数据交换。
核心要点回顾:
- 直接导出
struct
:使用#[wasm_bindgen]
,可生成对应的 JS 类,但需要注意手动内存管理 (free()
)。 - 处理 JS 对象:
JsValue
灵活,但类型不安全;定义类型映射更健壮。 - 终极神器
serde
:通过serde-wasm-bindgen
,可以轻松地在 Rust 结构体和普通 JS 对象之间进行自动转换,是处理复杂数据的首选方案。 - 集合类型不是问题:
Vec<T>
对应Array
,HashMap
对应Map
。 - 内存安全:始终关注数据在 Wasm 边界上的所有权和生命周期。
掌握了这些进阶技巧,就拥有了构建复杂、高性能 WebAssembly 应用的坚实基础。现在,你可以尝试将你项目中更核心、更复杂的逻辑用 Rust 来实现,并与你的 JavaScript 前端无缝集成了!