197605d8-d36f-4347-9fba-e1fa2915aebc_1746803106274269925_origin~tplv-a9rns2rl98-image-qvalue.jpeg

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]:标记这个 structimpl 块需要暴露给 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_stringas_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 则是连接 serdewasm-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通过提供SerializeDeserialize这两个核心 trait,让我们可以轻松地将 Rust 数据类型转换为其他格式,或者从其他格式还原为 Rust 数据类型 。

(二)serde-wasm-bindgen 的集成

serde-wasm-bindgen则是将serdewasm-bindgen完美集成的一个库,它使得 Rust 和 JavaScript 之间复杂数据的序列化和反序列化变得更加高效和便捷。

首先,在Cargo.toml文件中添加依赖:

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

假设有一个包含多个PointPoints结构体,并且希望在 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 实现无缝的复杂数据交换。

核心要点回顾:

  1. 直接导出 struct:使用 #[wasm_bindgen],可生成对应的 JS 类,但需要注意手动内存管理 (free())。
  2. 处理 JS 对象JsValue 灵活,但类型不安全;定义类型映射更健壮。
  3. 终极神器 serde:通过 serde-wasm-bindgen,可以轻松地在 Rust 结构体和普通 JS 对象之间进行自动转换,是处理复杂数据的首选方案。
  4. 集合类型不是问题Vec<T> 对应 ArrayHashMap 对应 Map
  5. 内存安全:始终关注数据在 Wasm 边界上的所有权和生命周期。

掌握了这些进阶技巧,就拥有了构建复杂、高性能 WebAssembly 应用的坚实基础。现在,你可以尝试将你项目中更核心、更复杂的逻辑用 Rust 来实现,并与你的 JavaScript 前端无缝集成了!

results matching ""

    No results matching ""