任何事情都是相對的,就像Rust給我們的印象一直是安全、快速,但實際上,完全的安全是不可能實現的。因此,Rust中也是會有不安全的代碼的。
嚴格來講,Rust語言可以分為Safe Rust和Unsafe Rust。Unsafe Rust是Safe Rust的超集。在Unsafe Rust中並不會禁用任何的安全檢查,Unsafe Rust出現的原因是為了讓開發者可以做一些更加底層的操作。這些事情本身也是不安全的,如果仍然要進行Rust的安全檢查,那么就無法進行這些操作。
在進行下面這5種操作時,Unsafe Rust不會進行安全檢查。
- 解引用原生指針
- 調用unsafe的函數或方法
- 訪問或修改可變的靜態變量
- 實現unsafe的trait
- 讀寫聯合體中的字段
基礎語法
Unsafe Rust的關鍵字是unsafe,它可以用來修飾函數、方法和trait,也可以用來標記代碼塊。
標准庫中也有不少函數是unsafe的。例如String中的from_utf8_unchecked()
函數。它的定義如下:
pub unsafe fn from_utf8_unchecked(bytes: Vec<u8>) -> String {
String { vec: bytes }
}
這個函數被標記為unsafe的原因是函數並沒有檢查傳入參數是否是合法的UTF-8序列。也就是提醒使用者注意,使用這個函數要自己保證參數的合法性。
用unsafe標記的trait也比較常見,在前面我們見過的Send和Sync都是unsafe的trait。它們被用來保證線程安全, 將其標記為unsafe是告訴開發者,如果自己實現這兩個trait,那么代碼就會有安全風險。
我們在調用unsafe函數或方法時,需要使用unsafe代碼塊。
fn main() {
let sparkle_heart = vec![240, 159, 146, 150];
let sparkle_heart = unsafe {
String::from_utf8_unchecked(sparkle_heart)
};
assert_eq!("💖", sparkle_heart);
}
在了解了unsafe的基礎語法之后,我們再來具體看看前面提到的5種操作。
解引用原生指針
Rust的原生指針分為兩種:可變類型*mut T
和不可變類型*const T
。
與引用和智能指針不同,原生指針具有以下特性:
- 可以不遵循借用規則,在同一代碼塊中可以同時出現可變和不可變指針,也可以同時有多個可變指針
- 不保證指向有效內存
- 允許是null
- 不會自動清理內存
由這些特性可以看出,原生指針並不受Rust那一套安全規則的限制,因此,解引用原生指針是一種不安全的操作。換句話說,我們應該把這種操作放在unsafe代碼塊中。下面這段代碼就展示了原生指針的第一條特性,以及如何解引用原生指針。
fn main() {
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);
}
}
在Rust編程中,原生指針常被用作和C語言打交道,原生指針有一些特有的方法,例如可以用is_null()
來判斷原生指針是否是空指針,用offset()
來獲取指定偏移量的內存地址的內容,使用read()/write()
方法來讀寫內存等。
調用unsafe的函數或方法
調用unsafe的函數或方法必須放到unsafe代碼塊中,這點我們在基礎知識中已經介紹過。因為函數本身被標記為unsafe,也就意味着調用它可能存在風險。這點無需贅述。
訪問或修改可變的靜態變量
對於不可變的靜態變量,我們訪問它不會存在任何安全問題,但是對於可變的靜態變量而言,如果我們在多線程中都訪問同一個變量,那么就會造成數據競爭。這當然也是一種不安全的操作。所以要放到unsafe代碼塊中,此時線程安全應由開發者自己來保證。
static mut COUNTER: u32 = 0;
fn add_to_count(inc: u32) {
unsafe {
COUNTER += inc;
}
}
fn main() {
add_to_count(3);
unsafe {
println!("COUNTER: {}", COUNTER);
}
}
在這個例子中我們沒有使用多線程,這里只是想展示一下如何訪問和修改可變靜態變量。
實現unsafe的trait
當trait中包含一個或多個編譯器無法驗證其安全性的方法時,這個trait就必須被標記為unsafe。而想要實現unsafe的trait,首先在實現代碼塊的關鍵字impl
前也要加上unsafe標記。其次,無法被編譯器驗證安全性的方法,其安全性必須由開發者自己來保證。
前面我們也提到了,常見的unsafe的trait有Send和Sync這兩個。
讀寫聯合體中的字段
Rust中的Union聯合體和Enum相似。我們可以使用union關鍵字來定義一個聯合體。
union MyUnion {
i: i32,
f: f32,
}
fn main() {
let my_union = MyUnion{i: 3};
unsafe {
println!("{}", my_union.i);
}
}
在初始化時,我們每次只能指定一個字段的值。這就造成我們在訪問聯合體中的字段時,有可能會訪問到未定義的字段。因此,Rust讓我們把訪問操作放到unsafe代碼塊中,以此來警示我們必須自己保證程序的安全性。
總結
本文我們聊了Unsafe Rust的一些使用場景和使用方法。你只需要記住Unsafe的5種操作就好,在遇到這些操作時,一定要使用unsafe代碼塊。unsafe代碼塊不光是為了“騙”過編譯器,要時刻提醒自己,unsafe代碼塊中的程序要由開發者自己保證其正確性。
- 解引用原生指針
- 調用unsafe的函數或方法
- 訪問或修改可變的靜態變量
- 實現unsafe的trait
- 讀寫聯合體中的字段