如何在wasm_bindgen限制下存储带生命周期参数的结构体引用
问题描述
请问是否可以修改结构体的泛型生命周期?我现有一个外部库,通过带生命周期参数的结构体引用对外提供必要数据,我需要使用wasm_bindgen对外暴露该数据,因此在wasm模块中定义了如下结构体:
#[wasm_bindgen] struct Event { // 编译不通过:引用来自外部库 inner: &'a InnerEvent<'b> } impl<'a,'b> From<&'a InnerEvent<'b>> for Event { fn from(e: &'a InnerEvent<'b>) -> Self { Event { inner: e } } }
现在的问题是,'a和'b都是外部库传入的生命周期,但wasm-bindgen存在限制:需要通过WebAssembly对外访问的Rust结构体自身不能携带任何泛型参数,因此Event结构体不能声明引用的'a或'b生命周期。
我可以通过使用裸指针去掉'a的限制,但无法用同样的方式处理'b。技术上我可以用'static来模拟,但我没法将结构体的泛型生命周期参数从'a修改为'static。
我如何才能在不违反wasm-bindgen限制的前提下,存储&'a InnerEvent<'b>的引用?
补充场景
我正在为一个支持自定义用户数据发布/订阅的库编写封装层,代码如下:
pub struct Transaction<'txn> { // 字段省略 } pub struct SomeStruct { pub fn subscribe<F>(&mut self, f: F) -> Subscription where F: Fn(&Transaction, &Event) -> () + 'static { // 方法实现省略 } }
现在我需要将SomeStruct暴露给WebAssembly,因此我创建了wasm-bindgen封装层,需要将其订阅能力暴露给JavaScript侧,代码如下:
#[wasm_bindgen] pub struct SomeStructWrapper(SomeStruct); #[wasm_bindgen] impl SomeStructWrapper { #[wasm_bindgen] pub fn observe(&mut self, f: js_sys::Function) -> SubscriptionWrapper { let sub = self.0.observe(move |transaction, event| { // transaction类型是&'a Transaction<'txn>,event类型是&'a Event let e = EventWrapper::new(e, txn); let arg: JsValue = e.into(); f.call1(&JsValue::UNDEFINED, &arg); }); SubscriptionWrapper(sub) } } #[wasm_bindgen] pub struct SubscriptionWrapper(Subscription);
现在的问题是我需要在JavaScript回调中持有Rust回调的两个参数(transaction和event)的引用,这意味着EventWrapper需要将它们存储为字段:
// 下方代码无法编译,因为wasm_bindgen不允许其修饰的结构体声明生命周期参数 #[wasm_bindgen] pub struct EventWrapper { transaction: &'a Transaction<'txn>, inner_event: &'a Event, } // 我们可以通过将引用转换为裸指针去掉'a的限制,但这无法解决'txn生命周期的问题 #[wasm_bindgen] pub struct EventWrapper { transaction: *const Transaction<'txn>, inner_event: *const Event, }
首先明确:Rust的生命周期是编译期的静态标记,没有运行时表示,无法在运行时修改生命周期参数,也不能强行把非'static的生命周期标注为'static,否则会产生未定义行为。
结合你的场景,有三种可行的实现方式:
方案1:仅在回调执行期间访问数据,不持有引用到回调外
你提到的subscribe方法要求回调的签名是Fn(&Transaction, &Event) -> () + 'static,这里的'static约束的是闭包本身的生命周期,不是闭包参数的生命周期。也就是说&Transaction和&Event的有效期仅在闭包执行的这段时间,回调执行完成后底层数据就会被外部库回收。
这种场景下你不需要在EventWrapper里长期持有这两个引用,只需要在回调执行的时候把需要的字段从Transaction和Event里拷贝出来,直接构造JsValue传给JS即可,不需要封装成带引用的EventWrapper结构体。示例如下:
#[wasm_bindgen] impl SomeStructWrapper { #[wasm_bindgen] pub fn observe(&mut self, f: js_sys::Function) -> SubscriptionWrapper { let sub = self.0.subscribe(move |transaction, event| { // 直接提取需要的字段,构造JS可用的对象,不需要保存引用 let txn_id = transaction.id(); let event_data = event.data().clone(); let js_obj = js_sys::Object::new(); js_sys::Reflect::set(&js_obj, &"txnId".into(), &txn_id.into()).unwrap(); js_sys::Reflect::set(&js_obj, &"data".into(), &event_data.into()).unwrap(); f.call1(&JsValue::UNDEFINED, &js_obj).unwrap(); }); SubscriptionWrapper(sub) } }
这种方案不需要unsafe代码,是最安全的实现方式。
方案2:确认生命周期安全的前提下,使用类型擦除
如果确实需要在JS侧持有Transaction和Event的引用,首先你要确认:你持有引用的这段时间,底层的外部库一定不会释放这两个对象,否则会出现野指针。
确认安全后,可以用unsafe代码擦除生命周期,把裸指针的泛型参数标注为'static,绕过编译期检查:
#[wasm_bindgen] pub struct EventWrapper { transaction: *const Transaction<'static>, inner_event: *const Event, } impl EventWrapper { pub fn new<'a, 'txn>(txn: &'a Transaction<'txn>, event: &'a Event) -> Self { // 安全前提:EventWrapper存活期间,txn和event指向的内存一定有效 Self { transaction: unsafe { std::mem::transmute(txn as *const Transaction<'txn>) }, inner_event: event as *const Event, } } } // 给JS暴露的访问方法,注意所有访问都要加unsafe,确保使用时指针有效 #[wasm_bindgen] impl EventWrapper { pub fn get_txn_id(&self) -> u64 { unsafe { (*self.transaction).id() } } pub fn get_event_data(&self) -> Vec<u8> { unsafe { (*self.inner_event).data().clone() } } }
注意:这种方案你必须自己保证生命周期安全:JS侧不能在订阅回调执行完成后再访问
EventWrapper实例,否则会触发未定义行为,严重时会导致Wasm实例崩溃。如果不能保证这一点,不要用这个方案。
方案3:修改外部库或者封装层的所有权逻辑
如果外部库允许你获取InnerEvent、Transaction的所有权拷贝,或者支持Arc引用计数,那你可以把值拷贝/移动到EventWrapper里,完全不需要处理生命周期问题。这是长期来看最稳妥的方案。
内容的提问来源于stack exchange,提问作者Bartosz Sypytkowski

