Rust WebAssembly - ошибка высвобождения памяти пользовательских элементов - PullRequest
3 голосов
/ 18 апреля 2019

Мой первый 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 ]

6 голосов
/ 18 апреля 2019

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

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 s (a.k.a. a..b) являются эксклюзивными в Rust. Это означает, что ваш цикл не повторяется дважды, как вы предполагаете, но вместо этого только один раз, и у вас есть один неинициализированный Vec<f32> , который затем будет паниковать при его отбрасывании. (Пожалуйста, см. Matthieu M. ' s ответ для правильного объяснения)

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

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

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

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

5 голосов
/ 18 апреля 2019

Здесь есть два вопроса:

  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, является неопределенным поведением; в этом случае вероятным эффектом является то, что некоторое случайное значение указателя считывается и освобождается Это может привести к сбою, это может освободить несвязанный указатель, что приведет к последнему сбою или повреждению памяти, это BAD .

Решение.

Нет особых оснований для использования 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() (a &mut Vec<f32>), в кортеж (usize, &mut Vec<32>):

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