ข้อผิดพลาดที่ไม่สามารถแก้ไขได้ด้วย panic! (Unrecoverable Errors with panic!)
บางครั้งสิ่งแย่ ๆ ก็เกิดขึ้นในโค้ดของคุณ และคุณไม่สามารถทำอะไรกับมันได้เลย ในกรณีเหล่านี้ ภาษา Rust มีมาโคร panic! ไว้รองรับ ในทางปฏิบัติ มีสองวิธีที่ทำให้เกิดการ panic ได้แก่: การกระทำคำสั่งที่ส่งผลให้โค้ดของเราเกิด panic (เช่น การเข้าถึงสมาชิกอาร์เรย์นอกขอบเขต) หรือการเรียกใช้มาโคร panic! โดยตรง ทั้งสองวิธีนี้ต่างก็ก่อให้เกิดการ panic ในโปรแกรมของเรา ตามค่าเริ่มต้น กระบวนการ panic เหล่านี้จะแสดงข้อความแจ้งความล้มเหลว (failure message), ทำการย้อนกลับโครงสร้างสแตก (unwind), กวาดล้างข้อมูลในสแตก และปิดการทำงานของโปรแกรมลง นอกจากนี้ คุณยังสามารถกำหนดค่าตัวแปรสภาพแวดล้อม (environment variable) เพื่อสั่งให้ Rust แสดงลำดับสแตกรันไทม์ (call stack) เมื่อเกิดการ panic เพื่อช่วยให้ค้นหาต้นตอของการ panic ได้ง่ายขึ้น
การย้อนกลับสแตกหรือการยกเลิกโปรแกรมทันทีเมื่อเกิด Panic (Unwinding the Stack or Aborting in Response to a Panic)
ตามค่าเริ่มต้น เมื่อเกิด panic โปรแกรมจะเริ่มต้นกระบวนการ ย้อนกลับสแตก (unwinding) ซึ่งหมายความว่า Rust จะไล่ย้อนลำดับสแตกขึ้นไปและทำการกวาดล้างข้อมูลต่าง ๆ ของฟังก์ชันแต่ละตัวที่พบเจอ ทว่าการย้อนรอยกลับไปและเคลียร์ข้อมูลนั้นเป็นงานที่ต้องใช้พลังงานประมวลผลสูง ด้วยเหตุนี้ Rust จึงเปิดโอกาสให้คุณมีทางเลือกอื่น นั่นคือการ ยกเลิกการทำงานทันที (aborting) ซึ่งจะทำให้โปรแกรมยุติลงโดยไม่มีการเคลียร์ข้อมูลใด ๆ เลย
หน่วยความจำที่โปรแกรมเคยใช้งานอยู่จะถูกเคลียร์สิทธิ์โดยระบบปฏิบัติการ (OS) แทน หากโปรเจกต์ของคุณจำเป็นต้องบีบอัดขนาดไฟล์ไบนารีที่ได้ให้มีขนาดเล็กที่สุดเท่าที่จะทำได้ คุณสามารถสลับพฤติกรรมจาก unwinding มาเป็น aborting ทันทีเมื่อเกิด panic ได้โดยการเพิ่มบรรทัด
panic = 'abort'ลงในส่วน[profile]ที่ต้องการในไฟล์ Cargo.toml ตัวอย่างเช่น หากคุณต้องการยกเลิกการทำงานทันทีเมื่อเกิด panic ในโหมด release ให้เพิ่มส่วนนี้เข้าไป:[profile.release] panic = 'abort'
เรามาลองเรียกใช้งาน panic! ในโปรแกรมง่าย ๆ กันครับ:
fn main() {
panic!("crash and burn");
}
เมื่อคุณรันโปรแกรมนี้ คุณจะเจอกับผลลัพธ์หน้าตาประมาณนี้:
$ cargo run
Compiling panic v0.1.0 (file:///projects/panic)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.25s
Running `target/debug/panic`
thread 'main' (6018279) panicked at src/main.rs:2:5:
crash and burn
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
การเรียกใช้คำสั่ง panic! ส่งผลให้เกิดข้อความแสดงข้อผิดพลาดที่ปรากฏในสองบรรทัดสุดท้าย บรรทัดแรกจะแสดงข้อความ panic ของเรา และตำแหน่งในซอร์สโค้ดที่เกิดการ panic ขึ้น: ค่า src/main.rs:2:5 บ่งบอกว่ามันคือบรรทัดที่สอง ตัวอักษรตัวที่ห้า ในไฟล์ src/main.rs ของเรานั่นเอง
ในกรณีนี้ บรรทัดที่ระบุถือเป็นส่วนหนึ่งในโค้ดของเราเอง และหากเราย้อนไปดูที่บรรทัดนั้น ก็จะพบคำสั่งเรียกใช้มาโคร panic! ชัดเจน แต่ในบางกรณี คำสั่ง panic! อาจเกิดขึ้นในโค้ดของคนอื่นที่โค้ดเราเรียกใช้อีกต่อหนึ่ง ซึ่งส่งผลให้ชื่อไฟล์และเลขบรรทัดที่รายงานในข้อความแสดงข้อผิดพลาดกลายเป็นโค้ดของคนอื่นที่มีการใช้มาโคร panic! ไม่ใช่บรรทัดโค้ดของเราที่ส่งผลให้เกิดการเรียกใช้ panic! ในท้ายที่สุด
เราสามารถใช้ประโยชน์จากการย้อนรอยการเรียกฟังก์ชัน (backtrace) ที่ส่งผลให้เกิดการ panic! เพื่อสืบหาบรรทัดในโค้ดของเราที่เป็นตัวก่อปัญหา เพื่อทำความเข้าใจวิธีการใช้งาน panic! backtrace ลองมาดูตัวอย่างถัดไปเพื่อศึกษาว่าจะเกิดอะไรขึ้นหากการสั่ง panic! ถูกเรียกใช้งานจากโค้ดไลบรารีอื่นเนื่องจากบั๊กในโปรแกรมของเรา แทนที่จะเป็นการเรียกมาโครตรง ๆ จากโค้ดเราเอง ตัวอย่างที่ 9-1 มีโค้ดที่พยายามเข้าถึงค่าดัชนีในเวกเตอร์ที่เกินช่วงดัชนีที่ใช้งานได้จริง
fn main() {
let v = vec![1, 2, 3];
v[99];
}
ในจุดนี้ เรากำลังพยายามเข้าถึงสมาชิกตัวที่ 100 ของเวกเตอร์ (ซึ่งอยู่ที่ดัชนี 99 เนื่องจากดัชนีเริ่มต้นด้วยศูนย์) แต่ทว่าตัวเวกเตอร์มีสมาชิกอยู่เพียงแค่สามตัวเท่านั้น ในสถานการณ์นี้ Rust จะเกิดการ panic ขึ้น การใช้งาน [] คาดหวังว่าจะคืนค่าสมาชิกตัวหนึ่งกลับมา แต่หากคุณส่งค่าดัชนีที่ไม่ถูกต้องไป ก็จะไม่มีสมาชิกตัวใดที่ Rust สามารถส่งกลับคืนมาได้อย่างถูกต้องสมบูรณ์
ในภาษา C การพยายามอ่านข้อมูลที่อยู่นอกขอบเขตของโครงสร้างข้อมูลจะถือเป็นพฤติกรรมที่ไม่ได้กำหนดไว้ (undefined behavior) คุณอาจจะได้รับค่าใด ๆ ก็ตามที่อยู่ในตำแหน่งหน่วยความจำซึ่งตรงกับตำแหน่งของสมาชิกนั้นในโครงสร้างข้อมูล ถึงแม้ว่าหน่วยความจำจุดนั้นจะไม่ได้เป็นของโครงสร้างข้อมูลดังกล่าวก็ตาม ปัญหานี้เรียกว่า การอ่านข้อมูลล้นบัฟเฟอร์ (buffer overread) และสามารถนำไปสู่ช่องโหว่ด้านความปลอดภัยได้ หากผู้ไม่ประสงค์ดีสามารถควบคุมค่าดัชนีในลักษณะที่ทำให้อ่านข้อมูลที่พวกเขาไม่มีสิทธิ์เข้าถึงซึ่งเก็บอยู่ต่อจากโครงสร้างข้อมูลนั้นได้
เพื่อปกป้องโปรแกรมของคุณจากช่องโหว่ประเภทนี้ หากคุณพยายามอ่านข้อมูลในตำแหน่งดัชนีที่ไม่มีอยู่จริง Rust จะหยุดทำงานและปฏิเสธที่จะประมวลผลคำสั่งถัดไปทันที ลองมาทดสอบกันดูครับ:
$ cargo run
Compiling panic v0.1.0 (file:///projects/panic)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.27s
Running `target/debug/panic`
thread 'main' (6017887) panicked at src/main.rs:4:6:
index out of bounds: the len is 3 but the index is 99
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
ข้อผิดพลาดนี้ชี้ไปที่บรรทัดที่ 4 ในไฟล์ main.rs ของเรา ซึ่งเป็นจุดที่เราพยายามเข้าถึงดัชนี 99 ของเวกเตอร์ในตัวแปร v
บรรทัด note: แจ้งให้เราทราบว่าเราสามารถกำหนดค่าตัวแปรสภาพแวดล้อม RUST_BACKTRACE เพื่อรับรายงาน backtrace เพื่อดูลำดับขั้นตอนที่ส่งผลให้เกิดข้อผิดพลาดขึ้นอย่างละเอียดได้ โดย backtrace คือรายการของฟังก์ชันทั้งหมดที่มีการเรียกใช้งานเพื่อดำเนินขั้นตอนมาจนถึงจุดเกิดเหตุนี้ การรายงาน backtrace ในภาษา Rust ทำงานคล้ายกับภาษาอื่น ๆ คือ: กุญแจสำคัญในการอ่าน backtrace คือการไล่อ่านจากบนสุดลงมาเรื่อย ๆ จนกระทั่งเจอชื่อไฟล์ที่คุณเขียนขึ้นมา นั่นคือจุดที่ข้อผิดพลาดนั้นมีจุดเริ่มต้นขึ้น บรรทัดที่อยู่เหนือจุดนั้นคือโค้ดภายนอกที่โค้ดของคุณทำการเรียกใช้งาน ส่วนบรรทัดที่อยู่ต่ำกว่าคือโค้ดที่เป็นตัวเรียกใช้งานโค้ดของคุณอีกที บรรทัดก่อนหน้าและหลังจากนั้นอาจรวมถึงโค้ดแกนหลักของ Rust (core Rust), โค้ดไลบรารีมาตรฐาน (standard library) หรือเครตส์ (crates) ต่าง ๆ ที่คุณใช้งานอยู่ ลองรับรายงาน backtrace โดยกำหนดค่าตัวแปรสภาพแวดล้อม RUST_BACKTRACE เป็นค่าใดก็ได้ยกเว้น 0 ตัวอย่างที่ 9-2 แสดงผลลัพธ์ที่คล้ายกับที่คุณจะพบเจอ
$ RUST_BACKTRACE=1 cargo run
thread 'main' panicked at src/main.rs:4:6:
index out of bounds: the len is 3 but the index is 99
stack backtrace:
0: rust_begin_unwind
at /rustc/4d91de4e48198da2e33413efdcd9cd2cc0c46688/library/std/src/panicking.rs:692:5
1: core::panicking::panic_fmt
at /rustc/4d91de4e48198da2e33413efdcd9cd2cc0c46688/library/core/src/panicking.rs:75:14
2: core::panicking::panic_bounds_check
at /rustc/4d91de4e48198da2e33413efdcd9cd2cc0c46688/library/core/src/panicking.rs:273:5
3: <usize as core::slice::index::SliceIndex<[T]>>::index
at file:///home/.rustup/toolchains/1.85/lib/rustlib/src/rust/library/core/src/slice/index.rs:274:10
4: core::slice::index::<impl core::ops::index::Index<I> for [T]>::index
at file:///home/.rustup/toolchains/1.85/lib/rustlib/src/rust/library/core/src/slice/index.rs:16:9
5: <alloc::vec::Vec<T,A> as core::ops::index::Index<I>>::index
at file:///home/.rustup/toolchains/1.85/lib/rustlib/src/rust/library/alloc/src/vec/mod.rs:3361:9
6: panic::main
at ./src/main.rs:4:6
7: core::ops::function::FnOnce::call_once
at file:///home/.rustup/toolchains/1.85/lib/rustlib/src/rust/library/core/src/ops/function.rs:250:5
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
นั่นเป็นผลลัพธ์ที่ยาวมาก! ผลลัพธ์จริงที่คุณเจออาจแตกต่างออกไปเล็กน้อยขึ้นอยู่กับระบบปฏิบัติการและเวอร์ชันภาษา Rust ของคุณ เพื่อที่จะรับรายงาน backtrace ที่มีรายละเอียดข้อมูลเหล่านี้ได้ คุณจำเป็นต้องเปิดใช้งานการเก็บสัญลักษณ์ดีบั๊ก (debug symbols) ซึ่งสัญลักษณ์ดีบั๊กนี้จะถูกเปิดใช้งานไว้เป็นปกติอยู่แล้วเมื่อเราใช้คำสั่ง cargo build หรือ cargo run โดยไม่มีแฟล็ก --release ดังเช่นที่เราสั่งไปที่นี่
ในผลลัพธ์จากตัวอย่างที่ 9-2 บรรทัดที่ 6 ของ backtrace ชี้ตรงไปยังบรรทัดในโปรเจกต์ของเราที่เป็นต้นเหตุของปัญหา: บรรทัดที่ 4 ของไฟล์ src/main.rs หากเราไม่ต้องการให้โปรแกรมของเราเกิด panic เราควรเริ่มต้นกระบวนการหาสาเหตุที่ตำแหน่งข้อมูลบรรทัดแรกที่ระบุชื่อไฟล์ที่เราเป็นคนเขียนขึ้นเอง ในตัวอย่างที่ 9-1 ที่เราตั้งใจเขียนโค้ดเพื่อให้เกิด panic ขึ้นมา แนวทางแก้ไขไม่ให้เกิด panic คือการระมัดระวังไม่เรียกขอข้อมูลของสมาชิกนอกช่วงดัชนีของเวกเตอร์ เมื่อโค้ดของคุณเกิด panic ขึ้นอีกในอนาคต คุณจำเป็นต้องวิเคราะห์ว่าโค้ดนั้นกำลังกระทำคำสั่งใด ด้วยค่าข้อมูลใดที่ส่งผลให้เกิดการ panic และโค้ดควรทำสิ่งใดแทนที่คำสั่งเดิม
เราจะย้อนกลับมาพูดถึงเรื่อง panic! และการพิจารณาว่าเมื่อใดที่เราควรหรือควรหลีกเลี่ยงการใช้ panic! เพื่อรับมือกับข้อผิดพลาดต่าง ๆ ในหัวข้อ “ควรจะ panic! หรือไม่ควรดี? (To panic! or Not to panic!” ในช่วงท้ายของบทนี้ ต่อไปเราจะมาดูกันว่ากระบวนการกู้คืนการทำงานจากข้อผิดพลาดโดยใช้ Result ทำอย่างไรได้บ้าง