如何将可变Rust结构体传入JS回调并保留Wasm中的修改?
如何通过wasm-bindgen让JavaScript直接操作Rust结构体并同步修改原对象
问题背景
使用wasm_bindgen传递包含Vec等不可Copy类型的Rust结构体到JavaScript回调时,Serde序列化仅能传递对象副本,无法调用Rust定义的方法,也无法同步修改Rust内存中的原对象。直接传递结构体需克隆副本,导致JS修改无法同步回原对象;手动复制字段会触发借用检查错误,直接调用原对象方法则会出现别名不安全问题。
解决方案
方案一:安全共享可变引用(推荐)
利用Rc<RefCell<T>>在单线程Wasm环境下实现结构体的共享可变访问,通过包装类将引用暴露给JS,确保所有修改直接作用于原对象。
Rust代码实现
use wasm_bindgen::prelude::*; use std::rc::Rc; use std::cell::RefCell; #[wasm_bindgen] #[derive(Debug)] pub struct SomeStruct { data: Vec<i32>, } #[wasm_bindgen] impl SomeStruct { #[wasm_bindgen(constructor)] pub fn new() -> Self { SomeStruct { data: vec![1, 2, 3] } } pub fn add_data(&mut self, value: i32) { self.data.push(value); } pub fn get_data(&self) -> JsValue { JsValue::from_serde(&self.data).unwrap() } } // 用于JS持有的结构体引用包装类 #[wasm_bindgen] pub struct SomeStructHandle { inner: Rc<RefCell<SomeStruct>>, } #[wasm_bindgen] impl SomeStructHandle { pub fn from_struct(s: SomeStruct) -> Self { SomeStructHandle { inner: Rc::new(RefCell::new(s)), } } // 暴露修改方法,内部通过RefCell获取可变引用 pub fn add_data(&self, value: i32) { self.inner.borrow_mut().add_data(value); } // 暴露数据获取方法 pub fn get_data(&self) -> JsValue { self.inner.borrow().get_data() } } // 示例:将结构体引用传递给JS回调 #[wasm_bindgen] pub fn run_with_struct(callback: &js_sys::Function) { let struct_instance = SomeStruct::new(); let handle = SomeStructHandle::from_struct(struct_instance); // 将handle作为参数传入JS回调 callback.call1(&JsValue::null(), &JsValue::from(handle)).unwrap(); // 验证原对象已被修改 let final_data = handle.inner.borrow().get_data(); web_sys::console::log_1(&final_data); }
JavaScript代码调用
import init, { run_with_struct } from './pkg/your_wasm_module.js'; async function main() { await init(); run_with_struct((handle) => { // 调用Rust方法修改原对象 handle.add_data(4); console.log("JS中获取的数据:", handle.get_data()); }); } main();
方案说明
Rc提供引用计数,确保结构体在JS持有引用期间不会被释放;RefCell在运行时检查借用规则,避免编译时的借用冲突- JS持有的
SomeStructHandle是原结构体的引用,所有方法调用直接作用于原对象,修改自动同步回Rust - 无需克隆结构体,避免了副本带来的数据不一致问题
方案二:Unsafe原始指针(性能导向)
如果追求极致性能或有特殊需求,可使用原始指针直接传递结构体地址,但需手动管理内存安全,避免悬空指针和内存泄漏。
Rust代码实现
use wasm_bindgen::prelude::*; use std::boxed::Box; #[wasm_bindgen] #[derive(Debug)] pub struct SomeStruct { data: Vec<i32>, } #[wasm_bindgen] impl SomeStruct { #[wasm_bindgen(constructor)] pub fn new() -> Self { SomeStruct { data: vec![1, 2, 3] } } pub fn add_data(&mut self, value: i32) { self.data.push(value); } pub fn get_data(&self) -> JsValue { JsValue::from_serde(&self.data).unwrap() } } // 创建结构体并返回原始指针 #[wasm_bindgen] pub fn create_struct_ptr() -> *mut SomeStruct { Box::into_raw(Box::new(SomeStruct::new())) } // 通过指针调用修改方法 #[wasm_bindgen] pub fn add_data_to_ptr(ptr: *mut SomeStruct, value: i32) { unsafe { (*ptr).add_data(value); } } // 通过指针获取数据 #[wasm_bindgen] pub fn get_data_from_ptr(ptr: *mut SomeStruct) -> JsValue { unsafe { (*ptr).get_data() } } // 释放结构体内存,避免泄漏 #[wasm_bindgen] pub fn drop_struct_ptr(ptr: *mut SomeStruct) { unsafe { Box::from_raw(ptr); } } // 将指针传递给JS回调 #[wasm_bindgen] pub fn run_with_struct_ptr(callback: &js_sys::Function) { let ptr = create_struct_ptr(); // 将指针转为JsValue传入回调 let ptr_js = JsValue::from(ptr as u32); callback.call1(&JsValue::null(), &ptr_js).unwrap(); // 验证修改 let final_data = get_data_from_ptr(ptr); web_sys::console::log_1(&final_data); // 必须手动释放内存 drop_struct_ptr(ptr); }
JavaScript代码调用
import init, { run_with_struct_ptr, add_data_to_ptr, get_data_from_ptr } from './pkg/your_wasm_module.js'; async function main() { await init(); run_with_struct_ptr((ptr) => { add_data_to_ptr(ptr, 4); console.log("JS中获取的数据:", get_data_from_ptr(ptr)); }); } main();
方案注意点
- 必须手动调用
drop_struct_ptr释放内存,否则会造成内存泄漏 - 确保指针在使用期间结构体未被释放,否则会出现悬空指针导致未定义行为
- 该方式跳过Rust的借用检查,仅推荐在单线程Wasm环境下使用,多线程场景需额外加锁
内容的提问来源于stack exchange,提问作者xarantolus
相关产品推荐
相关产品推荐

