You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

反复实例化并释放WASM模块引发"内存越界访问"问题排查

问题描述

我用wasm-bindgen构建了一个接收JS闭包的Rust结构体,在Node.js环境运行时出现内存越界错误。

Rust代码:

use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub struct WasmThing {
    func: Box<dyn FnMut()>,
}

#[wasm_bindgen]
impl WasmThing {
    #[wasm_bindgen(constructor)]
    pub fn new(js_func: js_sys::Function) -> WasmThing {
        WasmThing {
            func: Box::new(move || {
                js_func.call0(&wasm_bindgen::JsValue::UNDEFINED).unwrap();
            }),
        }
    }
}

通过wasm-pack生成可导入的WASM包后,在Node.js中循环创建大量WasmThing实例并立即释放:

while (true) {
  const thing = new WasmThing(() => {
    console.log("Hello there!");
  });
  thing.free();
}

运行约250次循环后触发错误:

RuntimeError: Memory Access out of bounds
  at wasm://wasm/0168863a:wasm-function[7107]:0x3ffc48
  at wasm://wasm/0168863a:wasm-function[137]:0x100fa1
  at new WasmThing (/home/ari/src/wasm-fail/index.js:1)

已知情况:

  • Node性能分析器和系统任务管理器未显示内存占用上升
  • 使用wasm-bindgen弱引用特性后问题仍存在
  • 在Node、Chrome、WebKit中均可复现,Firefox中无法复现
  • 猜测可能是闭包未被GC回收,或是WASM内部某种未绑定物理内存的内存类型耗尽

排查与解决方案

核心原因分析

问题根源在于Rust侧的Box<dyn FnMut()>闭包未与JS侧js_sys::Function的生命周期正确绑定,加上wasm-bindgen生成的free()方法未能彻底清理Rust堆上的闭包资源,导致WASM线性内存出现悬空引用或资源泄漏,最终触发内存越界。Firefox无此问题是因为其JS引擎GC策略对跨语言资源的回收时机更激进。

具体排查步骤

  1. 检查自动生成的绑定代码
    查看wasm-pack输出的index.js,确认free()方法是否正确调用了Rust结构体的析构逻辑。默认#[wasm_bindgen]生成的free()会触发Drop,但dyn FnMut()这类动态分发的trait对象可能存在析构不彻底的情况。

  2. 显式实现Drop trait
    手动实现Drop确保闭包资源被清理:

    #[wasm_bindgen]
    impl Drop for WasmThing {
        fn drop(&mut self) {
            // 将闭包重置为无操作函数,释放原引用
            self.func = Box::new(|| {});
        }
    }
    

    这能保证free()调用时,Rust堆上的闭包资源被正确重置,避免悬空引用。

  3. 替换动态trait对象为具体类型
    Box<dyn FnMut()的动态分发会在WASM堆存储额外vtable信息,容易引发生命周期不匹配。改为直接持有js_sys::Function:

    #[wasm_bindgen]
    pub struct WasmThing {
        func: js_sys::Function,
    }
    
    #[wasm_bindgen]
    impl WasmThing {
        #[wasm_bindgen(constructor)]
        pub fn new(js_func: js_sys::Function) -> WasmThing {
            WasmThing { func: js_func }
        }
    
        // 若需调用闭包,单独暴露方法
        pub fn call(&self) {
            self.func.call0(&wasm_bindgen::JsValue::UNDEFINED).unwrap();
        }
    }
    

    这种方式直接交由JS引擎管理函数引用的GC,避免跨语言资源生命周期不匹配问题。

  4. 强制触发JS GC验证
    Node.js启动时添加--expose-gc参数,在循环中强制GC:

    while (true) {
      const thing = new WasmThing(() => {
        console.log("Hello there!");
      });
      thing.free();
      global.gc(); // 强制触发GC
    }
    

    若问题消失,说明是V8 GC延迟导致的资源堆积,需调整代码避免短时间创建大量跨语言对象。

  5. 使用wasm-bindgen官方Closure类型
    wasm_bindgen::closure::Closure专门用于管理JS闭包与Rust函数的绑定,自动处理生命周期与GC:

    use wasm_bindgen::prelude::*;
    use wasm_bindgen::closure::Closure;
    
    #[wasm_bindgen]
    pub struct WasmThing {
        func: Closure<dyn FnMut()>,
    }
    
    #[wasm_bindgen]
    impl WasmThing {
        #[wasm_bindgen(constructor)]
        pub fn new(js_func: js_sys::Function) -> WasmThing {
            let closure = Closure::new(move || {
                js_func.call0(&JsValue::UNDEFINED).unwrap();
            });
            WasmThing { func: closure }
        }
    }
    

    Closure会自动维护JS侧引用计数,确保资源被正确回收。

验证手段

  • 编译时启用调试模式:wasm-pack build --debug,结合浏览器DevTools的WASM调试功能定位内存越界具体位置。
  • 使用dhat crate分析Rust堆内存,生成堆快照确认是否存在内存泄漏:
    [dependencies]
    dhat = "0.3"
    

内容的提问来源于stack exchange,提问作者Ari Lotter

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.22 19:07:35