ch19-01-unsafe-rust.md
commit 1524fa89fbaa4d52c4a2095141f6eaa6c22f8bd0
目前為止討論過的代碼都有 Rust 在編譯時會強制執(zhí)行的內(nèi)存安全保證。然而,Rust 還隱藏有第二種語言,它不會強制執(zhí)行這類內(nèi)存安全保證:這被稱為 不安全 Rust(unsafe Rust)。它與常規(guī) Rust 代碼無異,但是會提供額外的超能力。
盡管代碼可能沒問題,但如果 Rust 編譯器沒有足夠的信息可以確定,它將拒絕代碼。
不安全 Rust 之所以存在,是因為靜態(tài)分析本質(zhì)上是保守的。當(dāng)編譯器嘗試確定一段代碼是否支持某個保證時,拒絕一些合法的程序比接受錯誤的程序要好一些。這必然意味著有時代碼 可能 是合法的,但如果 Rust 編譯器沒有足夠的信息來確定,它將拒絕該代碼。在這種情況下,可以使用不安全代碼告訴編譯器,“相信我,我知道我在干什么?!边@么做的缺點就是你只能靠自己了:如果不安全代碼出錯了,比如解引用空指針,可能會導(dǎo)致不安全的內(nèi)存使用。
另一個 Rust 存在不安全一面的原因是:底層計算機硬件固有的不安全性。如果 Rust 不允許進(jìn)行不安全操作,那么有些任務(wù)則根本完成不了。Rust 需要能夠進(jìn)行像直接與操作系統(tǒng)交互,甚至于編寫你自己的操作系統(tǒng)這樣的底層系統(tǒng)編程!這也是 Rust 語言的目標(biāo)之一。讓我們看看不安全 Rust 能做什么,和怎么做。
可以通過 unsafe
關(guān)鍵字來切換到不安全 Rust,接著可以開啟一個新的存放不安全代碼的塊。這里有五類可以在不安全 Rust 中進(jìn)行而不能用于安全 Rust 的操作,它們稱之為 “不安全的超能力?!?這些超能力是:
union
? 的字段有一點很重要,unsafe
并不會關(guān)閉借用檢查器或禁用任何其他 Rust 安全檢查:如果在不安全代碼中使用引用,它仍會被檢查。unsafe
關(guān)鍵字只是提供了那五個不會被編譯器檢查內(nèi)存安全的功能。你仍然能在不安全塊中獲得某種程度的安全。
再者,unsafe
不意味著塊中的代碼就一定是危險的或者必然導(dǎo)致內(nèi)存安全問題:其意圖在于作為程序員你將會確保 unsafe
塊中的代碼以有效的方式訪問內(nèi)存。
人是會犯錯誤的,錯誤總會發(fā)生,不過通過要求這五類操作必須位于標(biāo)記為 unsafe
的塊中,就能夠知道任何與內(nèi)存安全相關(guān)的錯誤必定位于 unsafe
塊內(nèi)。保持 unsafe
塊盡可能小,如此當(dāng)之后調(diào)查內(nèi)存 bug 時就會感謝你自己了。
為了盡可能隔離不安全代碼,將不安全代碼封裝進(jìn)一個安全的抽象并提供安全 API 是一個好主意,當(dāng)我們學(xué)習(xí)不安全函數(shù)和方法時會討論到。標(biāo)準(zhǔn)庫的一部分被實現(xiàn)為在被評審過的不安全代碼之上的安全抽象。這個技術(shù)防止了 unsafe
泄露到所有你或者用戶希望使用由 unsafe
代碼實現(xiàn)的功能的地方,因為使用其安全抽象是安全的。
讓我們按順序依次介紹上述五個超能力,同時我們會看到一些提供不安全代碼的安全接口的抽象。
回到第四章的 “懸垂引用” 部分,那里提到了編譯器會確保引用總是有效的。不安全 Rust 有兩個被稱為 裸指針(raw pointers)的類似于引用的新類型。和引用一樣,裸指針是不可變或可變的,分別寫作 *const T
和 *mut T
。這里的星號不是解引用運算符;它是類型名稱的一部分。在裸指針的上下文中,不可變 意味著指針解引用之后不能直接賦值。
裸指針與引用和智能指針的區(qū)別在于
通過去掉 Rust 強加的保證,你可以放棄安全保證以換取性能或使用另一個語言或硬件接口的能力,此時 Rust 的保證并不適用。
示例 19-1 展示了如何從引用同時創(chuàng)建不可變和可變裸指針。
let mut num = 5;
let r1 = &num as *const i32;
let r2 = &mut num as *mut i32;
示例 19-1: 通過引用創(chuàng)建裸指針
注意這里沒有引入 unsafe
關(guān)鍵字??梢栽诎踩a中 創(chuàng)建 裸指針,只是不能在不安全塊之外 解引用 裸指針,稍后便會看到。
這里使用 as
將不可變和可變引用強轉(zhuǎn)為對應(yīng)的裸指針類型。因為直接從保證安全的引用來創(chuàng)建他們,可以知道這些特定的裸指針是有效,但是不能對任何裸指針做出如此假設(shè)。
接下來會創(chuàng)建一個不能確定其有效性的裸指針,示例 19-2 展示了如何創(chuàng)建一個指向任意內(nèi)存地址的裸指針。嘗試使用任意內(nèi)存是未定義行為:此地址可能有數(shù)據(jù)也可能沒有,編譯器可能會優(yōu)化掉這個內(nèi)存訪問,或者程序可能會出現(xiàn)段錯誤(segmentation fault)。通常沒有好的理由編寫這樣的代碼,不過卻是可行的:
let address = 0x012345usize;
let r = address as *const i32;
示例 19-2: 創(chuàng)建指向任意內(nèi)存地址的裸指針
記得我們說過可以在安全代碼中創(chuàng)建裸指針,不過不能 解引用 裸指針和讀取其指向的數(shù)據(jù)?,F(xiàn)在我們要做的就是對裸指針使用解引用運算符 *
,這需要一個 unsafe
塊,如示例 19-3 所示:
let mut num = 5;
let r1 = &num as *const i32;
let r2 = &mut num as *mut i32;
unsafe {
println!("r1 is: {}", *r1);
println!("r2 is: {}", *r2);
}
示例 19-3: 在 unsafe
塊中解引用裸指針
創(chuàng)建一個指針不會造成任何危險;只有當(dāng)訪問其指向的值時才有可能遇到無效的值。
還需注意示例 19-1 和 19-3 中創(chuàng)建了同時指向相同內(nèi)存位置 num
的裸指針 *const i32
和 *mut i32
。相反如果嘗試同時創(chuàng)建 num
的不可變和可變引用,將無法通過編譯,因為 Rust 的所有權(quán)規(guī)則不允許在擁有任何不可變引用的同時再創(chuàng)建一個可變引用。通過裸指針,就能夠同時創(chuàng)建同一地址的可變指針和不可變指針,若通過可變指針修改數(shù)據(jù),則可能潛在造成數(shù)據(jù)競爭。請多加小心!
既然存在這么多的危險,為何還要使用裸指針呢?一個主要的應(yīng)用場景便是調(diào)用 C 代碼接口,這在下一部分 “調(diào)用不安全函數(shù)或方法” 中會講到。另一個場景是構(gòu)建借用檢查器無法理解的安全抽象。讓我們先介紹不安全函數(shù),接著看一看使用不安全代碼的安全抽象的例子。
第二類要求使用不安全塊的操作是調(diào)用不安全函數(shù)。不安全函數(shù)和方法與常規(guī)函數(shù)方法十分類似,除了其開頭有一個額外的 unsafe
。在此上下文中,關(guān)鍵字unsafe
表示該函數(shù)具有調(diào)用時需要滿足的要求,而 Rust 不會保證滿足這些要求。通過在 unsafe
塊中調(diào)用不安全函數(shù),表明我們已經(jīng)閱讀過此函數(shù)的文檔并對其是否滿足函數(shù)自身的契約負(fù)責(zé)。
如下是一個沒有做任何操作的不安全函數(shù) dangerous
的例子:
unsafe fn dangerous() {}
unsafe {
dangerous();
}
必須在一個單獨的 unsafe
塊中調(diào)用 dangerous
函數(shù)。如果嘗試不使用 unsafe
塊調(diào)用 dangerous
,則會得到一個錯誤:
$ cargo run
Compiling unsafe-example v0.1.0 (file:///projects/unsafe-example)
error[E0133]: call to unsafe function is unsafe and requires unsafe function or 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` due to previous error
通過將 dangerous
調(diào)用插入 unsafe
塊中,我們就向 Rust 保證了我們已經(jīng)閱讀過函數(shù)的文檔,理解如何正確使用,并驗證過其滿足函數(shù)的契約。
不安全函數(shù)體也是有效的 unsafe
塊,所以在不安全函數(shù)中進(jìn)行另一個不安全操作時無需新增額外的 unsafe
塊。
僅僅因為函數(shù)包含不安全代碼并不意味著整個函數(shù)都需要標(biāo)記為不安全的。事實上,將不安全代碼封裝進(jìn)安全函數(shù)是一個常見的抽象。作為一個例子,標(biāo)準(zhǔn)庫中的函數(shù),split_at_mut
,它需要一些不安全代碼,讓我們探索如何可以實現(xiàn)它。這個安全函數(shù)定義于可變 slice 之上:它獲取一個 slice 并從給定的索引參數(shù)開始將其分為兩個 slice。split_at_mut
的用法如示例 19-4 所示:
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]);
示例 19-4: 使用安全的 split_at_mut
函數(shù)
這個函數(shù)無法只通過安全 Rust 實現(xiàn)。一個嘗試可能看起來像示例 19-5,它不能編譯。出于簡單考慮,我們將 split_at_mut
實現(xiàn)為函數(shù)而不是方法,并只處理 i32
值而非泛型 T
的 slice。
fn split_at_mut(slice: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
let len = slice.len();
assert!(mid <= len);
(&mut slice[..mid], &mut slice[mid..])
}
示例 19-5: 嘗試只使用安全 Rust 來實現(xiàn) split_at_mut
此函數(shù)首先獲取 slice 的長度,然后通過檢查參數(shù)是否小于或等于這個長度來斷言參數(shù)所給定的索引位于 slice 當(dāng)中。該斷言意味著如果傳入的索引比要分割的 slice 的索引更大,此函數(shù)在嘗試使用這個索引前 panic。
之后我們在一個元組中返回兩個可變的 slice:一個從原始 slice 的開頭直到 mid
索引,另一個從 mid
直到原 slice 的結(jié)尾。
如果嘗試編譯示例 19-5 的代碼,會得到一個錯誤:
$ cargo run
Compiling unsafe-example v0.1.0 (file:///projects/unsafe-example)
error[E0499]: cannot borrow `*slice` as mutable more than once at a time
--> src/main.rs:6:30
|
1 | fn split_at_mut(slice: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
| - let's call the lifetime of this reference `'1`
...
6 | (&mut slice[..mid], &mut slice[mid..])
| -------------------------^^^^^--------
| | | |
| | | second mutable borrow occurs here
| | first mutable borrow occurs here
| returning this value requires that `*slice` is borrowed for `'1`
For more information about this error, try `rustc --explain E0499`.
error: could not compile `unsafe-example` due to previous error
Rust 的借用檢查器不能理解我們要借用這個 slice 的兩個不同部分:它只知道我們借用了同一個 slice 兩次。本質(zhì)上借用 slice 的不同部分是可以的,因為結(jié)果兩個 slice 不會重疊,不過 Rust 還沒有智能到能夠理解這些。當(dāng)我們知道某些事是可以的而 Rust 不知道的時候,就是觸及不安全代碼的時候了
示例 19-6 展示了如何使用 unsafe
塊,裸指針和一些不安全函數(shù)調(diào)用來實現(xiàn) split_at_mut
:
use std::slice;
fn split_at_mut(slice: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
let len = slice.len();
let ptr = slice.as_mut_ptr();
assert!(mid <= len);
unsafe {
(
slice::from_raw_parts_mut(ptr, mid),
slice::from_raw_parts_mut(ptr.add(mid), len - mid),
)
}
}
示例 19-6: 在 split_at_mut
函數(shù)的實現(xiàn)中使用不安全代碼
回憶第四章的 “Slice 類型” 部分,slice 是一個指向一些數(shù)據(jù)的指針,并帶有該 slice 的長度??梢允褂?nbsp;len
方法獲取 slice 的長度,使用 as_mut_ptr
方法訪問 slice 的裸指針。在這個例子中,因為有一個 i32
值的可變
slice,as_mut_ptr
返回一個 *mut i32
類型的裸指針,儲存在 ptr
變量中。
我們保持索引 mid
位于 slice 中的斷言。接著是不安全代碼:slice::from_raw_parts_mut
函數(shù)獲取一個裸指針和一個長度來創(chuàng)建一個 slice。這里使用此函數(shù)從 ptr
中創(chuàng)建了一個有 mid
個項的 slice。之后在 ptr
上調(diào)用 add
方法并使用 mid
作為參數(shù)來獲取一個從 mid
開始的裸指針,使用這個裸指針并以 mid
之后項的數(shù)量為長度創(chuàng)建一個
slice。
slice::from_raw_parts_mut
函數(shù)是不安全的因為它獲取一個裸指針,并必須確信這個指針是有效的。裸指針上的 add
方法也是不安全的,因為其必須確信此地址偏移量也是有效的指針。因此必須將 slice::from_raw_parts_mut
和 add
放入 unsafe
塊中以便能調(diào)用它們。通過觀察代碼,和增加 mid
必然小于等于 len
的斷言,我們可以說 unsafe
塊中所有的裸指針將是有效的
slice 中數(shù)據(jù)的指針。這是一個可以接受的 unsafe
的恰當(dāng)用法。
注意無需將 split_at_mut
函數(shù)的結(jié)果標(biāo)記為 unsafe
,并可以在安全 Rust 中調(diào)用此函數(shù)。我們創(chuàng)建了一個不安全代碼的安全抽象,其代碼以一種安全的方式使用了 unsafe
代碼,因為其只從這個函數(shù)訪問的數(shù)據(jù)中創(chuàng)建了有效的指針。
與此相對,示例 19-7 中的 slice::from_raw_parts_mut
在使用 slice 時很有可能會崩潰。這段代碼獲取任意內(nèi)存地址并創(chuàng)建了一個長為一萬的 slice:
use std::slice;
let address = 0x01234usize;
let r = address as *mut i32;
let slice: &[i32] = unsafe { slice::from_raw_parts_mut(r, 10000) };
示例 19-7: 通過任意內(nèi)存地址創(chuàng)建 slice
我們并不擁有這個任意地址的內(nèi)存,也不能保證這段代碼創(chuàng)建的 slice 包含有效的 i32
值。試圖使用臆測為有效的 slice
會導(dǎo)致未定義的行為。
有時你的 Rust 代碼可能需要與其他語言編寫的代碼交互。為此 Rust 有一個關(guān)鍵字,extern
,有助于創(chuàng)建和使用 外部函數(shù)接口(Foreign Function Interface, FFI)。外部函數(shù)接口是一個編程語言用以定義函數(shù)的方式,其允許不同(外部)編程語言調(diào)用這些函數(shù)。
示例 19-8 展示了如何集成 C 標(biāo)準(zhǔn)庫中的 abs
函數(shù)。extern
塊中聲明的函數(shù)在 Rust 代碼中總是不安全的。因為其他語言不會強制執(zhí)行 Rust 的規(guī)則且 Rust 無法檢查它們,所以確保其安全是程序員的責(zé)任:
文件名: src/main.rs
extern "C" {
fn abs(input: i32) -> i32;
}
fn main() {
unsafe {
println!("Absolute value of -3 according to C: {}", abs(-3));
}
}
示例 19-8: 聲明并調(diào)用另一個語言中定義的 extern
函數(shù)
在 extern "C"
塊中,列出了我們希望能夠調(diào)用的另一個語言中的外部函數(shù)的簽名和名稱。"C"
部分定義了外部函數(shù)所使用的 應(yīng)用二進(jìn)制接口(application binary interface,ABI) —— ABI 定義了如何在匯編語言層面調(diào)用此函數(shù)。"C"
ABI 是最常見的,并遵循 C 編程語言的 ABI。
從其它語言調(diào)用 Rust 函數(shù)
也可以使用
extern
來創(chuàng)建一個允許其他語言調(diào)用 Rust 函數(shù)的接口。不同于extern
塊,就在fn
關(guān)鍵字之前增加extern
關(guān)鍵字并指定所用到的 ABI。還需增加#[no_mangle]
注解來告訴 Rust 編譯器不要 mangle 此函數(shù)的名稱。Mangling 發(fā)生于當(dāng)編譯器將我們指定的函數(shù)名修改為不同的名稱時,這會增加用于其他編譯過程的額外信息,不過會使其名稱更難以閱讀。每一個編程語言的編譯器都會以稍微不同的方式 mangle 函數(shù)名,所以為了使 Rust 函數(shù)能在其他語言中指定,必須禁用 Rust 編譯器的 name mangling。
在如下的例子中,一旦其編譯為動態(tài)庫并從 C 語言中鏈接,
call_from_c
函數(shù)就能夠在 C 代碼中訪問:
#[no_mangle] pub extern "C" fn call_from_c() { println!("Just called a Rust function from C!"); }
extern
的使用無需unsafe
。
目前為止全書都盡量避免討論 全局變量(global variables),Rust 確實支持他們,不過這對于 Rust 的所有權(quán)規(guī)則來說是有問題的。如果有兩個線程訪問相同的可變?nèi)肿兞?,則可能會造成數(shù)據(jù)競爭。
全局變量在 Rust 中被稱為 靜態(tài)(static)變量。示例 19-9 展示了一個擁有字符串 slice 值的靜態(tài)變量的聲明和應(yīng)用:
文件名: src/main.rs
static HELLO_WORLD: &str = "Hello, world!";
fn main() {
println!("name is: {}", HELLO_WORLD);
}
示例 19-9: 定義和使用一個不可變靜態(tài)變量
靜態(tài)(static
)變量類似于第三章 “變量和常量的區(qū)別” 部分討論的常量。通常靜態(tài)變量的名稱采用 SCREAMING_SNAKE_CASE
寫法。靜態(tài)變量只能儲存擁有 'static
生命周期的引用,這意味著 Rust 編譯器可以自己計算出其生命周期而無需顯式標(biāo)注。訪問不可變靜態(tài)變量是安全的。
常量與不可變靜態(tài)變量可能看起來很類似,不過一個微妙的區(qū)別是靜態(tài)變量中的值有一個固定的內(nèi)存地址。使用這個值總是會訪問相同的地址。另一方面,常量則允許在任何被用到的時候復(fù)制其數(shù)據(jù)。
常量與靜態(tài)變量的另一個區(qū)別在于靜態(tài)變量可以是可變的。訪問和修改可變靜態(tài)變量都是 不安全 的。示例 19-10 展示了如何聲明、訪問和修改名為 COUNTER
的可變靜態(tài)變量:
文件名: src/main.rs
static mut COUNTER: u32 = 0;
fn add_to_count(inc: u32) {
unsafe {
COUNTER += inc;
}
}
fn main() {
add_to_count(3);
unsafe {
println!("COUNTER: {}", COUNTER);
}
}
示例 19-10: 讀取或修改一個可變靜態(tài)變量是不安全的
就像常規(guī)變量一樣,我們使用 mut
關(guān)鍵來指定可變性。任何讀寫 COUNTER
的代碼都必須位于 unsafe
塊中。這段代碼可以編譯并如期打印出 COUNTER: 3
,因為這是單線程的。擁有多個線程訪問 COUNTER
則可能導(dǎo)致數(shù)據(jù)競爭。
擁有可以全局訪問的可變數(shù)據(jù),難以保證不存在數(shù)據(jù)競爭,這就是為何 Rust 認(rèn)為可變靜態(tài)變量是不安全的。任何可能的情況,請優(yōu)先使用第十六章討論的并發(fā)技術(shù)和線程安全智能指針,這樣編譯器就能檢測不同線程間的數(shù)據(jù)訪問是否是安全的。
unsafe
的另一個操作用例是實現(xiàn)不安全 trait。當(dāng) trait 中至少有一個方法中包含編譯器無法驗證的不變式(invariant)時 trait 是不安全的??梢栽?nbsp;trait
之前增加 unsafe
關(guān)鍵字將 trait 聲明為 unsafe
,同時 trait 的實現(xiàn)也必須標(biāo)記為 unsafe
,如示例 19-11 所示:
unsafe trait Foo {
// methods go here
}
unsafe impl Foo for i32 {
// method implementations go here
}
fn main() {}
示例 19-11: 定義并實現(xiàn)不安全 trait
通過 unsafe impl
,我們承諾將保證編譯器所不能驗證的不變量。
作為一個例子,回憶第十六章 “使用 Sync 和 Send trait 的可擴展并發(fā)” 部分中的 Sync
和 Send
標(biāo)記 trait,編譯器會自動為完全由 Send
和 Sync
類型組成的類型自動實現(xiàn)他們。如果實現(xiàn)了一個包含一些不是 Send
或 Sync
的類型,比如裸指針,并希望將此類型標(biāo)記為 Send
或 Sync
,則必須使用 unsafe
。Rust 不能驗證我們的類型保證可以安全的跨線程發(fā)送或在多線程間訪問,所以需要我們自己進(jìn)行檢查并通過 unsafe
表明。
僅適用于 unsafe
的最后一個操作是訪問 聯(lián)合體 中的字段,union
和 struct
類似,但是在一個實例中同時只能使用一個聲明的字段。聯(lián)合體主要用于和 C 代碼中的聯(lián)合體交互。訪問聯(lián)合體的字段是不安全的,因為 Rust 無法保證當(dāng)前存儲在聯(lián)合體實例中數(shù)據(jù)的類型??梢圆榭?nbsp;參考文檔 了解有關(guān)聯(lián)合體的更多信息。
使用 unsafe
來進(jìn)行這五個操作(超能力)之一是沒有問題的,甚至是不需要深思熟慮的,不過使得 unsafe
代碼正確也實屬不易,因為編譯器不能幫助保證內(nèi)存安全。當(dāng)有理由使用 unsafe
代碼時,是可以這么做的,通過使用顯式的 unsafe
標(biāo)注可以更容易地在錯誤發(fā)生時追蹤問題的源頭。
更多建議: