Rust WebAssembly - ошибка высвобождения памяти пользовательских элементов

Мой первый WASM, созданный в Rust, выдает следующую ошибку, я понятия не имею, как отладить ее.

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, код wasm-bindgen для Rust, вызываемый из пользовательского элемента TypeScript и работающий на холсте в теневом DOM. Данные, отображаемые на холст, поступают из HTML5 AudioBuffer. Все переменные ржавчины локально ограничены.

Веб-компонент работает отлично, если в документе появляется только один экземпляр, но если я продолжу его использовать, трассировка стека будет сброшена, как указано выше. Код работает без каких-либо других проблем.

Я знаю, что в 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"

Код ржавчины невелик и просто отображает два канала AudioBuffer для поставляемого 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();
    }
  // ...

Я попытался закомментировать функциональность, и если код не касается основы, но выполняет вышеуказанные действия, я получаю сообщение об ошибке. Внесение изменений, приведенных ниже, приводит к простой ошибке "Недостаточно памяти". Аудио файл - 1200 кБ.

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

РЕДАКТИРОВАТЬ: последний out of memory Ошибка, для правильного кода выше, действительно вызвала меня, но это на самом деле ошибка Chrome.

2 ответа

Решение

Ваша проблема в том, что вы создаете кусок неинициализированной памяти и не инициализируете его должным образом:

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> который тогда будет паниковать при падении. (Пожалуйста, смотрите ответ Матье М. для правильного объяснения)

Здесь есть несколько возможностей.

  1. Используйте правильный диапазон, например 0..2
  2. Используйте инклюзивный диапазон 0..=1
  3. Не используйте небезопасную конструкцию, но вместо этого
    let mut channel_data: [Vec<f32>; 2] = Default::default()
    
    Это правильно инициализирует два Vecs.

Для более полного обзора того, как инициализировать массив, см. Как правильно инициализировать массив фиксированной длины?

В качестве обозначения: избегать использования unsafeособенно если вы новичок в Rust.

Здесь есть две проблемы:

  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 нет оператора присваивания, для выполнения задания язык будет:

  • Оставьте предыдущий экземпляр.
  • Перезапишите теперь неиспользуемую память.

Отбрасывание сырой памяти, которая думает, что это Vec является неопределенным поведением; в этом случае вероятным эффектом является то, что некоторое случайное значение указателя считывается и освобождается. Это может привести к сбою, это может освободить несвязанный указатель, что приведет к последнему сбою или повреждению памяти, это ПЛОХО.

Решение.

Есть мало причин для использования unsafe Вот:

  • Вполне возможно безопасно выполнить инициализацию массива.
  • Вполне возможно напрямую инициализировать массив.
  • Если вы НЕ выполняете инициализацию по умолчанию, вы получаете небольшой выигрыш в производительности, если вы настаиваете на двухэтапной инициализации, так как Default реализация Vec не выделяет память.

Короче:

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>):

  • У вас есть прямой доступ к элементу, никаких вычислений не требуется.
  • У вас также есть индекс элемента, без отдельных ошибок.
Другие вопросы по тегам