Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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) ซูเปอร์เพาเวอร์เหล่านั้นรวมถึงความสามารถในการ:

  1. การเรียกดูค่าพอยน์เตอร์ดิบ (Dereference a raw pointer)
  2. การเรียกใช้ฟังก์ชันหรือเมธอดที่เป็น unsafe
  3. การเข้าถึงหรือแก้ไขตัวแปรสแตติกที่เปลี่ยนค่าได้ (mutable static variable)
  4. การนำ trait ที่เป็น unsafe ไปใช้งาน (Implement an unsafe trait)
  5. การเข้าถึงฟิลด์ของ 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