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 ของ Rust ที่ บังคับที่ compile time อย่างไรก็ตาม Rust มีภาษาที่สองซ่อนภายในที่ไม่ บังคับการรับประกัน memory safety เหล่านี้ — มันถูกเรียก unsafe Rust และทำงานเหมือน Rust ปกติแต่ให้เรา superpower เพิ่ม

Unsafe Rust มีอยู่เพราะตามธรรมชาติ การวิเคราะห์ static เป็น conservative เมื่อ compiler พยายามตัดสินว่าโค้ดยึดถือการรับประกันหรือไม่ มันดีกว่า สำหรับมันที่จะปฏิเสธโปรแกรม valid บางอย่างมากกว่ายอมรับโปรแกรม invalid บางอย่าง แม้โค้ด อาจ โอเค ถ้า compiler Rust ไม่มีข้อมูลพอที่จะมั่นใจ มันจะปฏิเสธโค้ด ในกรณีเหล่านี้ คุณใช้โค้ด unsafe เพื่อบอก compiler “เชื่อฉัน ฉันรู้ที่ฉันทำ” ได้ ถูกเตือน อย่างไรก็ตาม คุณใช้ unsafe Rust ที่ความเสี่ยงของคุณเอง — ถ้าคุณใช้โค้ด unsafe ผิด ปัญหาเกิดได้เพราะ memory unsafety เช่นการ dereference null pointer

เหตุผลอื่นที่ Rust มี alter ego unsafe คือ hardware computer พื้นฐาน unsafe ในตัวเอง ถ้า Rust ไม่ให้คุณทำ operation unsafe คุณจะทำงานบาง อย่างไม่ได้ Rust ต้องอนุญาตให้คุณทำ low-level systems programming เช่น interact กับ operating system โดยตรงหรือแม้เขียน operating system ของคุณเอง การทำงานกับ low-level systems programming เป็นหนึ่งใน เป้าหมายของภาษา มาสำรวจว่าเราทำอะไรได้กับ unsafe Rust และวิธีทำมัน

ใช้ Unsafe Superpower

เพื่อ switch ไป unsafe Rust ใช้คีย์เวิร์ด unsafe แล้วเริ่ม block ใหม่ ที่บรรจุโค้ด unsafe คุณทำ action ห้าอย่างใน unsafe Rust ที่คุณทำไม่ได้ ใน safe Rust ซึ่งเราเรียก unsafe superpower Superpower เหล่านั้น รวมถึงความสามารถใน:

  1. Dereference raw pointer
  2. เรียกฟังก์ชันหรือเมธอด unsafe
  3. เข้าถึงหรือแก้ตัวแปร static แบบ mutable
  4. Implement trait unsafe
  5. เข้าถึง field ของ union

มันสำคัญที่จะเข้าใจว่า unsafe ไม่ปิด borrow checker หรือปิดการ check safety อื่นของ Rust — ถ้าคุณใช้ reference ในโค้ด unsafe มันจะยังถูก check คีย์เวิร์ด unsafe เพียงให้คุณเข้าถึงห้าฟีเจอร์เหล่านี้ที่แล้ว ไม่ถูก check โดย compiler สำหรับ memory safety คุณจะยังได้ safety ระดับหนึ่งภายใน block unsafe

นอกจากนี้ unsafe ไม่ได้หมายความว่าโค้ดภายใน block อันตรายแน่หรือว่ามัน จะมีปัญหา memory safety แน่ — เจตนาคือในฐานะ programmer คุณจะรับประกัน ว่าโค้ดภายใน block unsafe จะเข้าถึง memory ในวิธี valid

คนผิดพลาดได้และความผิดพลาดจะเกิด แต่โดยต้องการ operation unsafe ห้านี้ อยู่ภายใน block ที่ annotate ด้วย unsafe คุณจะรู้ว่า error ใดที่ เกี่ยวกับ memory safety ต้องอยู่ภายใน block unsafe รักษา block unsafe ให้เล็ก — คุณจะขอบคุณภายหลังเมื่อคุณตรวจสอบ memory bug

เพื่อแยก isolate โค้ด unsafe มากที่สุด ดีที่สุดที่จะห่อโค้ดเช่นนั้น ภายใน safe abstraction และให้ safe API ซึ่งเราจะพูดถึงภายหลังในบทเมื่อ เราตรวจสอบฟังก์ชันและเมธอด unsafe ส่วนของ standard library ถูก implement เป็น safe abstraction เหนือโค้ด unsafe ที่ถูก audit แล้ว การห่อโค้ด unsafe ใน safe abstraction ป้องกันการใช้ unsafe จากการ รั่วไปยังทุกที่ที่คุณหรือ user ของคุณอาจต้องการใช้ functionality ที่ implement ด้วยโค้ด unsafe เพราะการใช้ safe abstraction เป็น safe

มาดูแต่ละ unsafe superpower ทั้งห้าตามลำดับ เราจะดู abstraction บาง อย่างที่ให้ interface safe ให้โค้ด unsafe ด้วย

Dereference Raw Pointer

ในบทที่ 4 ในส่วน “Dangling Reference” เรากล่าวว่า compiler รับประกันว่า reference valid เสมอ Unsafe Rust มี type ใหม่สองอันเรียก raw pointer ที่คล้ายกับ reference เช่นเดียวกับ reference, raw pointer เป็น immutable หรือ mutable ได้และเขียนเป็น *const T และ *mut T ตามลำดับ Asterisk ไม่ใช่ dereference operator — มันเป็นส่วนของชื่อ type ใน context ของ raw pointer, immutable หมาย ความว่า pointer ไม่สามารถถูก assign โดยตรงหลังจากถูก dereference

ต่างจาก reference และ smart pointer, raw pointer:

  • ถูกอนุญาตให้ ignore กฎ borrowing โดยมีทั้ง pointer immutable และ mutable หรือหลาย mutable pointer ไปยังที่เดียวกัน
  • ไม่ถูกรับประกันว่า point ไปยัง memory ที่ valid
  • ถูกอนุญาตให้เป็น null
  • ไม่ implement การ cleanup อัตโนมัติใด

โดย opt out จากการให้ Rust บังคับการรับประกันเหล่านี้ คุณยกเลิก safety ที่รับประกันเพื่อแลกกับ performance ที่ดีกว่าหรือความสามารถในการ interface กับภาษาอื่นหรือ hardware ที่การรับประกันของ Rust ไม่ apply

Listing 20-1 แสดงวิธีสร้าง raw pointer แบบ immutable และ mutable

fn main() {
    let mut num = 5;

    let r1 = &raw const num;
    let r2 = &raw mut num;
}
Listing 20-1: สร้าง raw pointer ด้วย raw borrow operator

สังเกตว่าเราไม่รวมคีย์เวิร์ด unsafe ในโค้ดนี้ เราสร้าง raw pointer ในโค้ด safe ได้ — เราเพียงไม่สามารถ dereference raw pointer นอก block unsafe ดังที่คุณจะเห็นในไม่กี่ที่

เราสร้าง raw pointer โดยใช้ raw borrow operator: &raw const num สร้าง raw pointer immutable *const i32 และ &raw mut num สร้าง raw pointer mutable *mut i32 เพราะเราสร้างพวกมันโดยตรงจากตัวแปร local เรารู้ว่า raw pointer เฉพาะเหล่านี้ valid แต่เราทำสมมติฐานนั้นเกี่ยวกับ raw pointer ใดไม่ได้

เพื่อสาธิตสิ่งนี้ ถัดไปเราจะสร้าง raw pointer ที่ความ valid ของมันเรา ไม่สามารถมั่นใจ โดยใช้คีย์เวิร์ด as เพื่อ cast ค่าแทนการใช้ raw borrow operator Listing 20-2 แสดงวิธีสร้าง raw pointer ไปยังที่ใด ๆ ใน memory การพยายามใช้ memory ใด ๆ คือ undefined — อาจมีข้อมูลที่ address นั้นหรือไม่มี compiler อาจ optimize โค้ดเพื่อให้ไม่มีการเข้า ถึง memory หรือโปรแกรมอาจสิ้นสุดด้วย segmentation fault ปกติไม่มีเหตุผล ดีที่จะเขียนโค้ดแบบนี้ โดยเฉพาะในกรณีที่คุณใช้ raw borrow operator แทน ได้ แต่มันเป็นไปได้

fn main() {
    let address = 0x012345usize;
    let r = address as *const i32;
}
Listing 20-2: สร้าง raw pointer ไปยัง memory address ใด ๆ

จำว่าเราสร้าง raw pointer ในโค้ด safe ได้ แต่เราไม่สามารถ dereference raw pointer และอ่านข้อมูลที่ถูก point ไป ใน Listing 20-3 เราใช้ dereference operator * บน raw pointer ที่ต้องการ block 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);
    }
}
Listing 20-3: Dereference raw pointer ภายใน block unsafe

สร้าง pointer ไม่ทำอันตราย — มันเพียงเมื่อเราพยายามเข้าถึงค่าที่มัน point ไปที่เราอาจจบลงที่จัดการกับค่า invalid

สังเกตด้วยว่าใน Listing 20-1 และ 20-3 เราสร้าง raw pointer *const i32 และ *mut i32 ที่ point ไปยังที่ memory เดียวกัน ที่ num ถูกเก็บ ถ้าเราลองสร้าง reference immutable และ mutable ของ num แทน โค้ดจะ ไม่ compile เพราะกฎ ownership ของ Rust ไม่อนุญาต mutable reference ใน เวลาเดียวกับ immutable reference ใด ๆ ด้วย raw pointer เราสร้าง mutable pointer และ immutable pointer ไปยังที่เดียวกันและเปลี่ยนข้อมูลผ่าน mutable pointer ได้ อาจสร้าง data race ระวัง!

ด้วยอันตรายทั้งหมดเหล่านี้ ทำไมคุณจะใช้ raw pointer? Use case หลัก อันหนึ่งคือเมื่อ interface กับโค้ด C ดังที่คุณจะเห็นในส่วนถัดไป กรณี อื่นคือเมื่อสร้าง safe abstraction ที่ borrow checker ไม่เข้าใจ เราจะ แนะนำฟังก์ชัน unsafe แล้วดูตัวอย่างของ safe abstraction ที่ใช้โค้ด unsafe

เรียกฟังก์ชันหรือเมธอด Unsafe

ชนิดที่สองของ operation ที่คุณทำใน block unsafe ได้คือการเรียกฟังก์ชัน unsafe ฟังก์ชันและเมธอด unsafe ดูเหมือนฟังก์ชันและเมธอดปกติเป๊ะ แต่ พวกมันมี unsafe เพิ่มก่อนส่วนที่เหลือของนิยาม คีย์เวิร์ด unsafe ใน context นี้บ่งบอกว่าฟังก์ชันมีข้อกำหนดที่เราต้องยึดถือเมื่อเราเรียก ฟังก์ชันนี้ เพราะ Rust ไม่สามารถรับประกันว่าเราพบข้อกำหนดเหล่านี้ โดย เรียกฟังก์ชัน unsafe ภายใน block unsafe เราบอกว่าเราอ่าน documentation ของฟังก์ชันนี้และเรารับผิดชอบสำหรับการยึดถือ contract ของฟังก์ชัน

นี่คือฟังก์ชัน unsafe ชื่อ dangerous ที่ไม่ทำอะไรใน body ของมัน:

fn main() {
    unsafe fn dangerous() {}

    unsafe {
        dangerous();
    }
}

เราต้องเรียกฟังก์ชัน dangerous ภายใน block unsafe แยก ถ้าเราพยายาม เรียก dangerous โดยไม่มี block unsafe เราจะได้ error:

$ 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

ด้วย block unsafe เรากำลัง assert กับ Rust ว่าเราอ่าน documentation ของฟังก์ชัน เราเข้าใจวิธีใช้มันอย่างเหมาะสม และเรา verify ว่าเรากำลัง fulfill contract ของฟังก์ชัน

เพื่อทำ operation unsafe ใน body ของฟังก์ชัน unsafe คุณยังต้องใช้ block unsafe เหมือนภายในฟังก์ชันปกติ และ compiler จะเตือนคุณถ้าคุณลืม นี่ช่วยเรารักษา block unsafe ให้เล็กที่สุดเท่าที่เป็นไปได้ เพราะ operation unsafe อาจไม่จำเป็นทั่ว body ฟังก์ชันทั้งหมด

สร้าง Safe Abstraction เหนือโค้ด Unsafe

เพียงเพราะฟังก์ชันบรรจุโค้ด unsafe ไม่ได้หมายความว่าเราต้องทำเครื่องหมาย ฟังก์ชันทั้งหมดเป็น unsafe จริง ๆ การห่อโค้ด unsafe ใน safe function คือ abstraction ปกติ เป็นตัวอย่าง มาศึกษาฟังก์ชัน split_at_mut จาก standard library ซึ่งต้องการโค้ด unsafe เราจะสำรวจว่าเราอาจ implement มันยังไง เมธอด safe นี้ถูกนิยามบน mutable slice — มันรับหนึ่ง slice และ ทำให้มันเป็นสองโดย split slice ที่ index ที่ให้เป็น argument Listing 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]);
}
Listing 20-4: ใช้ฟังก์ชัน split_at_mut ที่ safe

เราไม่สามารถ implement ฟังก์ชันนี้โดยใช้เพียง safe Rust ความพยายามอาจดู แบบ Listing 20-5 ซึ่งจะไม่ compile เพื่อความเรียบง่าย เราจะ implement split_at_mut เป็นฟังก์ชันแทนเมธอดและเพียงสำหรับ slice ของค่า i32 แทน type generic 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);
}
Listing 20-5: ความพยายาม implement split_at_mut โดยใช้เพียง safe Rust

ฟังก์ชันนี้ได้ความยาวทั้งหมดของ slice ก่อน แล้ว มัน assert ว่า index ที่ให้เป็น parameter อยู่ภายใน slice โดย check ว่ามันน้อยกว่าหรือเท่า ความยาว Assertion หมายความว่าถ้าเราส่ง index ที่มากกว่าความยาวเพื่อ split slice ที่ ฟังก์ชันจะ panic ก่อนมันพยายามใช้ index นั้น

แล้ว เรา return สอง mutable slice ใน tuple — หนึ่งจากตอนเริ่มของ slice เดิมถึง index mid และอีกอันจาก mid ถึงตอนจบของ slice

เมื่อเราพยายาม compile โค้ดใน Listing 20-5 เราจะได้ error:

$ 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

borrow checker ของ Rust ไม่สามารถเข้าใจว่าเรากำลัง borrow ส่วนต่างกันของ slice — มันเพียงรู้ว่าเรากำลัง borrow จาก slice เดียวกันสองครั้ง Borrowing ส่วนต่างกันของ slice พื้นฐานคือ okay เพราะสอง slice ไม่ overlap แต่ Rust ไม่ฉลาดพอที่จะรู้นี่ เมื่อเรารู้ว่าโค้ด okay แต่ Rust ไม่ มันถึงเวลาที่จะเอื้อมไปโค้ด unsafe

Listing 20-6 แสดงวิธีใช้ block unsafe, raw pointer และการเรียก ฟังก์ชัน unsafe บางตัวเพื่อทำให้ implementation ของ 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);
}
Listing 20-6: ใช้โค้ด unsafe ใน implementation ของฟังก์ชัน split_at_mut

จำจากส่วน “Type Slice” ในบทที่ 4 ว่า slice คือ pointer ไปยังข้อมูลและความยาวของ slice เราใช้เมธอด len เพื่อได้ความยาวของ slice และเมธอด as_mut_ptr เพื่อเข้าถึง raw pointer ของ slice ในกรณีนี้ เพราะเรามี mutable slice ของค่า i32, as_mut_ptr return raw pointer ที่มี type *mut i32 ซึ่งเราเก็บใน ตัวแปร ptr

เรารักษา assertion ว่า index mid อยู่ภายใน slice แล้ว เราไปยังโค้ด unsafe — ฟังก์ชัน slice::from_raw_parts_mut รับ raw pointer และความ ยาว และมันสร้าง slice เราใช้ฟังก์ชันนี้เพื่อสร้าง slice ที่เริ่มจาก ptr และยาว mid item แล้ว เราเรียกเมธอด add บน ptr กับ mid เป็น argument เพื่อได้ raw pointer ที่เริ่มที่ mid และเราสร้าง slice โดยใช้ pointer นั้นและจำนวน item ที่เหลือหลัง mid เป็นความยาว

ฟังก์ชัน slice::from_raw_parts_mut คือ unsafe เพราะมันรับ raw pointer และต้องเชื่อว่า pointer นี้ valid เมธอด add บน raw pointer ก็ unsafe เพราะมันต้องเชื่อว่าที่ตั้ง offset ก็เป็น pointer valid ดังนั้น เรา ต้องใส่ block unsafe รอบการเรียก slice::from_raw_parts_mut และ add เพื่อให้เราเรียกพวกมันได้ โดยดูที่โค้ดและโดยเพิ่ม assertion ว่า mid ต้องน้อยกว่าหรือเท่า len เราบอกได้ว่า raw pointer ทั้งหมดที่ ใช้ภายใน block unsafe จะเป็น pointer valid ไปยังข้อมูลภายใน slice นี่คือการใช้ unsafe ที่ยอมรับได้และเหมาะสม

สังเกตว่าเราไม่ต้องทำเครื่องหมายฟังก์ชัน split_at_mut ผลลัพธ์เป็น unsafe และเราเรียกฟังก์ชันนี้จาก safe Rust ได้ เราสร้าง safe abstraction ให้โค้ด unsafe ด้วย implementation ของฟังก์ชันที่ใช้โค้ด unsafe ในวิธี safe เพราะมันสร้างเพียง pointer valid จากข้อมูลที่ ฟังก์ชันนี้เข้าถึงได้

ในทางกลับ การใช้ slice::from_raw_parts_mut ใน Listing 20-7 น่าจะ crash เมื่อ slice ถูกใช้ โค้ดนี้รับที่ memory ใด ๆ และสร้าง slice ยาว 10,000 item

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) };
}
Listing 20-7: สร้าง slice จากที่ memory ใด ๆ

เราไม่เป็นเจ้าของ memory ที่ที่ใด ๆ นี้ และไม่มีการรับประกันว่า slice ที่โค้ดนี้สร้างบรรจุค่า i32 valid ความพยายามใช้ values ราวกับมัน เป็น slice valid ส่งผลเป็น undefined behavior

ใช้ฟังก์ชัน extern เพื่อเรียกโค้ดภายนอก

บางครั้งโค้ด Rust ของคุณอาจต้อง interact กับโค้ดที่เขียนในภาษาอื่น สำหรับนี้ Rust มีคีย์เวิร์ด extern ที่ facilitate การสร้างและใช้ Foreign Function Interface (FFI) ซึ่งคือวิธีสำหรับภาษาโปรแกรมที่จะ นิยามฟังก์ชันและเปิดใช้ภาษาโปรแกรมที่ต่าง (foreign) เพื่อเรียกฟังก์ชัน เหล่านั้น

Listing 20-8 สาธิตวิธีตั้ง integration กับฟังก์ชัน abs จาก C standard library ฟังก์ชันที่ประกาศภายใน block extern โดยทั่วไป unsafe ที่จะเรียกจากโค้ด Rust ดังนั้น block extern ต้องถูกทำเครื่องหมาย unsafe ด้วย เหตุผลคือภาษาอื่นไม่บังคับกฎและการรับประกันของ Rust และ Rust ไม่สามารถ check พวกมัน ดังนั้นความรับผิดชอบตกที่ programmer เพื่อ รับประกัน safety

Filename: src/main.rs
unsafe extern "C" {
    fn abs(input: i32) -> i32;
}

fn main() {
    unsafe {
        println!("Absolute value of -3 according to C: {}", abs(-3));
    }
}
Listing 20-8: ประกาศและเรียกฟังก์ชัน extern ที่นิยามในภาษาอื่น

ภายใน block unsafe extern "C" เรา list ชื่อและ signature ของฟังก์ชัน ภายนอกจากภาษาอื่นที่เราต้องการเรียก ส่วน "C" นิยามว่า application binary interface (ABI) ใดที่ฟังก์ชันภายนอกใช้ — ABI นิยามวิธีเรียก ฟังก์ชันที่ระดับ assembly ABI "C" ปกติที่สุดและตาม ABI ของภาษาโปรแกรม C ข้อมูลเกี่ยวกับ ABI ทั้งหมดที่ Rust สนับสนุนใช้ได้ใน the Rust Reference

ทุก item ที่ประกาศภายใน block unsafe extern เป็น unsafe โดยปริยาย อย่างไรก็ตาม ฟังก์ชัน FFI บาง เป็น safe ที่จะเรียก ตัวอย่างเช่น ฟังก์ชัน abs จาก C standard library ไม่มีการพิจารณา memory safety ใด และเรารู้ว่ามันถูกเรียกได้ด้วย i32 ใด ในกรณีแบบนี้ เราใช้คีย์เวิร์ด safe เพื่อบอกว่าฟังก์ชันเฉพาะนี้ safe ที่จะเรียกแม้มันอยู่ใน block unsafe extern ได้ เมื่อเราทำการเปลี่ยนแปลงนั้น การเรียกมันไม่ต้องการ block unsafe อีก ดังที่แสดงใน Listing 20-9

Filename: src/main.rs
unsafe extern "C" {
    safe fn abs(input: i32) -> i32;
}

fn main() {
    println!("Absolute value of -3 according to C: {}", abs(-3));
}
Listing 20-9: ทำเครื่องหมายฟังก์ชันเป็น safe อย่างชัดเจนภายใน block unsafe extern และเรียกมันอย่าง safe

ทำเครื่องหมายฟังก์ชันเป็น safe ไม่ทำให้มัน safe ในตัวเอง! แทน มัน เหมือนสัญญาที่คุณกำลังทำกับ Rust ว่ามัน safe มันยังเป็นความรับผิดชอบของ คุณที่จะรับประกันสัญญานั้นถูกรักษา!

เรียกฟังก์ชัน Rust จากภาษาอื่น

เราใช้ extern เพื่อสร้าง interface ที่อนุญาตให้ภาษาอื่นเรียกฟังก์ชัน Rust ได้ด้วย แทนการสร้าง block extern ทั้งหมด เราเพิ่มคีย์เวิร์ด extern และระบุ ABI ที่จะใช้ก่อนคีย์เวิร์ด fn สำหรับฟังก์ชันที่ เกี่ยวข้อง เราต้องเพิ่ม annotation #[unsafe(no_mangle)] เพื่อบอก compiler Rust ไม่ให้ mangle ชื่อของฟังก์ชันนี้ Mangling คือเมื่อ compiler เปลี่ยนชื่อที่เราให้ฟังก์ชันเป็นชื่อต่างที่บรรจุข้อมูลมากขึ้น สำหรับส่วนอื่นของกระบวนการ compilation เพื่อใช้แต่ readable โดยมนุษย์ น้อยกว่า ทุก compiler ภาษาโปรแกรม mangle ชื่อต่างกันเล็กน้อย ดังนั้น สำหรับฟังก์ชัน Rust ที่จะ nameable โดยภาษาอื่น เราต้องปิดการ mangling ชื่อของ compiler Rust นี่คือ unsafe เพราะอาจมี collision ชื่อทั่ว library ที่ไม่มี mangling ในตัว ดังนั้นมันคือความรับผิดชอบของเราที่จะ รับประกันว่าชื่อที่เราเลือก safe ที่จะ export โดยไม่ mangle

ในตัวอย่างต่อไปนี้ เราทำฟังก์ชัน call_from_c เข้าถึงได้จากโค้ด C หลังจากมันถูก compile เป็น shared library และ link จาก C:

#[unsafe(no_mangle)]
pub extern "C" fn call_from_c() {
    println!("Just called a Rust function from C!");
}

การใช้ extern นี้ต้องการ unsafe เพียงใน attribute ไม่ใช่บน block extern

เข้าถึงหรือแก้ตัวแปร Static แบบ Mutable

ในหนังสือนี้ เรายังไม่ได้พูดเกี่ยวกับตัวแปร global ซึ่ง Rust สนับสนุน แต่ซึ่งเป็นปัญหาได้กับกฎ ownership ของ Rust ถ้าสองเธรดเข้าถึงตัวแปร global แบบ mutable เดียวกัน มันสาเหตุ data race ได้

ใน Rust ตัวแปร global ถูกเรียกตัวแปร static Listing 20-10 แสดงตัวอย่าง การประกาศและการใช้ตัวแปร static กับ string slice เป็นค่า

Filename: src/main.rs
static HELLO_WORLD: &str = "Hello, world!";

fn main() {
    println!("value is: {HELLO_WORLD}");
}
Listing 20-10: นิยามและใช้ตัวแปร static แบบ immutable

ตัวแปร static คล้ายกับ constant ซึ่งเราพูดถึงในส่วน “ประกาศ Constant” ในบทที่ 3 ชื่อของตัวแปร static อยู่ใน SCREAMING_SNAKE_CASE โดย convention ตัวแปร static เก็บได้ เพียง reference ที่มี lifetime 'static ซึ่งหมายความว่า compiler Rust คำนวณ lifetime ได้และเราไม่ต้องการ annotate มันอย่างชัดเจน การเข้าถึง ตัวแปร static แบบ immutable เป็น safe

ความแตกต่างเล็กน้อยระหว่าง constant และตัวแปร static แบบ immutable คือ ค่าในตัวแปร static มี address คงที่ใน memory ใช้ค่าจะเข้าถึงข้อมูล เดียวกันเสมอ ตรงข้าม Constant ถูกอนุญาตให้ duplicate ข้อมูลของพวกมัน เมื่อใดก็ตามที่พวกมันถูกใช้ ความแตกต่างอื่นคือตัวแปร static เป็น mutable ได้ การเข้าถึงและแก้ตัวแปร static แบบ mutable เป็น unsafe Listing 20-11 แสดงวิธีประกาศ เข้าถึง และแก้ตัวแปร static แบบ mutable ชื่อ COUNTER

Filename: src/main.rs
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));
    }
}
Listing 20-11: อ่านจากหรือเขียนไปตัวแปร static แบบ mutable เป็น unsafe

เช่นเดียวกับตัวแปรปกติ เราระบุ mutability โดยใช้คีย์เวิร์ด mut โค้ดใด ที่อ่านหรือเขียนจาก COUNTER ต้องอยู่ภายใน block unsafe โค้ดใน Listing 20-11 compile และ print COUNTER: 3 ตามที่เราคาดเดาเพราะมัน เป็น single threaded การมีหลายเธรดเข้าถึง COUNTER น่าจะส่งผลเป็น data race ดังนั้นมันเป็น undefined behavior ดังนั้น เราต้องทำเครื่องหมาย ฟังก์ชันทั้งหมดเป็น unsafe และ document ข้อจำกัด safety เพื่อให้ใคร ก็ตามที่เรียกฟังก์ชันรู้ว่าพวกเขาได้รับและไม่ได้รับอนุญาตให้ทำอะไรอย่าง safe

เมื่อใดก็ตามที่เราเขียนฟังก์ชัน unsafe มันคือ idiomatic ที่จะเขียน comment เริ่มต้นด้วย SAFETY และอธิบายสิ่งที่ caller ต้องทำเพื่อเรียก ฟังก์ชันอย่าง safe เช่นกัน เมื่อใดก็ตามที่เราทำ operation unsafe มัน คือ idiomatic ที่จะเขียน comment เริ่มต้นด้วย SAFETY เพื่ออธิบายวิธี ที่กฎ safety ถูกยึดถือ

นอกจากนี้ compiler จะปฏิเสธโดยค่าเริ่มต้นความพยายามใดที่จะสร้าง reference ไปยังตัวแปร static แบบ mutable ผ่าน compiler lint คุณต้อง ทั้ง opt out จากการปกป้องของ lint นั้นอย่างชัดเจนโดยเพิ่ม annotation #[allow(static_mut_refs)] หรือเข้าถึงตัวแปร static แบบ mutable ผ่าน raw pointer ที่สร้างกับหนึ่งใน raw borrow operator นั้นรวมกรณีที่ reference ถูกสร้างมองไม่เห็น เช่นเมื่อมันถูกใช้ใน println! ใน listing โค้ดนี้ ต้องการ reference ไปยังตัวแปร static mutable ให้ถูกสร้างผ่าน raw pointer ช่วยทำให้ข้อกำหนด safety สำหรับการใช้พวกมันชัดเจนมากขึ้น

ด้วยข้อมูล mutable ที่ globally เข้าถึงได้ มันยากที่จะรับประกันว่าไม่มี data race ซึ่งเป็นเหตุผลที่ Rust พิจารณาตัวแปร static แบบ mutable ว่า เป็น unsafe ที่เป็นไปได้ มันเป็นที่นิยมมากกว่าที่จะใช้เทคนิค concurrency และ smart pointer thread-safe ที่เราพูดถึงในบทที่ 16 เพื่อ ให้ compiler check ว่าการเข้าถึงข้อมูลจากเธรดต่างกันถูกทำอย่าง safe

Implement Trait Unsafe

เราใช้ unsafe เพื่อ implement trait unsafe ได้ Trait เป็น unsafe เมื่อ อย่างน้อยหนึ่งในเมธอดของมันมี invariant บางอย่างที่ compiler ไม่ สามารถ verify เราประกาศว่า trait คือ unsafe โดยเพิ่มคีย์เวิร์ด unsafe ก่อน trait และทำเครื่องหมาย implementation ของ trait เป็น unsafe ด้วย ดังที่แสดงใน Listing 20-12

unsafe trait Foo {
    // methods go here
}

unsafe impl Foo for i32 {
    // method implementations go here
}

fn main() {}
Listing 20-12: นิยามและ implement trait unsafe

โดยใช้ unsafe impl เราสัญญาว่าเราจะยึดถือ invariant ที่ compiler ไม่ สามารถ verify

เป็นตัวอย่าง จำ trait marker Send และ Sync ที่เราพูดถึงในส่วน “Concurrency แบบขยายได้ด้วย Send และ Sync ในบทที่ 16 — compiler implement trait เหล่านี้อัตโนมัติถ้า type ของเรา ประกอบทั้งหมดของ type อื่นที่ implement Send และ Sync ถ้าเรา implement type ที่บรรจุ type ที่ไม่ implement Send หรือ Sync เช่น raw pointer และเราต้องการทำเครื่องหมาย type นั้นเป็น Send หรือ Sync เราต้องใช้ unsafe Rust ไม่สามารถ verify ว่า type ของเรา ยึดถือการรับประกันว่ามันถูกส่งทั่วเธรดได้อย่าง safe หรือเข้าถึงจากหลาย เธรด ดังนั้น เราต้องทำ check เหล่านั้นโดยมือและบ่งบอกเช่นนั้นด้วย unsafe

เข้าถึง Field ของ Union

action สุดท้ายที่ทำงานเพียงกับ unsafe คือการเข้าถึง field ของ union union คล้ายกับ struct แต่เพียงหนึ่ง field ที่ประกาศถูกใช้ใน instance เฉพาะในเวลาเดียวกัน Union ใช้หลัก ๆ เพื่อ interface กับ union ในโค้ด C การเข้าถึง field union คือ unsafe เพราะ Rust ไม่สามารถรับประกัน type ของข้อมูลที่ถูกเก็บปัจจุบันใน instance union คุณเรียนเพิ่มเกี่ยวกับ union ใน the Rust Reference ได้

ใช้ Miri เพื่อ Check โค้ด Unsafe

เมื่อเขียนโค้ด unsafe คุณอาจต้องการ check ว่าสิ่งที่คุณเขียนจริง ๆ คือ safe และถูก หนึ่งในวิธีดีที่สุดในการทำนั้นคือใช้ Miri ซึ่งคือเครื่อง มือ Rust official สำหรับการตรวจหา undefined behavior ขณะที่ borrow checker คือเครื่องมือ static ที่ทำงานที่ compile time, Miri คือ เครื่องมือ dynamic ที่ทำงานที่ runtime มัน check โค้ดของคุณโดยรัน โปรแกรมของคุณ หรือ test suite ของมัน และตรวจหาเมื่อคุณละเมิดกฎที่มัน เข้าใจเกี่ยวกับวิธีที่ Rust ควรทำงาน

ใช้ Miri ต้องการ nightly build ของ Rust (ซึ่งเราพูดมากขึ้นใน ภาคผนวก G — Rust ถูกสร้างยังไงและ “Nightly Rust”) คุณ ติดตั้งทั้ง version nightly ของ Rust และเครื่องมือ Miri ได้โดยพิมพ์ rustup +nightly component add miri นี่ไม่เปลี่ยน version ของ Rust ที่ project ของคุณใช้ — มันเพียงเพิ่มเครื่องมือให้ system ของคุณเพื่อให้ คุณใช้มันได้เมื่อคุณต้องการ คุณรัน Miri บน project ได้โดยพิมพ์ cargo +nightly miri run หรือ cargo +nightly miri test

สำหรับตัวอย่างของวิธีที่มีประโยชน์นี้ได้ พิจารณาสิ่งที่เกิดขึ้นเมื่อ เรารันมันกับ Listing 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.01s
     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
  = note: BACKTRACE:
  = note: inside `main` at src/main.rs:5:13: 5:32

error: Undefined Behavior: pointer not dereferenceable: pointer must be dereferenceable for 40000 bytes, but got 0x1234[noalloc] which is a dangling pointer (it 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: BACKTRACE:
  = note: inside `main` at src/main.rs:7:35: 7:70

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 เตือนเราอย่างถูกต้องว่าเรากำลัง cast integer เป็น pointer ซึ่ง อาจเป็นปัญหา แต่ Miri ไม่สามารถตัดสินว่าปัญหามีหรือไม่เพราะมันไม่รู้ว่า pointer มาจากที่ไหน แล้ว Miri return error ที่ Listing 20-7 มี undefined behavior เพราะเรามี dangling pointer ขอบคุณ Miri ตอนนี้เรา รู้มีความเสี่ยงของ undefined behavior และเราคิดเกี่ยวกับวิธีทำให้โค้ด safe ได้ ในบางกรณี Miri แม้ทำคำแนะนำเกี่ยวกับวิธี fix error ได้

Miri ไม่จับทุกอย่างที่คุณอาจได้ผิดเมื่อเขียนโค้ด unsafe Miri คือ เครื่องมือวิเคราะห์ dynamic ดังนั้นมันเพียงจับปัญหากับโค้ดที่ถูกรันจริง นั่นหมายความว่าคุณจะต้องใช้มันร่วมกับเทคนิค testing ที่ดีเพื่อเพิ่ม ความมั่นใจของคุณเกี่ยวกับโค้ด unsafe ที่คุณเขียน Miri ก็ไม่ครอบคลุม ทุกวิธีที่เป็นไปได้ที่โค้ดของคุณ unsound ได้

ในอีกคำพูด — ถ้า Miri จับ ปัญหา คุณรู้มี bug แต่เพียงเพราะ Miri ไม่ จับ bug ไม่ได้หมายความว่าไม่มีปัญหา มันจับเยอะได้ ลองรันมันบนตัวอย่าง อื่นของโค้ด unsafe ในบทนี้และดูว่ามันบอกอะไร!

คุณเรียนรู้เพิ่มเกี่ยวกับ Miri ที่ GitHub repository ของมัน ได้

ใช้โค้ด Unsafe อย่างถูกต้อง

ใช้ unsafe เพื่อใช้หนึ่งในห้า superpower ที่เพิ่งพูดถึงไม่ผิดหรือแม้ ถูกขมวดคิ้วใส่ แต่มันยุ่งยากกว่าที่จะได้โค้ด unsafe ถูกเพราะ compiler ไม่สามารถช่วยยึดถือ memory safety เมื่อคุณมีเหตุผลที่จะใช้โค้ด unsafe คุณทำเช่นนั้นได้ และมี annotation unsafe ที่ชัดเจนทำให้ง่ายขึ้นที่จะ ติดตามแหล่งของปัญหาเมื่อพวกมันเกิด เมื่อใดก็ตามที่คุณเขียนโค้ด unsafe คุณใช้ Miri เพื่อช่วยคุณให้มั่นใจมากขึ้นว่าโค้ดที่คุณเขียนยึดถือกฎของ Rust ได้

สำหรับการสำรวจที่ลึกกว่าเกี่ยวกับวิธีทำงานอย่างมีประสิทธิภาพกับ unsafe Rust อ่านคู่มือ official ของ Rust สำหรับ unsafe, The Rustonomicon