Rust WebAssembly 自定义元素内存释放错误

3

我第一次使用Rust生成WASM时出现了以下错误,我不知道该如何进行调试。

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)

上下文:最新版本的Chrome浏览器,Rust wasm-bindgen代码被调用并从TypeScript自定义元素操作在shadow DOM中的canvas上。渲染到画布的数据来自HTML5 AudioBuffer。所有的Rust变量都是限定作用域的。
如果在文档中只有一个实例,Web组件可以完美地工作。但是,如果有多个实例,就会像上面那样转储堆栈跟踪信息。代码运行没有其他问题。
我知道Chrome浏览器中存在未解决的内存错误 - 这是这类错误的表现吗?或者有经验的Rust/wasm开发人员能否告诉我这是否不寻常?
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"

这段 Rust 代码很简短,只是将音频缓冲区的两个通道呈现到提供的 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();
    }
  // ...

我尝试注释掉功能,如果代码不涉及画布却做了上述操作,就会出现错误。作出以下更改会导致一个简单的“wam内存不足”错误。音频文件大小为1,200 k。

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

编辑:对于上面正确的代码,后面的内存不足错误真的让我很困扰,但实际上这是一个Chrome bug


1
希望你能明白一个 [mcve] 为什么至关重要 ;) 我很高兴我能够解决你的问题。我必须承认,我们(Rust 标签 "成员")有时非常热衷于 MCVE,但这确实有很大帮助! - hellow
谢谢你帮我解决了这个荒谬情况下的最小示例问题!现在我只遇到了一个内存溢出错误,但那是另一个问题。 - Lee Goddard
事实上,“内存不足”错误是由于我昨天看到但忘记的Chrome错误引起的:https://dev59.com/e7Lma4cB1Zd3GeqPZVm8 - Lee Goddard
2个回答

6
你的问题在于你创建了一块未初始化的内存,并且没有正确地进行初始化:
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();
}

Range(也称为a..b)在Rust中是独占的。这意味着您的循环不会像您想象的那样迭代两次,而仅会迭代一次,并且您将有一个未初始化的Vec<f32>然后在删除它时可能会引发错误。(请参见Matthieu M. 的答案以获得适当的解释)

这里有几种可能性。

  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.

了解如何初始化数组的更完整概述,请参见什么是初始化固定长度数组的正确方法?

顺便提一下:避免使用unsafe,特别是如果你是 Rust 的新手。


1
我认为推荐一个适当的迭代方式会更好。不要使用范围,而是使用迭代+enumerate(),这样可以保证遍历所有内容。 - Matthieu M.
@MatthieuM。可以添加第四个选项,随意。 - hellow
实际上,诊断中存在一个错误:问题不在于留下一个元素未初始化,而是尝试分配给第一个元素将导致未初始化内存的丢失。我决定发表自己的答案,而不是大量编辑你的答案。 - Matthieu M.

5
这里有两个问题:
  1. 您创建了一个未初始化的内存块,并将其视为已初始化。
  2. 您的迭代是错误的,0..1 迭代 [0](它是排除在外的)。

让我们逐个检查它们。


不要使用 unsafe

一般来说,您应该尽量避免使用 unsafe。很少有理由使用它,并且有很多使用不当的方式(比如这里)。

问题所在。

在这种特殊情况下:

let mut channel_data: [Vec<f32>; 2] = unsafe { std::mem::uninitialized() };
for channel_number in /*...*/ {
    channel_data[channel_number] = /*...*/;
}

有两个问题:
1. 使用`std::mem::uninitialized`已被弃用,因为有安全隐患;使用它是一个非常糟糕的想法。它的替代品是`MaybeUninitialized`。
2. 给未初始化的内存赋值是未定义行为。
在Rust中没有赋值运算符,为了执行赋值操作,语言将:
1. 丢弃先前的实例。 2. 覆盖现在未使用的内存。
释放认为自己是`Vec`的原始内存是未定义行为;在这种情况下,可能会读取并释放一些随机指针值。这可能会导致崩溃,也可能会释放不相关的指针,从而导致后续崩溃或内存损坏,这是非常危险的。
解决方案:
1. 完全可以安全地执行数组的初始化。 2. 完全可以直接初始化数组。 3. 如果您坚持进行两步初始化,则不执行默认初始化几乎没有性能优势,因为`Vec`的`Default`实现不会分配内存。
简而言之:
auto create_channel = |channel_number: u32| {
    audio_buffer
        .get_channel_data(channel_number)
        .unwrap()
};

let mut channel_data = [create_channel(0), create_channel(1)];

是简单、安全且高效的。


优先选择迭代器而不是索引。

如果您坚持使用两步初始化,则使用迭代器而不是索引来避免偏移错误。

在您的情况下:

let mut channel_data = [vec!(), vec!()];
for (channel_number, channel) = channel_data.iter_mut().enumerate() {
    *channel = audio_buffer
        .get_channel_data(channel_number as u32)
        .unwrap();
}

Iterator有很多实用的函数,特别是在这种情况下,enumerate将由iter_mut()返回的项(一个&mut Vec<f32>)封装到元组(usize, &mut Vec<32>)中:

  • 您可以直接访问元素,无需计算。
  • 您还可以获得元素的索引,避免了偏移错误。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接