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. สำหรับคำอธิบายที่ถูกต้อง)
มีความเป็นไปได้ไม่กี่อย่างที่นี่
- ใช้ช่วงที่ถูกต้อง เช่น
0..2 - ใช้ช่วงรวม
0..=1 อย่าใช้โครงสร้างที่ไม่ปลอดภัย แต่ให้ใช้
let mut channel_data: [Vec<f32>; 2] = Default::default()
นี่จะเป็นการเริ่มต้น Vec ทั้งสองอย่างถูกต้อง
สำหรับภาพรวมที่สมบูรณ์ยิ่งขึ้นเกี่ยวกับวิธีการเริ่มต้นอาเรย์ ให้ดูที่ วิธีการที่ถูกต้องในการเริ่มต้นอาเรย์ที่มีความยาวคงที่คืออะไร?
หมายเหตุเพิ่มเติม: หลีกเลี่ยงการใช้ unsafe โดยเฉพาะหากคุณยังใหม่กับ Rust
คำตอบ #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] = /*...*/;
}
มีสองประเด็น:
- การใช้
std::mem::uninitializedถูกยกเลิกการใช้งานเนื่องจากเหตุผลด้านความปลอดภัย; เป็นความคิดที่ไม่ดีมากที่จะใช้งาน มันถูกแทนที่ด้วยMaybeUninitialized - การกำหนดค่าให้หน่วยความจำที่ยังไม่ได้ถูกกำหนดค่าเป็นพฤติกรรมที่ไม่ถูกกำหนด
ในภาษา 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>):
- คุณสามารถเข้าถึงองค์ประกอบได้โดยตรง ไม่จำเป็นต้องคำนวณ
- คุณยังมีดัชนีขององค์ประกอบโดยปราศจากข้อผิดพลาดบวกหรือลบหนึ่ง
