Search code examples
typescriptrustshadow-domwasm-bindgen

Rust WebAssembly Custom Elements Memory Deallocation Error


My first Rust-produced WASM is producing the following error, I have no idea how to go about it debugging.

wasm-000650c2-23:340 Uncaught RuntimeError: memory access out of bounds
    at dlmalloc::dlmalloc::Dlmalloc::free::h36961b6fbcc40c05 (wasm-function[23]:670)
    at __rdl_dealloc (wasm-function[367]:8)
    at __rust_dealloc (wasm-function[360]:7)
    at alloc::alloc::dealloc::h90df92e1f727e726 (wasm-function[146]:100)
    at <alloc::alloc::Global as core::alloc::Alloc>::dealloc::h7f22ab187c7f5835 (wasm-function[194]:84)
    at <alloc::raw_vec::RawVec<T, A>>::dealloc_buffer::hdce29184552be976 (wasm-function[82]:231)
    at <alloc::raw_vec::RawVec<T, A> as core::ops::drop::Drop>::drop::h3910dccc175e44e6 (wasm-function[269]:38)
    at core::ptr::real_drop_in_place::hd26be2408c00ce9d (wasm-function[267]:38)
    at core::ptr::real_drop_in_place::h6acb013dbd13c114 (wasm-function[241]:50)
    at core::ptr::real_drop_in_place::hb270ba635548ab74 (wasm-function[69]:192)

The context: latest Chrome, Rust wasm-bindgen code called from a TypeScript custom element, operating upon a canvas in the shadow DOM. Data rendered to the canvas comes from an HTML5 AudioBuffer. All rust variables are locally scoped.

The web component works perfectly if only one instance appears in the document, but if I further instances, a stack trace is dumped as above. The code runs without any other issue.

I know there are outstanding memory bugs in Chrome -- is this what they look like, or can an experienced rust/wasm developer tell me if this is unusual?

js-sys = "0.3.19"
wasm-bindgen = "0.2.42"
wee_alloc = { version = "0.4.2", optional = true }
[dependencies.web-sys]
version = "0.3.4"

The rust code is small, and just renders two channels of an AudioBuffer to a supplied HTMLCanvasElement:

#[wasm_bindgen]
pub fn render(
    canvas: web_sys::HtmlCanvasElement,
    audio_buffer: &web_sys::AudioBuffer,
    stroke_style: &JsValue,
    line_width: f64,
    step_size: usize,
) { 
  // ...
    let mut channel_data: [Vec<f32>; 2] = unsafe { std::mem::uninitialized() }; // !
    for channel_number in 0..1 {
        channel_data[channel_number] = audio_buffer
            .get_channel_data(channel_number as u32)
            .unwrap();
    }
  // ...

I've tried commenting out functionality, and if the code doesn't touch the canvas but does the above, I get the error. Making the below change results in a simple 'out of wam memory' error. The audio file is is 1,200 k.

    let channel_data: [Vec<f32>; 2] = [
        audio_buffer.get_channel_data(0).unwrap(),
        audio_buffer.get_channel_data(1).unwrap()
    ];

EDIT: The latter out of memory error, for the correct code above, really threw me, but it is actually a Chrome bug.


Solution

  • Your problem is that you create a chunk of uninitialized memory and don't initialize it properly:

    let mut channel_data: [Vec<f32>; 2] = unsafe { std::mem::uninitialized() };
    for channel_number in 0..1 {
        channel_data[channel_number] = audio_buffer
            .get_channel_data(channel_number as u32) // no need for `as u32` here btw
            .unwrap();
    }
    

    Ranges (a.k.a. a..b) are exclusive in Rust. This means that your loop does not iterate twice as you would suppose, but instead only once and you have one uninitialized Vec<f32> which then will panic while dropping it. (Please see Matthieu M.'s answer for a proper explanation)

    There are a few possibilities here.

    1. Use the proper range, e.g. 0..2
    2. Use an inclusive range 0..=1
    3. Don't use the unsafe construct, but instead
      let mut channel_data: [Vec<f32>; 2] = Default::default()
      
      This will properly initialize the two Vecs.

    For a more complete overview on how to initialize an array, see What is the proper way to initialize a fixed length array?

    As a sidenote: avoid using unsafe, especially if you're new to Rust.