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 เหล่านั้น
รวมถึงความสามารถใน:
- Dereference raw pointer
- เรียกฟังก์ชันหรือเมธอด unsafe
- เข้าถึงหรือแก้ตัวแปร static แบบ mutable
- Implement trait unsafe
- เข้าถึง 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;
}
สังเกตว่าเราไม่รวมคีย์เวิร์ด 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;
}
จำว่าเราสร้าง 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);
}
}
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]);
}
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);
}
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);
}
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) };
}
เราไม่เป็นเจ้าของ 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
unsafe extern "C" {
fn abs(input: i32) -> i32;
}
fn main() {
unsafe {
println!("Absolute value of -3 according to C: {}", abs(-3));
}
}
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
unsafe extern "C" {
safe fn abs(input: i32) -> i32;
}
fn main() {
println!("Absolute value of -3 according to C: {}", abs(-3));
}
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 เป็นค่า
static HELLO_WORLD: &str = "Hello, world!";
fn main() {
println!("value is: {HELLO_WORLD}");
}
ตัวแปร 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
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));
}
}
เช่นเดียวกับตัวแปรปกติ เราระบุ 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() {}
โดยใช้ 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