如何创建捕获Rc<RefCell<_>>不移动所有权的wasm_bindgen Closure
问题根因
你的代码报错核心是两个认知偏差:
Closure::wrap要求传入的闭包必须满足'static生命周期——因为JS回调的存活时间不受Rust栈帧控制,闭包不能借用任何栈上的临时数据,否则函数返回后栈帧销毁,借用就会悬空。- 你虽然用
Rc<RefCell<_>>创建了共享状态,但闭包是通过&mut ||的形式借用了外层栈上的data变量本身,并没有把Rc的所有权移进闭包。当run函数执行结束,栈上的data被drop,闭包持有的引用就成了野指针,和你用不用Rc没有关系。
修复方式
核心逻辑是:为闭包克隆一份Rc引用,通过move闭包把这份Rc的所有权完全移进闭包内部,让闭包不依赖任何外层栈上的变量,自然满足'static要求。
修复后的可运行代码:
use std::{cell::RefCell, rc::Rc}; use wasm_bindgen::{prelude::*, JsCast}; use web_sys::console; macro_rules! console_log { ($($t:tt)*) => { console::log_1(&format_args!($($t)*).to_string().into()) }; } #[wasm_bindgen(start)] pub fn run() { // 初始化全局共享的游戏状态 let game_state = Rc::new(RefCell::new(vec![0])); // 克隆一份Rc,仅增加引用计数,不会复制底层数据 let state_for_closure = Rc::clone(&game_state); // 用move关键字强制闭包拿走捕获变量的所有权,不要加&mut借用闭包 let callback = Closure::wrap(Box::new(move || { state_for_closure.borrow_mut().push(0); console_log!("{:?}", state_for_closure.borrow()); }) as Box<dyn FnMut()>); // 实际使用时把回调绑定到对应的JS事件上,示例为绑定window点击事件 let window = web_sys::window().unwrap(); window.set_onclick(Some(callback.as_ref().unchecked_ref())); // 如果需要回调永久存活(比如绑定全局生命周期的事件)再调用forget,否则建议把callback实例存到全局状态中,避免内存泄漏 callback.forget(); }
扩展说明
- 如果需要在多个不同的JS回调中共享同一份游戏状态,只需要在创建每个闭包前都调用一次
Rc::clone(&game_state),把克隆出的Rc移进对应闭包即可,所有闭包访问修改的都是同一份底层数据。 Rc<RefCell<T>>仅适用于WASM主线程单线程场景,如果后续需要在WebWorker多线程环境使用,替换为Arc<Mutex<T>>/Arc<RwLock<T>>即可,逻辑完全一致。- 不要滥用
closure.forget():它会直接泄漏闭包内存,适合全局生命周期的事件回调;如果回调是绑定到会被销毁的DOM节点上,建议把Closure实例存到和DOM同生命周期的Rust状态中,节点销毁时手动释放闭包,避免内存泄漏。
内容的提问来源于stack exchange,提问作者Michael Francis
相关产品推荐
相关产品推荐

