ค้นหาเว็บไซต์

typescript - ข้อผิดพลาดของ Rust WebAssembly ในการปล่อยหน่วยความจำของ Custom Elements


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 เวอร์ชันล่าสุด โค้ด Rust wasm-bindgen ถูกเรียกจากองค์ประกอบที่กำหนดเองของ TypeScript ทำงานกับแคนวาสใน shadow DOM ข้อมูลที่แสดงบนแคนวาสมาจาก HTML5 AudioBuffer ตัวแปร Rust ทั้งหมดมีขอบเขตภายในท้องถิ่น

คอมโพเนนต์เว็บทำงานได้อย่างสมบูรณ์หากมีเพียงอินสแตนซ์เดียวในเอกสาร แต่หากมีอินสแตนซ์อื่น ๆ เพิ่มขึ้น จะมีการแสดงสแตกเทรซดังที่เห็นข้างต้น โค้ดทำงานโดยไม่มีปัญหาอื่นใด

ฉันรู้ว่ามีบั๊กหน่วยความจำที่ยังไม่ได้แก้ไขใน 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 มีขนาดเล็ก และเพียงทำการเรนเดอร์สองช่องของ 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();
    }
  // ...

ฉันได้ลองคอมเมนต์ฟังก์ชันออก และถ้ารหัสไม่แตะต้องแคนวาสแต่ทำสิ่งที่กล่าวข้างต้น ฉันจะได้รับข้อผิดพลาด การเปลี่ยนแปลงด้านล่างนี้ทำให้เกิดข้อผิดพลาดง่าย ๆ ว่า 'หน่วยความจำ WAM หมด' ไฟล์เสียงมีขนาด 1,200 กิโลไบต์

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

แก้ไข: ข้อผิดพลาด out of memory ที่เกิดขึ้นต่อมา สำหรับโค้ดที่ถูกต้องข้างต้น ทำให้ฉันตาลุกวาวจริง ๆ แต่จริง ๆ แล้วมันเป็นบั๊กของ Chrome

คำตอบ #1

ปัญหาของคุณคือคุณสร้างหน่วยความจำขนาดใหญ่ที่ยังไม่ได้ถูกเริ่มต้นและไม่ทำการเริ่มต้นอย่างถูกต้อง:

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) เป็นแบบ exclusive ใน Rust ซึ่งหมายความว่าลูปของคุณจะไม่วนซ้ำสองครั้งตามที่คุณคาดคิด แต่จะวนเพียงครั้งเดียว และคุณจะมี Vec<f32> ที่ยังไม่ได้กำหนดค่า ซึ่งจะทำให้เกิด panic ขณะปล่อยเมมโมรี (โปรดดูคำตอบของ Matthieu M. สำหรับคำอธิบายที่ถูกต้อง)

มีความเป็นไปได้ไม่กี่อย่างที่นี่

  1. ใช้ช่วงที่ถูกต้อง เช่น 0..2
  2. ใช้ช่วงรวม 0..=1
  3. อย่าใช้โครงสร้างที่ไม่ปลอดภัย แต่ให้ใช้

    let mut channel_data: [Vec<f32>; 2] = Default::default()
    
  4. นี่จะเป็นการเริ่มต้น Vec ทั้งสองอย่างถูกต้อง

สำหรับภาพรวมที่สมบูรณ์ยิ่งขึ้นเกี่ยวกับวิธีการเริ่มต้นอาเรย์ ให้ดูที่ วิธีการที่ถูกต้องในการเริ่มต้นอาเรย์ที่มีความยาวคงที่คืออะไร?

หมายเหตุเพิ่มเติม: หลีกเลี่ยงการใช้ unsafe โดยเฉพาะหากคุณยังใหม่กับ Rust

คำตอบ #2

มีประเด็นสองข้อที่นี่:

  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 จะไม่มีตัวดำเนินการกำหนดค่า เพื่อที่จะทำการกำหนดค่า ภาษา 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>):

  • คุณสามารถเข้าถึงองค์ประกอบได้โดยตรง ไม่จำเป็นต้องคำนวณ
  • คุณยังมีดัชนีขององค์ประกอบโดยปราศจากข้อผิดพลาดบวกหรือลบหนึ่ง

บทความที่เกี่ยวข้อง