Rust入坑指南:居安思危


任何事情都是相對的,就像Rust給我們的印象一直是安全、快速,但實際上,完全的安全是不可能實現的。因此,Rust中也是會有不安全的代碼的。

嚴格來講,Rust語言可以分為Safe RustUnsafe 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
  • 讀寫聯合體中的字段


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM