Unsafe Rust
โค้ดทั้งหมดที่เราได้พูดถึงจนถึงตอนนี้ได้รับการบังคับใช้การรับประกันความปลอดภัยของหน่วยความจำ (memory safety guarantees) ของ Rust ในขณะคอมไพล์ อย่างไรก็ตาม Rust มีภาษาที่สองซ่อนอยู่ภายใน ซึ่งไม่ได้บังคับใช้การรับประกันความปลอดภัยของหน่วยความจำเหล่านี้: สิ่งนี้เรียกว่า unsafe Rust และทำงานเหมือนกับ Rust ปกติ แต่ให้ซูเปอร์เพาเวอร์ (superpowers) เพิ่มเติมแก่เรา
Unsafe Rust มีอยู่เนื่องจากโดยธรรมชาติแล้ว การวิเคราะห์แบบสแตติก (static analysis) มีความระมัดระวังเป็นพิเศษ (conservative) เมื่อคอมไพเลอร์พยายามพิจารณาว่าโค้ดปฏิบัติตามการรับประกันหรือไม่ การปฏิเสธโปรแกรมที่ถูกต้องบางโปรแกรม ย่อมดีกว่าการยอมรับโปรแกรมที่ไม่ถูกต้อง แม้ว่าโค้ด อาจจะ ปกติ แต่หากคอมไพเลอร์ Rust มีข้อมูลไม่เพียงพอที่จะมั่นใจ มันจะปฏิเสธโค้ดนั้น ในกรณีเหล่านี้ คุณสามารถใช้โค้ด unsafe เพื่อบอกคอมไพเลอร์ว่า “เชื่อฉันเถอะ ฉันรู้ว่ากำลังทำอะไรอยู่” อย่างไรก็ตาม ขอเตือนว่าคุณใช้ unsafe Rust ด้วยความเสี่ยงของคุณเอง: หากคุณใช้โค้ด unsafe ไม่ถูกต้อง ปัญหาอาจเกิดขึ้นเนื่องจากความไม่ปลอดภัยของหน่วยความจำ เช่น การเรียกดูค่าพอยน์เตอร์ที่เป็น null (null pointer dereferencing)
อีกเหตุผลหนึ่งที่ Rust มีคู่หูที่เป็น unsafe คือการที่ฮาร์ดแวร์คอมพิวเตอร์เบื้องหลังมีความไม่ปลอดภัยโดยธรรมชาติ หาก Rust ไม่ยอมให้คุณทำปฏิบัติการที่ไม่ปลอดภัย คุณก็จะไม่สามารถทำภารกิจบางอย่างได้ Rust จำเป็นต้องอนุญาตให้คุณทำการเขียนโปรแกรมระบบระดับล่าง (low-level systems programming) เช่น การปฏิสัมพันธ์กับระบบปฏิบัติการโดยตรง หรือแม้กระทั่งการเขียนระบบปฏิบัติการของคุณเอง การทำงานกับการเขียนโปรแกรมระบบระดับล่างเป็นหนึ่งในเป้าหมายของภาษานี้ เรามาสำรวจสิ่งที่เราทำได้ด้วย unsafe Rust และวิธีทำสิ่งเหล่านั้นกัน
การใช้ซูเปอร์เพาเวอร์ Unsafe
ในการสลับไปใช้ unsafe Rust ให้ใช้คีย์เวิร์ด unsafe แล้วเริ่มต้นบล็อกใหม่ที่บรรจุโค้ด unsafe คุณสามารถดำเนินการ 5 อย่างใน unsafe Rust ที่คุณไม่สามารถทำได้ใน safe Rust ซึ่งเราเรียกว่า ซูเปอร์เพาเวอร์ unsafe (unsafe superpowers) ซูเปอร์เพาเวอร์เหล่านั้นรวมถึงความสามารถในการ:
- การเรียกดูค่าพอยน์เตอร์ดิบ (Dereference a raw pointer)
- การเรียกใช้ฟังก์ชันหรือเมธอดที่เป็น unsafe
- การเข้าถึงหรือแก้ไขตัวแปรสแตติกที่เปลี่ยนค่าได้ (mutable static variable)
- การนำ trait ที่เป็น unsafe ไปใช้งาน (Implement an unsafe trait)
- การเข้าถึงฟิลด์ของ
unions
สิ่งสำคัญคือต้องเข้าใจว่า unsafe ไม่ได้ปิดตัวตรวจสอบการยืม (borrow checker) หรือยกเลิกการตรวจสอบความปลอดภัยอื่นๆ ของ Rust: หากคุณใช้การอ้างอิง (reference) ในโค้ด unsafe มันยังคงถูกตรวจสอบอยู่ คีย์เวิร์ด unsafe เพียงให้คุณเข้าถึงฟีเจอร์ 5 อย่างนี้ซึ่งจะไม่ถูกตรวจสอบความปลอดภัยของหน่วยความจำโดยคอมไพเลอร์ คุณยังคงได้รับความปลอดภัยในระดับหนึ่งภายในบล็อก unsafe
นอกจากนี้ unsafe ไม่ได้หมายความว่าโค้ดภายในบล็อกเป็นอันตรายโดยจำเป็น หรือจะประสบปัญหาความปลอดภัยของหน่วยความจำอย่างแน่นอน: เจตนาคือในฐานะโปรแกรมเมอร์ คุณจะเป็นผู้รับประกันว่าโค้ดภายในบล็อก unsafe จะเข้าถึงหน่วยความจำในทางที่ถูกต้อง
มนุษย์ย่อมมีความผิดพลาดและข้อผิดพลาดจะเกิดขึ้น แต่โดยการกำหนดให้ปฏิบัติการ unsafe ทั้ง 5 นี้ต้องอยู่ภายในบล็อกที่ใส่คำอธิบายประกอบด้วย unsafe คุณจะรู้ว่าข้อผิดพลาดใดๆ ที่เกี่ยวข้องกับความปลอดภัยของหน่วยความจำต้องอยู่ภายในบล็อก unsafe ควบคุมบล็อก unsafe ให้มีขนาดเล็กไว้; คุณจะขอบคุณตัวเองในภายหลังเมื่อคุณสืบสวนหาบั๊กทางหน่วยความจำ
เพื่อแยกโค้ด unsafe ให้ได้มากที่สุด เป็นการดีที่สุดที่จะห่อหุ้มโค้ดดังกล่าวไว้ภายในโครงสร้างนามธรรมที่ปลอดภัย (safe abstraction) และมอบ API ที่ปลอดภัย ซึ่งเราจะพูดถึงในภายหลังในบทนี้เมื่อเราตรวจสอบฟังก์ชันและเมธอดที่เป็น unsafe บางส่วนของไลบรารีมาตรฐานถูกนำมาใช้งานในฐานะโครงสร้างนามธรรมที่ปลอดภัยเหนือโค้ด unsafe ที่ผ่านการตรวจสอบแล้ว การห่อหุ้มโค้ด unsafe ไว้ในโครงสร้างนามธรรมที่ปลอดภัยช่วยป้องกันไม่ให้การใช้ unsafe รั่วไหลออกไปในทุกที่ที่คุณหรือผู้ใช้ของคุณอาจต้องการใช้ฟังก์ชันการทำงานที่นำไปใช้งานด้วยโค้ด unsafe เพราะการใช้โครงสร้างนามธรรมที่ปลอดภัยนั้นปลอดภัย
เรามาดูซูเปอร์เพาเวอร์ unsafe แต่ละอย่างทั้ง 5 ข้อตามลำดับ เราจะดูโครงสร้างนามธรรมที่มอบอินเทอร์เฟซที่ปลอดภัยให้กับโค้ด unsafe ด้วยเช่นกัน
การเรียกดูค่าพอยน์เตอร์ดิบ
ในบทที่ 4 ในส่วน “Dangling References” เราได้กล่าวไว้ว่าคอมไพเลอร์ทำให้แน่ใจว่าการอ้างอิงมีความถูกต้องเสมอ Unsafe Rust มีชนิดข้อมูลใหม่สองชนิดเรียกว่า พอยน์เตอร์ดิบ (raw pointers) ซึ่งคล้ายกับการอ้างอิง เช่นเดียวกับการอ้างอิง พอยน์เตอร์ดิบสามารถเปลี่ยนค่าไม่ได้หรือเปลี่ยนค่าได้ และเขียนเป็น *const T และ *mut T ตามลำดับ เครื่องหมายดอกจันไม่ใช่ตัวดำเนินการเรียกดูค่า (dereference operator); มันเป็นส่วนหนึ่งของชื่อชนิดข้อมูล ในบริบทของพอยน์เตอร์ดิบ คำว่า เปลี่ยนค่าไม่ได้ หมายความว่าพอยน์เตอร์ไม่สามารถถูกกำหนดค่าโดยตรงได้หลังจากถูก dereferenced แล้ว
ข้อแตกต่างจากการอ้างอิงและสมาร์ตพอยน์เตอร์ พอยน์เตอร์ดิบ:
- ได้รับอนุญาตให้ละเว้นกฎการยืม โดยมีได้ทั้งพอยน์เตอร์ที่เปลี่ยนค่าไม่ได้และเปลี่ยนค่าได้ หรือมีพอยน์เตอร์ที่เปลี่ยนค่าได้หลายตัวชี้ไปยังตำแหน่งเดียวกัน
- ไม่ได้รับการรับประกันว่าจะชี้ไปยังหน่วยความจำที่ถูกต้อง
- ได้รับอนุญาตให้เป็น null
- ไม่ได้นำการล้างข้อมูลอัตโนมัติมาใช้งาน
โดยการถอนตัวจากการให้ Rust บังคับใช้การรับประกันเหล่านี้ คุณสามารถละทิ้งความปลอดภัยที่ได้รับการรับประกันเพื่อแลกกับประสิทธิภาพที่สูงขึ้น หรือความสามารถในการเชื่อมต่อกับภาษาอื่นหรือฮาร์ดแวร์ที่การรับประกันของ Rust ไม่ครอบคลุม
โค้ดตัวอย่างที่ 20-1 แสดงวิธีสร้างพอยน์เตอร์ดิบแบบเปลี่ยนค่าไม่ได้และเปลี่ยนค่าได้
fn main() {
let mut num = 5;
let r1 = &raw const num;
let r2 = &raw mut num;
}
โปรดสังเกตว่าเราไม่ได้รวมคีย์เวิร์ด unsafe ไว้ในโค้ดนี้ เราสามารถสร้างพอยน์เตอร์ดิบในโค้ด safe ได้ เพียงแต่เราไม่สามารถ dereference พอยน์เตอร์ดิบนอกบล็อก unsafe ได้ ดังที่คุณจะได้เห็นในอีกสักครู่
เราได้สร้างพอยน์เตอร์ดิบขึ้นโดยใช้ตัวดำเนินการยืมดิบ (raw borrow operators): &raw const num สร้างพอยน์เตอร์ดิบที่เปลี่ยนค่าไม่ได้ *const i32 และ &raw mut num สร้างพอยน์เตอร์ดิบที่เปลี่ยนค่าได้ *mut i32 เนื่องจากเราสร้างพวกมันขึ้นโดยตรงจากตัวแปรท้องถิ่น เราจึงรู้ว่าพอยน์เตอร์ดิบเฉพาะเหล่านี้มีความถูกต้อง แต่เราไม่สามารถตั้งข้อสันนิษฐานแบบนั้นกับพอยน์เตอร์ดิบใดๆ ได้
เพื่อสาธิตสิ่งนี้ ถัดมาเราจะสร้างพอยน์เตอร์ดิบที่เราไม่สามารถแน่ใจในความถูกต้องของมันได้ โดยใช้คีย์เวิร์ด as เพื่อแปลงค่าแทนที่จะใช้ตัวดำเนินการยืมดิบ โค้ดตัวอย่างที่ 20-2 แสดงวิธีสร้างพอยน์เตอร์ดิบไปยังตำแหน่งตามใจชอบในหน่วยความจำ การพยายามใช้หน่วยความจำตามใจชอบคือพฤติกรรมที่ไม่ถูกกำหนดไว้ (undefined behavior): อาจมีข้อมูล ณ ที่อยู่นั้น หรืออาจจะไม่มีก็ได้ คอมไพเลอร์อาจปรับเปลี่ยนโค้ดเพื่อไม่ให้มีการเข้าถึงหน่วยความจำ หรือโปรแกรมอาจยุติการทำงานด้วยข้อผิดพลาด segmentation fault โดยปกติแล้ว ไม่มีเหตุผลที่ดีที่จะเขียนโค้ดเช่นนี้ โดยเฉพาะในกรณีที่คุณสามารถใช้ตัวดำเนินการยืมดิบแทนได้ แต่มันเป็นไปได้
fn main() {
let address = 0x012345usize;
let r = address as *const i32;
}
จำไว้ว่าเราสามารถสร้างพอยน์เตอร์ดิบในโค้ด safe ได้ แต่เราไม่สามารถ dereference พอยน์เตอร์ดิบและอ่านข้อมูลที่ถูกชี้ไปได้ ในโค้ดตัวอย่างที่ 20-3 เราใช้ตัวดำเนินการ dereference * บนพอยน์เตอร์ดิบซึ่งจำเป็นต้องใช้บล็อก unsafe
fn main() {
let mut num = 5;
let r1 = &raw const num;
let r2 = &raw mut num;
unsafe {
println!("r1 is: {}", *r1);
println!("r2 is: {}", *r2);
}
}
การสร้างพอยน์เตอร์ไม่ได้ก่อให้เกิดอันตรายใดๆ เฉพาะเมื่อเราพยายามเข้าถึงค่าที่มันชี้ไปเท่านั้นที่เราอาจจบลงด้วยการจัดการกับค่าที่ไม่ถูกต้อง
โปรดสังเกตในโค้ดตัวอย่างที่ 20-1 และ 20-3 ว่าเราได้สร้างพอยน์เตอร์ดิบ *const i32 และ *mut i32 ซึ่งทั้งคู่ชี้ไปยังตำแหน่งหน่วยความจำเดียวกันที่ num ถูกจัดเก็บอยู่ หากเราพยายามสร้างการอ้างอิงแบบเปลี่ยนค่าไม่ได้และเปลี่ยนค่าได้ไปยัง num แทน โค้ดจะคอมไพล์ไม่ผ่านเนื่องจากกฎความเป็นเจ้าของของ Rust ไม่อนุญาตให้มีการอ้างอิงแบบเปลี่ยนค่าได้ในเวลาเดียวกับการอ้างอิงแบบเปลี่ยนค่าไม่ได้ สำหรับพอยน์เตอร์ดิบ เราสามารถสร้างพอยน์เตอร์เปลี่ยนค่าได้และพอยน์เตอร์เปลี่ยนค่าไม่ได้ชี้ไปยังตำแหน่งเดียวกัน และเปลี่ยนแปลงข้อมูลผ่านพอยน์เตอร์เปลี่ยนค่าได้ ซึ่งอาจก่อให้เกิดภาวะแย่งชิงข้อมูล (data race) โปรดระมัดระวัง!
ด้วยอันตรายทั้งหมดนี้ เหตุใดคุณจึงต้องใช้พอยน์เตอร์ดิบ? กรณีการใช้งานหลักประการหนึ่งคือเมื่อเชื่อมต่อกับโค้ดภาษา C ดังที่คุณจะได้เห็นในส่วนถัดไป อีกกรณีหนึ่งคือเมื่อสร้างโครงสร้างนามธรรมที่ปลอดภัยซึ่งตัวตรวจสอบการยืมไม่เข้าใจ เราจะแนะนำฟังก์ชัน unsafe แล้วไปดูตัวอย่างของโครงสร้างนามธรรมที่ปลอดภัยที่ใช้โค้ด unsafe
การเรียกใช้ฟังก์ชันหรือเมธอด Unsafe
ปฏิบัติการประเภทที่สองที่คุณสามารถทำได้ในบล็อก unsafe คือการเรียกใช้ฟังก์ชัน unsafe ฟังก์ชันและเมธอด unsafe มีลักษณะเหมือนกับฟังก์ชันและเมธอดปกติทุกประการ แต่จะมี unsafe เพิ่มเติมล่วงหน้าก่อนส่วนที่เหลือของนิยาม คีย์เวิร์ด unsafe ในบริบทนี้ระบุว่าฟังก์ชันมีข้อกำหนดที่เราต้องปฏิบัติตามเมื่อเรียกใช้ฟังก์ชันนี้ เนื่องจาก Rust ไม่สามารถรับประกันได้ว่าเราได้ปฏิบัติตามข้อกำหนดเหล่านี้แล้ว โดยการเรียกใช้ฟังก์ชัน unsafe ภายในบล็อก unsafe เรากำลังบอกว่าเราได้อ่านเอกสารประกอบของฟังก์ชันนี้แล้ว และเรารับผิดชอบในการปฏิบัติตามข้อตกลงของฟังก์ชัน
นี่คือฟังก์ชัน unsafe ชื่อ dangerous ซึ่งไม่ได้ทำอะไรในบอร์ดี้ของมัน:
fn main() {
unsafe fn dangerous() {}
unsafe {
dangerous();
}
}
เราต้องเรียกใช้ฟังก์ชัน dangerous ภายในบล็อก unsafe ที่แยกต่างหาก หากเราพยายามเรียกใช้ dangerous โดยไม่มีบล็อก unsafe เราจะได้รับข้อผิดพลาด:
$ cargo run
Compiling unsafe-example v0.1.0 (file:///projects/unsafe-example)
error[E0133]: call to unsafe function `dangerous` is unsafe and requires unsafe block
--> src/main.rs:4:5
|
4 | dangerous();
| ^^^^^^^^^^^ call to unsafe function
|
= note: consult the function's documentation for information on how to avoid undefined behavior
For more information about this error, try `rustc --explain E0133`.
error: could not compile `unsafe-example` (bin "unsafe-example") due to 1 previous error
ด้วยบล็อก unsafe เรากำลังยืนยันกับ Rust ว่าเราได้อ่านเอกสารประกอบของฟังก์ชันแล้ว เข้าใจวิธีใช้งานอย่างถูกต้อง และได้ตรวจสอบแล้วว่าเราได้ปฏิบัติตามข้อตกลงของฟังก์ชันเรียบร้อยแล้ว
ในการดำเนินการปฏิบัติการ unsafe ในบอร์ดี้ของฟังก์ชัน unsafe คุณยังคงต้องใช้บล็อก unsafe เช่นเดียวกับภายในฟังก์ชันปกติ และคอมไพเลอร์จะเตือนหากคุณลืม สิ่งนี้ช่วยให้เราควบคุมบล็อก unsafe ให้มีขนาดเล็กที่สุดเท่าที่จะเป็นไปได้ เนื่องจากปฏิบัติการ unsafe อาจไม่จำเป็นต้องใช้ตลอดทั้งบอร์ดี้ฟังก์ชัน
การสร้างโครงสร้างนามธรรมที่ปลอดภัยเหนือโค้ด Unsafe
เพียงเพราะฟังก์ชันบรรจุโค้ด unsafe ไม่ได้หมายความว่าเราจำเป็นต้องทำเครื่องหมายทั้งฟังก์ชันเป็น unsafe ในความเป็นจริง การห่อหุ้มโค้ด unsafe ไว้ในฟังก์ชันที่ปลอดภัยเป็นโครงสร้างนามธรรมทั่วไป ตัวอย่างเช่น เรามาศึกษากระบวนการของฟังก์ชัน split_at_mut จากไลบรารีมาตรฐาน ซึ่งจำเป็นต้องใช้โค้ด unsafe เราจะสำรวจวิธีที่เราอาจนำมันไปใช้งาน เมธอดที่ปลอดภัยนี้ถูกนิยามไว้บน mutable slices: มันรับสไลซ์หนึ่งอันและทำให้มันกลายเป็นสองอันโดยการแบ่งสไลซ์ ณ ดัชนีที่ส่งมาเป็นอาร์กิวเมนต์ โค้ดตัวอย่างที่ 20-4 แสดงวิธีใช้ split_at_mut
fn main() {
let mut v = vec![1, 2, 3, 4, 5, 6];
let r = &mut v[..];
let (a, b) = r.split_at_mut(3);
assert_eq!(a, &mut [1, 2, 3]);
assert_eq!(b, &mut [4, 5, 6]);
}
เราไม่สามารถนำฟังก์ชันนี้ไปใช้งานโดยใช้เฉพาะ safe Rust ได้ ความพยายามอาจมีลักษณะเหมือนโค้ดตัวอย่างที่ 20-5 ซึ่งจะไม่คอมไพล์ เพื่อความเรียบง่าย เราจะนำ split_at_mut ไปใช้งานในฐานะฟังก์ชันแทนที่จะเป็นเมธอด และใช้เฉพาะสำหรับสไลซ์ของค่า i32 แทนที่จะเป็นชนิดข้อมูลเจเนอริก T
fn split_at_mut(values: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
let len = values.len();
assert!(mid <= len);
(&mut values[..mid], &mut values[mid..])
}
fn main() {
let mut vector = vec![1, 2, 3, 4, 5, 6];
let (left, right) = split_at_mut(&mut vector, 3);
}
ฟังก์ชันนี้รับความยาวทั้งหมดของสไลซ์ก่อน จากนั้น ยืนยันว่าดัชนีที่ส่งมาเป็นพารามิเตอร์อยู่ภายในสไลซ์โดยการตรวจสอบว่ามันน้อยกว่าหรือเท่ากับความยาว การยืนยันหมายความว่าหากเราส่งดัชนีที่มากกว่าความยาวในการแบ่งสไลซ์ ฟังก์ชันจะเกิด panic ก่อนที่จะพยายามใช้ดัชนะนั้น
จากนั้น เราคืนค่าสไลซ์แบบเปลี่ยนค่าได้สองอันใน tuple: อันหนึ่งจากจุดเริ่มต้นของสไลซ์ดั้งเดิมจนถึงดัชนี mid และอีกอันจาก mid จนถึงจุดสิ้นสุดของสไลซ์
เมื่อเราพยายามคอมไพล์โค้ดในโค้ดตัวอย่างที่ 20-5 เราจะได้รับข้อผิดพลาด:
$ cargo run
Compiling unsafe-example v0.1.0 (file:///projects/unsafe-example)
error[E0499]: cannot borrow `*values` as mutable more than once at a time
--> src/main.rs:6:31
|
1 | fn split_at_mut(values: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
| - let's call the lifetime of this reference `'1`
...
6 | (&mut values[..mid], &mut values[mid..])
| --------------------------^^^^^^--------
| | | |
| | | second mutable borrow occurs here
| | first mutable borrow occurs here
| returning this value requires that `*values` is borrowed for `'1`
|
= help: use `.split_at_mut(position)` to obtain two mutable non-overlapping sub-slices
For more information about this error, try `rustc --explain E0499`.
error: could not compile `unsafe-example` (bin "unsafe-example") due to 1 previous error
ตัวตรวจสอบการยืมของ Rust ไม่สามารถเข้าใจว่าเรากำลังยืมส่วนที่แตกต่างกันของสไลซ์; มันรู้เพียงว่าเรากำลังยืมจากสไลซ์เดียวกันสองครั้ง การยืมส่วนที่แตกต่างกันของสไลซ์เป็นสิ่งที่ไม่เป็นไรในระดับพื้นฐานเพราะสไลซ์ทั้งสองไม่ได้ทับซ้อนกัน แต่ Rust ไม่ฉลาดพอที่จะรู้สิ่งนี้ เมื่อเรารู้ว่าโค้ดนั้นโอเค แต่ Rust ไม่รู้ ก็ถึงเวลาที่ต้องเอื้อมไปหาโค้ด unsafe
โค้ดตัวอย่างที่ 20-6 แสดงวิธีใช้บล็อก unsafe, พอยน์เตอร์ดิบ, และการเรียกใช้ฟังก์ชัน unsafe บางส่วนเพื่อทำให้การนำ split_at_mut ไปใช้งานทำงานได้
use std::slice;
fn split_at_mut(values: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
let len = values.len();
let ptr = values.as_mut_ptr();
assert!(mid <= len);
unsafe {
(
slice::from_raw_parts_mut(ptr, mid),
slice::from_raw_parts_mut(ptr.add(mid), len - mid),
)
}
}
fn main() {
let mut vector = vec![1, 2, 3, 4, 5, 6];
let (left, right) = split_at_mut(&mut vector, 3);
}
ย้อนกลับไปจากส่วน “The Slice Type” ในบทที่ 4 สไลซ์คือพอยน์เตอร์ไปยังข้อมูลบางอย่างและความยาวของสไลซ์ เราใช้เมธอด len เพื่อรับความยาวของสไลซ์และเมธอด as_mut_ptr เพื่อเข้าถึงพอยน์เตอร์ดิบของสไลซ์ ในกรณีนี้ เนื่องจากเรามีสไลซ์เปลี่ยนค่าได้ไปยังค่า i32 เมธอด as_mut_ptr จะคืนค่าพอยน์เตอร์ดิบที่มีชนิดข้อมูล *mut i32 ซึ่งเราได้จัดเก็บไว้ในตัวแปร ptr
เราคงการยืนยันว่าดัชนี mid อยู่ภายในสไลซ์ไว้ จากนั้น เราไปถึงโค้ด unsafe: ฟังก์ชัน slice::from_raw_parts_mut รับพอยน์เตอร์ดิบและความยาว และสร้างสไลซ์ขึ้นมา เราใช้ฟังก์ชันนี้เพื่อสร้างสไลซ์ที่เริ่มต้นจาก ptr และมีความยาวเท่ากับ mid รายการ จากนั้น เราเรียกเมธอด add บน ptr ด้วย mid เป็นอาร์กิวเมนต์เพื่อรับพอยน์เตอร์ดิบที่เริ่มต้นที่ mid และเราสร้างสไลซ์โดยใช้พอยน์เตอร์นั้นและจำนวนรายการที่เหลือหลังจาก mid เป็นความยาว
ฟังก์ชัน slice::from_raw_parts_mut เป็น unsafe เนื่องจากรับพอยน์เตอร์ดิบและต้องวางใจว่าพอยน์เตอร์นี้มีความถูกต้อง เมธอด add บนพอยน์เตอร์ดิบก็นำไปใช้เป็น unsafe เช่นกันเนื่องจากต้องวางใจว่าตำแหน่งออฟเซ็ตเป็นพอยน์เตอร์ที่ถูกต้องด้วย ดังนั้น เราจึงต้องวางบล็อก unsafe ล้อมรอบการเรียกใช้ slice::from_raw_parts_mut และ add เพื่อให้เราเรียกใช้พวกมันได้ โดยการดูที่โค้ดและการเพิ่มการยืนยันว่า mid ต้องน้อยกว่าหรือเท่ากับ len เราสามารถบอกได้ว่าพอยน์เตอร์ดิบทั้งหมดที่ใช้ภายในบล็อก unsafe จะเป็นพอยน์เตอร์ที่ถูกต้องไปยังข้อมูลภายในสไลซ์ นี่เป็นการใช้ unsafe ที่ยอมรับได้และเหมาะสม
สังเกตว่าเราไม่จำเป็นต้องทำเครื่องหมายฟังก์ชัน split_at_mut ที่ได้เป็น unsafe และเราสามารถเรียกใช้ฟังก์ชันนี้จาก safe Rust ได้ เราได้สร้างโครงสร้างนามธรรมที่ปลอดภัยขึ้นปกคลุมโค้ด unsafe ด้วยการนำฟังก์ชันไปใช้งานที่ใช้โค้ด unsafe ในทางที่ปลอดภัย เนื่องจากมันสร้างเฉพาะพอยน์เตอร์ที่ถูกต้องจากข้อมูลที่ฟังก์ชันนี้มีสิทธิ์เข้าถึง
ในทางกลับกัน การใช้ slice::from_raw_parts_mut ในโค้ดตัวอย่างที่ 20-7 น่าจะทำให้โปรแกรมค้าง (crash) เมื่อใช้สไลซ์ โค้ดนี้รับตำแหน่งหน่วยความจำตามใจชอบและสร้างสไลซ์ที่มีความยาว 10,000 รายการ
fn main() {
use std::slice;
let address = 0x01234usize;
let r = address as *mut i32;
let values: &[i32] = unsafe { slice::from_raw_parts_mut(r, 10000) };
}
เราไม่ได้เป็นเจ้าของหน่วยความจำ ณ ตำแหน่งตามใจชอบนี้ และไม่มีหลักประกันว่าสไลซ์ที่โค้ดนี้สร้างขึ้นจะบรรจุค่า i32 ที่ถูกต้อง การพยายามใช้ values เสมือนว่าเป็นสไลซ์ที่ถูกต้องส่งผลให้เกิดพฤติกรรมที่ไม่ถูกกำหนดไว้ (undefined behavior)
การใช้ฟังก์ชัน extern เพื่อเรียกใช้โค้ดภายนอก
บางครั้งโค้ด Rust ของคุณอาจจำเป็นต้องมีปฏิสัมพันธ์กับโค้ดที่เขียนด้วยภาษาอื่น สำหรับสิ่งนี้ Rust มีคีย์เวิร์ด extern ซึ่งช่วยความสะดวกในการสร้างและใช้งาน Foreign Function Interface (FFI) ซึ่งเป็นวิธีสำหรับภาษาโปรแกรมในการนิยามฟังก์ชันและอนุญาตให้ภาษาโปรแกรมอื่น (ต่างประเทศ) สามารถเรียกใช้ฟังก์ชันเหล่านั้นได้
โค้ดตัวอย่างที่ 20-8 สาธิตวิธีตั้งค่าการบูรณาการกับฟังก์ชัน abs จากไลบรารีมาตรฐาน C ฟังก์ชันที่ประกาศไว้ภายในบล็อก extern โดยทั่วไปไม่ปลอดภัยที่จะเรียกใช้จากโค้ด Rust ดังนั้นบล็อก extern จึงต้องถูกทำเครื่องหมายเป็น unsafe ด้วย เหตุผลคือภาษาอื่นไม่ได้บังคับใช้กฎและการรับประกันของ Rust และ Rust ไม่สามารถตรวจสอบพวกมันได้ ดังนั้นความรับผิดชอบจึงตกอยู่กับโปรแกรมเมอร์ในการรับประกันความปลอดภัย
unsafe extern "C" {
fn abs(input: i32) -> i32;
}
fn main() {
unsafe {
println!("Absolute value of -3 according to C: {}", abs(-3));
}
}
ภายในบล็อก unsafe extern "C" เราจะรายการชื่อและลายเซ็นของฟังก์ชันภายนอกจากภาษาอื่นที่เราต้องการเรียกใช้ ส่วน "C" จะนิยามว่าฟังก์ชันภายนอกใช้ application binary interface (ABI) ใด: โดย ABI จะนิยามวิธีเรียกใช้ฟังก์ชันในระดับภาษาแอสเซมบลี "C" ABI เป็นภาษาที่พบบ่อยที่สุดและปฏิบัติตาม ABI ของภาษาโปรแกรม C ข้อมูลเกี่ยวกับ ABI ทั้งหมดที่ Rust รองรับมีอยู่ใน the Rust Reference
ทุกรายการที่ประกาศภายในบล็อก unsafe extern เป็น unsafe โดยนัย อย่างไรก็ตาม ฟังก์ชัน FFI บางอย่าง ก็ปลอดภัย ที่จะเรียกใช้ ตัวอย่างเช่น ฟังก์ชัน abs จากไลบรารีมาตรฐานของ C ไม่มีการพิจารณาเรื่องความปลอดภัยของหน่วยความจำใดๆ และเรารู้ว่าสามารถเรียกใช้ด้วย i32 ใดก็ได้ ในกรณีแบบนี้ เราสามารถใช้คีย์เวิร์ด safe เพื่อบอกว่าฟังก์ชันเฉพาะนี้ปลอดภัยที่จะเรียกใช้ แม้ว่าจะอยู่ในบล็อก unsafe extern ก็ตาม เมื่อเราทำการเปลี่ยนแปลงนั้น การเรียกใช้ก็ไม่จำเป็นต้องใช้บล็อก unsafe อีกต่อไป ดังแสดงในโค้ดตัวอย่างที่ 20-9
unsafe extern "C" {
safe fn abs(input: i32) -> i32;
}
fn main() {
println!("Absolute value of -3 according to C: {}", abs(-3));
}
การทำเครื่องหมายฟังก์ชันเป็น safe ไม่ได้ทำให้มันปลอดภัยโดยเนื้อแท้! แต่เป็นเหมือนคำสัญญาที่คุณทำไว้กับ Rust ว่ามันปลอดภัย มันยังคงเป็นความรับผิดชอบของคุณในการทำให้แน่ใจว่าคำสัญญานั้นได้รับการรักษาไว้!
การเรียกใช้ฟังก์ชัน Rust จากภาษาอื่น
เรายังสามารถใช้ extern เพื่อสร้างอินเทอร์เฟซที่อนุญาตให้ภาษาอื่นเรียกใช้ฟังก์ชัน Rust ได้ด้วย แทนที่จะสร้างบล็อก extern ทั้งบล็อก เราเพิ่มคีย์เวิร์ด extern และระบุ ABI ที่จะใช้ล่วงหน้าก่อนคีย์เวิร์ด fn สำหรับฟังก์ชันที่เกี่ยวข้อง นอกจากนี้ เราจำเป็นต้องเพิ่มแอตทริบิวต์ #[unsafe(no_mangle)] เพื่อบอกคอมไพเลอร์ Rust ไม่ให้แต่งเติมชื่อ (mangle) ของฟังก์ชันนี้ การแต่งเติมชื่อ (Mangling) คือการที่คอมไพเลอร์เปลี่ยนชื่อที่เรามอบให้กับฟังก์ชันไปเป็นชื่ออื่นที่บรรจุข้อมูลเพิ่มเติมสำหรับส่วนอื่นของกระบวนการคอมไพล์เพื่อนำไปใช้ แต่มีความสามารถในการอ่านโดยมนุษย์น้อยลง คอมไพเลอร์ภาษาโปรแกรมทุกภาษาจะแต่งเติมชื่อแตกต่างกันเล็กน้อย ดังนั้นเพื่อให้ฟังก์ชัน Rust สามารถตั้งชื่อโดยภาษาอื่นได้ เราจึงต้องปิดการแต่งเติมชื่อของคอมไพเลอร์ Rust สิ่งนี้เป็น unsafe เพราะอาจมีการชนกันของชื่อ (name collisions) ข้ามไลบรารีโดยไม่มีการแต่งเติมชื่อในตัว ดังนั้นจึงเป็นความรับผิดชอบของเราในการทำให้แน่ใจว่าชื่อที่เราเลือกนั้นปลอดภัยที่จะส่งออกโดยไม่แต่งเติมชื่อ
ในตัวอย่างต่อไปนี้ เราทำให้ฟังก์ชัน call_from_c เข้าถึงได้จากโค้ด C หลังจากที่มันถูกคอมไพล์เป็นไลบรารีที่แชร์และลิงก์จาก C:
#![allow(unused)]
fn main() {
#[unsafe(no_mangle)]
pub extern "C" fn call_from_c() {
println!("Just called a Rust function from C!");
}
}
การใช้ extern นี้ต้องการ unsafe เฉพาะในแอตทริบิวต์เท่านั้น ไม่ใช่บนบล็อก extern
การเข้าถึงหรือแก้ไขตัวแปรสแตติกที่เปลี่ยนค่าได้
ในหนังสือเล่มนี้ เรายังไม่ได้พูดถึงตัวแปรโกลบอล (global variables) ซึ่ง Rust รองรับแต่ว่าอาจมีปัญหาเกี่ยวกับกฎความเป็นเจ้าของของ Rust หากสอง threads เข้าถึงตัวแปรโกลบอลเปลี่ยนค่าได้ตัวเดียวกัน มันอาจก่อให้เกิดภาวะแย่งชิงข้อมูล (data race) ได้
ใน Rust ตัวแปรโกลบอลเรียกว่าตัวแปร static โค้ดตัวอย่างที่ 20-10 แสดงตัวอย่างการประกาศและการใช้ตัวแปร static โดยมีสไลซ์สตริงเป็นค่า
static HELLO_WORLD: &str = "Hello, world!";
fn main() {
println!("value is: {HELLO_WORLD}");
}
ตัวแปร static มีความคล้ายคลึงกับค่าคงที่ (constants) ซึ่งเราอภิปรายในส่วน “Declaring Constants” ในบทที่ 3 ชื่อของตัวแปร static จะเป็นตัวพิมพ์ใหญ่แบบ snake case (SCREAMING_SNAKE_CASE) ตามธรรมเนียม ตัวแปร static สามารถจัดเก็บเฉพาะการอ้างอิงที่มีอายุการใช้งาน 'static เท่านั้น ซึ่งหมายความว่าคอมไพเลอร์ Rust สามารถคำนวณอายุการใช้งานได้และเราไม่จำเป็นต้องระบุชื่ออย่างชัดแจ้ง การเข้าถึงตัวแปร static ที่เปลี่ยนค่าไม่ได้นั้นปลอดภัย
ข้อแตกต่างเล็กน้อยระหว่างค่าคงที่และตัวแปร static ที่เปลี่ยนค่าไม่ได้คือ ค่าในตัวแปร static จะมีที่อยู่ที่แน่นอนในหน่วยความจำ การใช้ค่าจะเข้าถึงข้อมูลเดิมเสมอ ในทางกลับกัน ค่าคงที่ได้รับอนุญาตให้คัดลอกซ้ำข้อมูลของมันเมื่อใดก็ตามที่ถูกใช้งาน อีกข้อแตกต่างหนึ่งคือตัวแปร static สามารถเปลี่ยนค่าได้ (mutable) การเข้าถึงและการแก้ไขตัวแปร static ที่เปลี่ยนค่าได้นั้นเป็น unsafe โค้ดตัวอย่างที่ 20-11 แสดงวิธีประกาศ เข้าถึง และแก้ไขตัวแปร static ที่เปลี่ยนค่าได้ชื่อ COUNTER
static mut COUNTER: u32 = 0;
/// SAFETY: Calling this from more than a single thread at a time is undefined
/// behavior, so you *must* guarantee you only call it from a single thread at
/// a time.
unsafe fn add_to_count(inc: u32) {
unsafe {
COUNTER += inc;
}
}
fn main() {
unsafe {
// SAFETY: This is only called from a single thread in `main`.
add_to_count(3);
println!("COUNTER: {}", *(&raw const COUNTER));
}
}
เช่นเดียวกับตัวแปรปกติ เราระบุความสามารถในการเปลี่ยนค่าโดยใช้คีย์เวิร์ด mut โค้ดใดๆ ที่อ่านหรือเขียนจาก COUNTER ต้องอยู่ภายในบล็อก unsafe โค้ดในโค้ดตัวอย่างที่ 20-11 คอมไพล์และพิมพ์ COUNTER: 3 ตามที่เราคาดหวังเพราะเป็นแบบ single-threaded การให้หลาย thread เข้าถึง COUNTER น่าจะส่งผลให้เกิดภาวะแย่งชิงข้อมูล ดังนั้นจึงเป็นพฤติกรรมที่ไม่ถูกกำหนดไว้ (undefined behavior) ดังนั้น เราจึงจำเป็นต้องทำเครื่องหมายทั้งฟังก์ชันเป็น unsafe และบันทึกข้อจำกัดด้านความปลอดภัยไว้ในเอกสาร เพื่อให้ใครก็ตามที่เรียกใช้ฟังก์ชันรู้ว่าสิ่งใดอนุญาตและไม่อนุญาตให้ทำได้อย่างปลอดภัย
เมื่อใดก็ตามที่เราเขียนฟังก์ชัน unsafe ถือเป็นแบบฉบับ (idiomatic) ที่จะเขียนคอมเมนต์เริ่มต้นด้วย SAFETY และอธิบายสิ่งที่ผู้เรียกจำเป็นต้องทำเพื่อเรียกใช้ฟังก์ชันได้อย่างปลอดภัย ในทำนองเดียวกัน เมื่อใดก็ตามที่เราปฏิบัติการ unsafe ถือเป็นแบบฉบับที่จะเขียนคอมเมนต์เริ่มต้นด้วย SAFETY เพื่ออธิบายว่ากฎระเบียบความปลอดภัยได้รับการปฏิบัติตามอย่างไร
นอกจากนี้ คอมไพเลอร์จะปฏิเสธโดยค่านิยมเริ่มต้น ความพยายามใดๆ ในการสร้างการอ้างอิงไปยังตัวแปร static ที่เปลี่ยนค่าได้ผ่านทาง compiler lint คุณต้องถอนตัวออกจากการปกป้องของ lint นั้นอย่างชัดแจ้งโดยการเพิ่มแอตทริบิวต์ #[allow(static_mut_refs)] หรือเข้าถึงตัวแปร static ที่เปลี่ยนค่าได้ผ่านพอยน์เตอร์ดิบที่สร้างด้วยตัวดำเนินการยืมดิบตัวใดตัวหนึ่ง ซึ่งรวมถึงกรณีที่การอ้างอิงถูกสร้างขึ้นอย่างมองไม่เห็น เช่น เมื่อมันถูกใช้ใน println! ในรายการโค้ดนี้ การกำหนดให้การอ้างอิงไปยังตัวแปร static ที่เปลี่ยนค่าได้สร้างขึ้นผ่านพอยน์เตอร์ดิบช่วยให้ข้อกำหนดด้านความปลอดภัยสำหรับการใช้งานชัดเจนยิ่งขึ้น
ด้วยข้อมูลที่เปลี่ยนค่าได้ซึ่งเข้าถึงได้ทั่วโลก (globally accessible) เป็นเรื่องยากที่จะรับประกันว่าไม่มีภาวะแย่งชิงข้อมูลเกิดขึ้น นั่นคือเหตุผลที่ Rust ถือว่าตัวแปร static ที่เปลี่ยนค่าได้เป็น unsafe ในตำแหน่งที่เป็นไปได้ ควรเลือกใช้เทคนิคการทำงานแบบพร้อมกันและสมาร์ตพอยน์เตอร์ที่ปลอดภัยสำหรับ thread ที่เราอภิปรายในบทที่ 16 เพื่อให้คอมไพเลอร์ตรวจสอบว่าการเข้าถึงข้อมูลจาก thread ต่างๆ ถูกต้องอย่างปลอดภัย
การนำ Unsafe Trait ไปใช้งาน
เราสามารถใช้ unsafe เพื่อนำ unsafe trait ไปใช้งานได้ trait จะเป็น unsafe เมื่ออย่างน้อยหนึ่งในเมธอดของมันมีคุณสมบัติคงตัวบางประการ (invariant) ที่คอมไพเลอร์ไม่สามารถตรวจสอบได้ เราประกาศว่า trait เป็น unsafe โดยการเพิ่มคีย์เวิร์ด unsafe ก่อน trait และทำเครื่องหมายการนำ trait ไปใช้งานเป็น unsafe ด้วยเช่นกัน ดังแสดงในโค้ดตัวอย่างที่ 20-12
unsafe trait Foo {
// methods go here
}
unsafe impl Foo for i32 {
// method implementations go here
}
fn main() {}
โดยการใช้ unsafe impl เรากำลังสัญญาว่าเราจะรักษาคุณสมบัติคงตัวที่คอมไพเลอร์ไม่สามารถตรวจสอบได้ไว้
ตัวอย่างเช่น ลองนึกถึง marker traits อย่าง Send และ Sync ที่เราอภิปรายในส่วน “Extensible Concurrency with Send and Sync” ในบทที่ 16: คอมไพเลอร์จะนำ trait เหล่านี้ไปใช้งานโดยอัตโนมัติหากชนิดข้อมูลของเราประกอบขึ้นจากชนิดข้อมูลอื่นที่นำ Send และ Sync ไปใช้งานทั้งหมด หากเรานำชนิดข้อมูลที่บรรจุชนิดข้อมูลที่ไม่นำ Send หรือ Sync ไปใช้งานมาใช้งาน เช่น พอยน์เตอร์ดิบ และเราต้องการทำเครื่องหมายชนิดข้อมูลนั้นเป็น Send หรือ Sync เราต้องใช้ unsafe Rust ไม่สามารถตรวจสอบได้ว่าชนิดข้อมูลของเราปฏิบัติตามการรับประกันว่าสามารถส่งข้าม threads หรือเข้าถึงจากหลาย threads ได้อย่างปลอดภัยหรือไม่ ดังนั้นเราจึงจำเป็นต้องทำความสะอาดการตรวจสอบเหล่านั้นด้วยตนเองและระบุดังกล่าวด้วย unsafe
การเข้าถึงฟิลด์ของ Union
ปฏิบัติการสุดท้ายที่ทำงานได้เฉพาะกับ unsafe คือการเข้าถึงฟิลด์ของ union ตัว union จะคล้ายกับ struct แต่ฟิลด์ที่ถูกประกาศไว้เพียงฟิลด์เดียวเท่านั้นที่จะถูกใช้ในอินสแตนซ์เฉพาะ ณ เวลาหนึ่ง Unions ถูกนำมาใช้เป็นหลักในการปฏิสัมพันธ์กับ unions ในโค้ดภาษา C การเข้าถึงฟิลด์ของ union นั้นเป็น unsafe เนื่องจาก Rust ไม่สามารถรับประกันชนิดข้อมูลของข้อมูลที่ถูกจัดเก็บอยู่ในอินสแตนซ์ของ union ณ ปัจจุบันได้ คุณสามารถเรียนรู้เพิ่มเติมเกี่ยวกับ unions ได้ใน the Rust Reference
การใช้ Miri ตรวจสอบโค้ด Unsafe
เมื่อเขียนโค้ด unsafe คุณอาจต้องการตรวจสอบว่าสิ่งที่คุณเขียนมีความปลอดภัยและถูกต้องจริงหรือไม่ หนึ่งในวิธีที่ดีที่สุดในการทำเช่นนั้นคือการใช้ Miri ซึ่งเป็นเครื่องมืออย่างเป็นทางการของ Rust สำหรับการตรวจจับพฤติกรรมที่ไม่ถูกกำหนดไว้ (undefined behavior) ในขณะที่ตัวตรวจสอบการยืมเป็นเครื่องมือแบบ สแตติก ที่ทำงานในขณะคอมไพล์ Miri เป็นเครื่องมือแบบ ไดนามิก ที่ทำงานในขณะรัน มันจะตรวจสอบโค้ดของคุณโดยการรันโปรแกรมของคุณ หรือชุดการทดสอบ และตรวจจับเมื่อคุณละเมิดกฎระเบียบที่มันเข้าใจว่า Rust ควรทำงานอย่างไร
การใช้ Miri จำเป็นต้องใช้การบิลด์ nightly ของ Rust (ซึ่งเราพูดถึงเพิ่มเติมใน Appendix G: How Rust is Made and “Nightly Rust”) คุณสามารถติดตั้งทั้งเวอร์ชัน nightly ของ Rust และเครื่องมือ Miri ได้โดยการพิมพ์ rustup +nightly component add miri สิ่งนี้ไม่ได้เปลี่ยนเวอร์ชันของ Rust ที่โปรเจกต์ของคุณใช้; มันเพียงเพิ่มเครื่องมือลงในระบบของคุณเพื่อให้คุณสามารถใช้งานได้เมื่อต้องการ คุณสามารถรัน Miri บนโปรเจกต์ได้โดยการพิมพ์ cargo +nightly miri run หรือ cargo +nightly miri test
สำหรับตัวอย่างของประโยชน์จากเครื่องมือนี้ ให้พิจารณาสิ่งที่เกิดขึ้นเมื่อเรารันมันกับโค้ดตัวอย่างที่ 20-7:
$ cargo +nightly miri run
Compiling unsafe-example v0.1.0 (file:///projects/unsafe-example)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.17s
Running `file:///home/.rustup/toolchains/nightly/bin/cargo-miri runner target/miri/debug/unsafe-example`
warning: integer-to-pointer cast
--> src/main.rs:5:13
|
5 | let r = address as *mut i32;
| ^^^^^^^^^^^^^^^^^^^ integer-to-pointer cast
|
= help: this program is using integer-to-pointer casts or (equivalently) `ptr::with_exposed_provenance`, which means that Miri might miss pointer bugs in this program
= help: see https://doc.rust-lang.org/nightly/std/ptr/fn.with_exposed_provenance.html for more details on that operation
= help: to ensure that Miri does not miss bugs in your program, use Strict Provenance APIs (https://doc.rust-lang.org/nightly/std/ptr/index.html#strict-provenance, https://crates.io/crates/sptr) instead
= help: you can then set `MIRIFLAGS=-Zmiri-strict-provenance` to ensure you are not relying on `with_exposed_provenance` semantics
= help: alternatively, `MIRIFLAGS=-Zmiri-permissive-provenance` disables this warning
error: Undefined Behavior: constructing invalid value of type &mut [i32]: encountered a dangling reference (0x1234[noalloc] has no provenance)
--> src/main.rs:7:35
|
7 | let values: &[i32] = unsafe { slice::from_raw_parts_mut(r, 10000) };
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Undefined Behavior occurred here
|
= help: this indicates a bug in the program: it performed an invalid operation, and caused Undefined Behavior
= help: see https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html for further information
note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace
error: aborting due to 1 previous error; 1 warning emitted
Miri แจ้งเตือนอย่างถูกต้องว่าเรากำลังแปลงชนิดข้อมูลจำนวนเต็มเป็นพอยน์เตอร์ ซึ่งอาจเป็นปัญหาได้ แต่ Miri ไม่สามารถระบุได้ว่ามีปัญหาเกิดขึ้นหรือไม่เนื่องจากมันไม่รู้ว่าพอยน์เตอร์เกิดขึ้นมาจากไหน จากนั้น Miri คืนค่าข้อผิดพลาดตรงที่โค้ดตัวอย่างที่ 20-7 มีพฤติกรรมที่ไม่ถูกกำหนดไว้เนื่องจากเรามี dangling pointer ขอบคุณ Miri ตอนนี้เรารู้แล้วว่ามีความเสี่ยงที่จะเกิดพฤติกรรมที่ไม่ถูกกำหนดไว้ และเราสามารถคิดคำนึงเกี่ยวกับวิธีทำให้โค้ดปลอดภัยได้ ในบางกรณี Miri ยังสามารถให้คำแนะนำเกี่ยวกับวิธีแก้ไขข้อผิดพลาดได้ด้วย
Miri ไม่ได้จับทุกอย่างที่คุณอาจทำผิดพลาดเมื่อเขียนโค้ด unsafe Miri เป็นเครื่องมือวิเคราะห์แบบไดนามิก ดังนั้นจึงจับเฉพาะปัญหาของโค้ดที่ถูกรันจริงๆ เท่านั้น นั่นหมายความว่าคุณจะต้องใช้มันร่วมกับเทคนิคการทดสอบที่ดีเพื่อเพิ่มความมั่นใจเกี่ยวกับโค้ด unsafe ที่คุณเขียนขึ้น นอกจากนี้ Miri ไม่ได้ครอบคลุมทุกวิถีทางที่เป็นไปได้ที่โค้ดของคุณอาจไม่ปลอดภัย (unsound)
กล่าวอีกนัยหนึ่ง: หาก Miri ตรวจพบ ปัญหา คุณรู้ว่ามีบั๊กอยู่ แต่เพียงเพราะ Miri ไม่ตรวจพบ บั๊ก ไม่ได้หมายความว่าไม่มีปัญหา อย่างไรก็ตาม มันสามารถจับปัญหาได้จำนวนมาก ลองรันมันกับตัวอย่างอื่นๆ ของโค้ด unsafe ในบทนี้และดูว่ามันรายงานอะไร!
คุณสามารถเรียนรู้เพิ่มเติมเกี่ยวกับ Miri ได้ที่ GitHub repository ของมัน
การใช้โค้ด Unsafe อย่างถูกต้อง
การใช้ unsafe เพื่อใช้ซูเปอร์เพาเวอร์ทั้ง 5 ประการที่เพิ่งกล่าวถึงนั้นไม่ใช่เรื่องที่ผิดหรือถูกมองข้าม แต่มันซับซ้อนกว่าที่จะเขียนโค้ด unsafe ให้ถูกต้องเนื่องจากคอมไพเลอร์ไม่สามารถช่วยรักษาความปลอดภัยของหน่วยความจำได้ เมื่อคุณมีเหตุผลในการใช้โค้ด unsafe คุณสามารถทำเช่นนั้นได้และการระบุคำอธิบายประกอบ unsafe อย่างชัดแจ้งจะทำให้ง่ายต่อการติดตามที่มาของปัญหาเมื่อเกิดขึ้น เมื่อใดก็ตามที่คุณเขียนโค้ด unsafe คุณสามารถใช้ Miri เพื่อช่วยให้มั่นใจได้มากขึ้นว่าโค้ดที่คุณเขียนปฏิบัติตามกฎระเบียบของ Rust
สำหรับการสำรวจเชิงลึกเพิ่มเติมเกี่ยวกับการทำงานกับ unsafe Rust อย่างมีประสิทธิภาพ อ่านคู่มืออย่างเป็นทางการของ Rust สำหรับ unsafe, The Rustonomicon