介紹#
眾所周知,Rust 語言由兩個主要部分組成:安全 Rust 和不安全 Rust。不安全 Rust 是安全 Rust 的超集。這意味著所有用安全 Rust 編寫的代碼也可以在不安全 Rust 中正常工作,但不安全 Rust 提供了額外的功能和操作,這些功能和操作不能直接在安全 Rust 中使用。
然而,社區中最常被問到的問題之一是:為什麼在標準庫中使用大量不安全 Rust 的情況下,Rust 被認為是一種安全的語言?
此外,社區中的一些人甚至對不安全 Rust 產生了創傷後壓力症,認為任何涉及不安全的東西都是不安全的。誠然,我們應該最小化不安全的使用,但在面對不安全不可或缺的情況時,我們應該知道如何安全地使用不安全,知道如何審查不安全代碼,因此在看到不安全時不必如此害怕和抵觸。
因此,本文的目標是引導讀者系統地理解不安全 Rust,真正掌握不安全 Rust 的巧妙用法。
目錄:
- 為什麼需要不安全 Rust
- 不安全 Rust 可以做什麼
- 不安全 Rust 的安全哲學
- 安全抽象:維護安全不變量和有效性不變量
- 來自標準庫的案例研究
- 來自 FFI 的示例
- 不安全 Rust 編程指南
- 熱點討論:不安全 Rust 與 Zig
- Zig 語言介紹
- Zig 真的是比不安全 Rust 更安全嗎?
- 總結
為什麼需要不安全 Rust#
一般來說,安全 Rust 是從不安全 Rust 中抽象出來的。世界本身是不安全的,因此不安全先於安全。這是 Rust 語言的基本世界觀。
這種世界觀實際上與客觀世界非常一致,並且非常容易讓人理解。宇宙是不安全的,因此我們需要安全的太空船和太空服來探索它;地球是不安全的,因此我們人類利用科學和文明不斷創造安全的家園。
從語言設計的角度來看,需要不安全 Rust 的原因如下:
- 為高級抽象提供低級支持:Rust 提供了一些高級抽象,例如引用計數、智能指針、同步原語等。這些抽象需要在低級別進行不安全操作,例如原始指針操作和內存管理。通過不安全 Rust,庫開發者可以實現這些低級操作並將其封裝在安全接口中。
- 高性能優化:為了確保內存安全,Rust 編譯器自動插入一些運行時檢查,例如數組邊界檢查。在某些性能關鍵的場景中,這些檢查可能成為性能瓶頸。不安全 Rust 允許開發者在確保安全的情況下繞過這些檢查,以實現更高的性能。
- 系統級編程:作為一種系統編程語言,Rust 需要處理低級操作,例如操作系統或嵌入式系統,或驅動程序開發。不安全 Rust 提供了這種靈活性,使 Rust 適合這些任務。
- 與其他語言的互操作性:在現實世界的項目中,Rust 代碼可能需要與用其他語言(如 C/C++)編寫的代碼進行交互。由於這些語言可能使用不同的內存管理和類型系統,Rust 需要提供一種安全地與這些外部代碼通信的方法。不安全 Rust 提供了這樣的機制,允許在 Rust 中處理原始指針和類型轉換,從而實現與其他語言的互操作性。
- 語言擴展性:不安全 Rust 為未來的語言特性和庫提供了可能性。通過允許低級操作,不安全 Rust 使 Rust 社區能夠不斷探索新想法並將其整合到語言中。
總之,從 Rust 語言設計的角度來看,不安全 Rust 是一種有意的設計妥協。它滿足了低級操作、性能優化和互操作性的需求,同時通過嚴格的限制和封裝確保整體安全。
不安全 Rust 可以做什麼#
如前所述,不安全 Rust 是安全 Rust 的超集。因此,不安全 Rust 也包括安全 Rust 中的所有編譯器安全檢查。然而,不安全 Rust 還包括安全 Rust 中不存在的操作,即只能在不安全 Rust 中執行的操作:
- 原始指針操作:您可以創建、解引用和操作原始指針(
*const T
和*mut T
)。這使您可以直接訪問內存地址,執行內存分配、釋放和修改等操作。 - 調用不安全函數:不安全 Rust 可以調用標記為
unsafe
的函數。這些函數可能導致未定義行為,因此需要在unsafe
代碼塊內調用。這些函數通常用於實現低級操作,例如內存管理、硬件訪問等。 - 實現不安全特徵:您可以實現標記為
unsafe
的特徵。這些特徵可能包含潛在的風險操作,並且在實現時需要明確標記為unsafe
。 - 訪問和修改可變靜態變量:在不安全 Rust 中,您可以訪問和修改具有全局生命週期的可變靜態變量。這些變量在整個程序執行過程中保持活動,可能導致潛在的數據競爭問題。
- 與
Union
類型一起工作。由於多個字段共享相同的內存位置,使用union
會帶來一定的風險。當訪問union
字段時,編譯器無法保證類型安全,因為它無法確定當前存儲的值屬於哪個字段。為了確保安全訪問union
字段,您需要在unsafe
代碼塊內執行操作。 - 禁用運行時邊界檢查:不安全 Rust 允許您繞過數組邊界檢查。通過使用
get_unchecked
和get_unchecked_mut
方法,您可以在不執行邊界檢查的情況下訪問數組和切片元素,從而提高性能。 - 行內組合語言:在不安全 Rust 中,您可以使用行內組合語言(
asm!
宏)直接編寫處理器指令。這使您能夠實現特定於平台的優化和操作。 - 外部函數接口(FFI):不安全 Rust 允許您與用其他編程語言(如 C/C++)編寫的代碼進行交互。這通常涉及本機指針操作、類型轉換和調用不安全函數。
需要注意的是,使用不安全 Rust 時需要謹慎。盡可能偏好使用安全 Rust 來編寫代碼。雖然它提供了強大的功能,但也可能導致未定義行為和內存安全問題。因此,Rust 的官方標準庫源代碼實現和官方 不安全代碼指導方針 都包含不安全 Rust 的安全哲學,以維護其安全性。
不安全 Rust 的安全哲學#
不安全 Rust 的安全哲學是允許開發者在受限條件下執行低級操作和性能優化,同時確保整體代碼保持安全。
安全抽象:維護安全不變量和有效性不變量#
整體代碼保持安全意味著什麼?不安全 Rust 有一個術語叫做安全不變量專門用於定義這一點。
安全 Rust 具有編譯器安全檢查,以確保內存安全和並發安全,但對於不安全 Rust 的那些專門操作場景,Rust 編譯器無法提供幫助,因此開發者需要自己確保代碼的內存安全和並發安全。不安全 Rust 是開發者對 Rust 編譯器的安全承諾:“讓我來保護安全!”
為了遵守這一安全承諾,在編寫不安全 Rust 代碼時,開發者必須始終維護安全不變量。安全不變量是指必須在整個程序執行過程中保持的條件,以確保內存安全。這些條件通常包括指針有效性、數據結構完整性、數據訪問同步等。安全不變量主要關注程序的正確執行和避免未定義行為。在使用不安全 Rust 時,開發者負責確保滿足這些安全不變量,以避免內存安全問題。
在維護安全不變量的過程中,另一個需要理解的概念是有效性不變量。有效性不變量是指某些數據類型和結構在其生命週期內必須滿足的條件。有效性不變量主要關注數據類型和結構的正確性。例如,對於引用類型,有效性不變量包括非空指針和有效內存。編寫和使用不安全 Rust 代碼時,開發者需要確保這些有效性不變量得到維護。
安全不變量和有效性不變量之間存在一定程度的關聯,因為它們都關注代碼的正確性和安全性。維護有效性不變量通常有助於確保安全不變量的滿足。例如,確保引用類型指針的有效性(有效性不變量)可以防止空指針解引用(安全不變量)。儘管它們是相關的,但安全不變量和有效性不變量的關注領域是不同的。安全不變量主要關注整個程序的內存安全和避免未定義行為,而有效性不變量主要關注特定數據類型和結構的正確性。
它們之間的關係可以總結為以下幾個方面:
- 目的:安全不變量主要關注內存安全和數據完整性,以防止未定義行為。有效性不變量關注類型實例在其生命週期內必須滿足的條件,以便正確使用。
- 範圍:安全不變量通常涉及整個程序或模塊的內存安全,而有效性不變量則特定於類型約束。在某種程度上,安全不變量可以被視為全局限制,而有效性不變量則是局部限制。
- 層次:安全不變量和有效性不變量可以在不同層次上互動。通常,維護有效性不變量是實現安全不變量的基礎。換句話說,一種類型的有效性不變量通常是實現更高級安全不變量所需的。
- 依賴性:安全不變量依賴於有效性不變量。當一種類型的有效性不變量得到滿足時,有助於確保實現安全不變量所需的條件。例如,在 Rust 中,安全引用訪問依賴於底層類型的有效性不變量得到滿足。
因此,當不安全 Rust 代碼違反安全不變量或有效性不變量時,我們稱該代碼為不安全。不安全代碼可能導致未定義行為、內存泄漏、數據競爭等問題。Rust 通過編譯器和類型系統確保大多數代碼的內存安全,特別是避免未定義行為,來嘗試避免不安全的情況。
了解更多:兩種不變量:安全和有效性
未定義行為是指程序執行結果不可預測的情況。這可能是由於越界內存訪問、空指針解引用、數據競爭和其他錯誤造成的。當遇到未定義行為時,程序可能崩潰、產生錯誤結果或表現出其他意外行為。
在 Rust 中,特別注意可能導致未定義行為的情況:
- 數據競爭:當多個線程同時訪問同一內存位置,並且至少有一個線程執行寫操作時,就會發生數據競爭。Rust 的所有權系統和借用檢查器在編譯時防止大多數數據競爭,但在不安全 Rust 中,開發者需要格外小心以避免數據競爭。
- 無效指針解引用:解引用無效指針(例如,空指針、指向已釋放內存的指針)會導致未定義行為。
- 整數溢出:在某些情況下,整數溢出(例如,整數加法、減法、乘法等)可能導致意外的程序情況。Rust 在調試模式下默認啟用整數溢出檢查,但在釋放模式下,整數溢出行為是定義的,見 RFC 0560。
- 訪問未初始化內存:訪問未初始化內存會導致未定義行為。這包括讀取或寫入未初始化內存或將未初始化內存傳遞給外部函數。
- 錯誤的類型轉換:強制將一種類型的指針轉換為另一種類型的指針然後解引用可能導致未定義行為。這通常發生在不安全 Rust 代碼中,需要特別注意以確保類型轉換是安全的。
總之,在使用不安全 Rust 時,開發者需要特別注意維護安全不變量和有效性不變量,以避免不安全和未定義行為。我們稱嚴格遵循這些原則的不安全 Rust 代碼為不安全安全抽象。它們可以被視為安全的,允許進行低級操作和性能優化,同時保持整體安全。
來自標準庫的示例#
Vec<T>
#
標準庫中 Vec<T>
類型的 push
方法是一個典型的不安全安全抽象示例。以下代碼是此方法的簡化實現:
pub struct Vec<T> {
ptr: *mut T,
len: usize,
cap: usize,
}
impl<T> Vec<T> {
pub fn push(&mut self, value: T) {
if self.len == self.cap {
// 重新分配內存(此處省略詳細實現)
self.reallocate();
}
// 確保安全不變量和有效性不變量在此處得到滿足
unsafe {
let end = self.ptr.add(self.len); // end 是有效指針,因為 len < cap
std::ptr::write(end, value); // 將值寫入 end 指向的內存,假設 end 是有效的
self.len += 1; // 更新長度
}
}
}
在這個簡化版本的 Vec<T>
實現中,push
方法使用了不安全 Rust。現在讓我們分析如何滿足安全不變量和有效性不變量:
- 安全不變量:
- 內存分配和釋放:當長度等於容量時,
push
方法調用reallocate
方法(此處省略詳細實現),確保分配足夠的內存。這確保不會發生越界內存訪問。 - 數據訪問同步:在這個例子中,
push
方法是一個可變引用(&mut self
),確保在調用時沒有其他可變或不可變引用。這確保不會發生數據競爭。
- 內存分配和釋放:當長度等於容量時,
- 有效性不變量:
- 指針有效性:
end
指針是通過self.ptr.add(self.len)
計算的。由於self.len < self.cap
(在reallocate
之後),我們可以確保end
指向的內存地址是有效的。 - 數據類型正確性:
std::ptr::write(end, value)
將value
寫入end
指向的內存。由於我們已經確保了end
的有效性,因此這個操作是安全的,並確保了數據類型的正確性。
- 指針有效性:
通過維護安全不變量和有效性不變量,Rust 標準庫中的 Vec<T>
類型可以提供高性能操作,同時確保內存安全。在這個簡化版本的 push
方法中,使用了不安全 Rust 來執行低級內存操作,但在整個過程中,開發者確保滿足安全不變量和有效性不變量。
總之,Rust 標準庫使用不安全 Rust 進行低級操作和優化,同時確保安全不變量和有效性不變量得到滿足,實現安全和高性能的抽象。這種方法也應用於許多其他標準庫類型和方法,例如 String
、HashMap
等。這使得開發者能夠利用低級操作和性能優化,同時編寫安全的高級代碼。
String
#
在標準庫的 String
類型中,有一對類似的方法:from_utf8
和 from_utf8_unchecked
。它們的區別在於前者是一個安全函數,而後者是一個不安全函數。
pub fn from_utf8(vec: Vec<u8>) -> Result<String, FromUtf8Error> {
// 這不是標準庫的源代碼,而是一個演示
let vec = match run_utf8_validation(v) {
Ok(_) => {
// 安全:驗證成功。
Ok(unsafe { from_utf8_unchecked(v) })
}
Err(err) => Err(err),
};
Ok(String { vec })
}
在 from_utf8
代碼中,輸入的字節序列經過 UTF8 編碼檢查,以確保 vec 參數的數據有效性。因此,當處理任意字節序列時,整個函數不會表現出不安全行為,特別是沒有未定義行為。在這一點上,我們可以認為它維護了安全不變量。然後,該函數是安全的。
from_utf8_unchecked
源代碼示例:
/// # 安全
///
/// 此函數是不安全的,因為它不檢查傳遞給它的字節是否是有效的 UTF-8。如果違反了這一約束,則可能會導致未來使用 `String` 的用戶出現內存不安全問題,因為標準庫的其餘部分假設 `String` 是有效的 UTF-8。
pub unsafe fn from_utf8_unchecked(bytes: Vec<u8>) -> String {
String { vec: bytes }
}
在這個函數中,輸入的字節序列不會進行 UTF8 編碼檢查,而是直接構造為 String,這是有風險的。由於無法保證 bytes
參數的數據有效性,因此整個函數是 unsafe
的,因此需要標記為 unsafe fn
。還應添加一個 安全性
文檔註釋,以告知使用此函數的開發者在什麼情況下使用是安全的,即讓調用者維護安全不變量。
一些讀者可能會想知道,為什麼我們需要一個 from_utf8_unchecked
函數,而已經有一個 from_utf8
函數?
這是在不安全 Rust 實踐中的一種慣例,提供一個帶有 _unchecked
名稱後綴的 unsafe
函數作為性能出口。例如,在某些環境中,from_utf8_unchecked
函數的 bytes
參數在外部(例如,在 C 接口中)進行了 UTF8 編碼驗證,因此在這裡不需要再次驗證。出於性能原因,可以使用 _unchecked
方法。
來自 FFI 的示例#
不安全 Rust 的一個常見場景是通過 C-ABI 與各種其他語言進行交互,即 FFI(外部函數接口)場景。在這種情況下,我們需要考慮許多安全因素,以實現不安全安全抽象。讓我們用一個示例來演示如何在 FFI 中實現安全抽象。
假設我們有一個 C 語言庫(my_c_lib.c
):
#include <stdint.h>
int32_t add(int32_t a, int32_t b) {
return a + b;
}
我們需要編寫一個 Rust 程序來調用這個 add
函數。首先,我們需要在 Rust 中創建一個 FFI 綁定:
// my_c_lib.rs
extern "C" {
fn add(a: i32, b: i32) -> i32;
}
現在,我們可以創建一個安全的 Rust 函數來包裝這個不安全的 FFI 綁定:
// my_c_lib.rs
pub fn safe_add(a: i32, b: i32) -> i32 {
// 開發者負責維護安全不變量和有效性不變量
// 如果 a 或 b 不是有效的 i32,則返回 0(根據具體業務選擇)
if a >= i32::MAX || b >= i32::MAX || (i32::MAX - a - b) < 0 {return 0}
unsafe {
add(a, b);
}
}
在這個示例中,我們將不安全的 FFI 綁定 add
函數包裝在一個安全的 safe_add
函數中。這樣,當其他 Rust 代碼調用 safe_add
時,他們不必擔心潛在的安全問題(例如,整數溢出)。不安全 Rust 代碼僅限於 safe_add
函數的內部,開發者負責確保滿足安全不變量和有效性不變量。
現在我們可以安全地在 Rust 中使用 safe_add
函數:
fn main() {
let result = my_c_lib::safe_add(5, 7);
println!("5 + 7 = {}", result);
}
在這個 FFI 場景中,不安全 Rust 用於調用在 C 中實現的 add
函數。通過將不安全代碼封裝在安全 API 中,我們確保了 Rust 代碼在調用該 API 時的安全性。在實踐中,開發者需要注意潛在的安全不變量和有效性不變量問題,並確保在封裝不安全 Rust 時滿足這些要求。
不安全 Rust 編程指南#
遵循這些不安全 Rust 實踐中的指南可以有效地實現不安全安全抽象:
- 最小化不安全代碼:不安全 Rust 的使用應限於必要時。大多數功能應使用安全 Rust 實現,以確保內存安全並避免未定義行為。盡可能將不安全 Rust 代碼封裝在安全 API 中,以為用戶提供安全接口。
- 明確標記為不安全:不安全 Rust 必須明確標記為
unsafe
,以便開發者能夠清楚地識別潛在的安全風險。當遇到unsafe
關鍵字時,開發者應特別注意,仔細審查代碼,以確保妥善處理潛在的內存安全問題和未定義行為。開發者需要仔細考慮函數或特徵是否應標記為unsafe
,並且不應故意刪除或不使用應標記為unsafe
的標記。 - 審查和測試:不安全 Rust 代碼需要更嚴格的審查和測試,以確保其正確性。這包括測試低級操作、內存分配和釋放以及並發行為。確保不安全 Rust 代碼的正確性至關重要,因為錯誤可能導致嚴重的內存安全問題和未定義行為。
- 文檔和註釋:不安全 Rust 代碼應有良好的文檔和註釋,以便其他開發者能夠理解其目的、操作和潛在風險。這有助於維護者和其他貢獻者在修改代碼時遵循正確的安全實踐。
- 減少複雜性:不安全 Rust 代碼應盡可能簡單明了,以便於理解和維護。複雜的不安全 Rust 代碼可能導致難以發現的錯誤,增加潛在的安全風險。
通過遵循這些原則,不安全 Rust 的安全哲學旨在平衡低級操作和性能優化的需求,同時確保整體代碼保持安全。儘管使用不安全 Rust 可能導致潛在的安全問題,但 Rust 語言試圖通過明確標記、限制使用範圍、嚴格審查和徹底文檔來最小化這些風險。
熱點討論:不安全 Rust 與 Zig#
最後,我想討論在編寫不安全代碼時,哪種語言更安全:不安全 Rust 還是 Zig。
這個話題的起源是一篇最近在 Reddit Rust 頻道上熱烈討論的帖子:當 Zig 比 Rust 更安全和更快。雖然標題提到 Rust
,但實際上是在與 不安全 Rust
進行比較。
Zig 語言介紹#
Zig 是一種現代的高性能系統編程語言,旨在簡化 C 的一些複雜性,同時提供更大的安全性和易用性。Zig 由 Andrew Kelley 在 2016 年發起,並得到了活躍的開源社區的支持。Zig 適用於各種場景,例如操作系統開發、嵌入式系統、遊戲開發、高性能計算等。儘管 Zig 語言相對較新,但它吸引了許多開發者的注意,並已應用於實際項目中。隨著社區和生態系統的發展,Zig 有望成為系統編程領域的重要選擇。
特徵 | Zig | Rust |
---|---|---|
設計目標 | 簡單性、高性能、易用性、C 兼容性 | 安全性、並發性、高性能、內存安全 |
語法 | 更接近 C 語言,更簡單 | 更接近 ML 系列語言,表達性類型系統 |
內存安全 | 編譯時檢查,無借用檢查器和所有權系統 | 借用檢查器和所有權系統,編譯時保證內存安全 |
性能 | 高性能,接近 C 語言 | 高性能,與 C++ 可比 |
行逐行編譯 | 支持逐行編譯,允許編譯時執行代碼 | 支持常量泛型,有限的編譯時執行能力 |
錯誤處理 | 錯誤返回值和錯誤聯合類型,無異常 | 結果和選項類型,無異常 |
FFI | 優秀的 C 語言兼容性,易於與現有 C 代碼互操作 | 良好的 FFI 支持,需要額外的綁定創建 |
包管理 | 內置包管理器 | Cargo 包管理器 |
運行時 | 無運行時開銷 | 最小運行時,可以在無 std 的情況下運行 |
社區和生態系統 | 相對較新,社區正在發展 | 成熟的社區和豐富的生態系統 |
請注意,雖然此表總結了主要差異,但每種語言在實踐中都有其獨特的特徵,因此您可能需要更深入地了解每種語言的特點,才能做出選擇。
Zig 語言的設計重點是內存安全,但與 Rust 不同,它沒有所有權系統和借用檢查器。然而,Zig 通過一些編譯時檢查和語言特性來增強內存安全。以下是 Zig 語言實現內存安全的一些方式:
- 編譯時檢查:Zig 編譯器在編譯期間執行許多檢查,以捕捉潛在的內存錯誤,例如數組越界訪問、空指針解引用等。當 Zig 編譯器檢測到這些錯誤時,它會停止編譯並報告錯誤。
- 錯誤處理:Zig 通過明確的錯誤處理來提高代碼的穩健性。Zig 沒有異常;相反,它使用錯誤返回值和錯誤聯合類型來處理錯誤。這迫使開發者明確處理潛在錯誤,有助於減少由未處理錯誤引起的內存安全問題。
- 可選類型:Zig 提供可選類型(Optional)來表示可能為空的值。通過使用可選類型,可以明確處理空值情況,減少空指針解引用的風險。
- 定義行為:Zig 為許多與內存相關的操作設計了定義行為,以避免未定義行為帶來的安全風險。例如,在解引用空指針時,Zig 確保發生明確定義的錯誤,而不是產生未定義行為。
- 內存管理:Zig 提供靈活的內存管理選項,包括手動內存管理、內置分配器和使用用戶定義的分配器。通過明確的內存管理,開發者可以更好地控制內存使用,減少內存泄漏和內存錯誤的風險。
總之,Zig 語言與 C 一樣,將內存管理委託給人類,完全信任人類的開發。然後,Zig 提供一些內存安全檢查來確保內存安全。然而,它仍然缺乏 Rust 的所有權系統和借用檢查器的嚴格編譯時保證。因此,在編寫 Zig 代碼時,開發者需要更加注意潛在的內存安全問題,並確保正確處理錯誤和異常情況。
Zig 真的是比不安全 Rust 更安全嗎?#
與安全 Rust 相比,Zig 語言給開發者更多的自由,但不如安全 Rust 安全。然而,Zig 比不安全 Rust 更安全嗎?
讓我們回到 Reddit 文章 當 Zig 比 Rust 更安全和更快。
1. 作者說使用不安全 Rust 很困難,完全依賴 Miri 的檢查。 這個說法似乎是正確的,但並不完全正確。
Miri 是一個 MIR 解釋器,具有許多功能。其中之一是在不安全 Rust 中進行 UB 檢查。
首先,不安全 Rust 確實具有挑戰性。在理解了前面提到的不安全 Rust 安全抽象的內容後,這種困難應該減少了一半。至少,不安全 Rust 的使用不再如此混亂,並且有了一個正確的方向。
UB 問題在 Zig 語言中也存在,Zig 也會面臨不安全代碼的問題。在 Zig 中編寫不安全代碼時,內存安全保證主要依賴於開發者的經驗和編碼實踐。儘管 Zig 編譯器提供了一些編譯時檢查,但在不安全代碼中,這些檢查可能不足以捕捉所有潛在的內存錯誤。為了確保在編寫不安全代碼時的內存安全,開發者可以遵循以下實踐:
- 減少不安全代碼的使用:儘量最小化不安全代碼的使用,而不妨礙性能和功能。將不安全代碼限制在最小的範圍內,使其更易於審查和維護。
- 使用類型系統:充分利用 Zig 的類型系統來表示不同類型的數據和約束。類型系統可以幫助開發者在編譯時捕捉潛在錯誤,減少內存錯誤的風險。
- 明確的錯誤處理:確保在不安全代碼中明確處理潛在錯誤,使用錯誤返回值和錯誤聯合類型來表示可能的錯誤情況。這有助於提高代碼的穩健性,減少由未處理錯誤引起的內存安全問題。
- 適當的封裝和抽象:對於需要使用不安全代碼的部分,考慮將其封裝成安全抽象,隔離不安全代碼。這確保其他部分的代碼在調用時不會接觸到潛在的內存錯誤。
- 代碼審查:對涉及不安全代碼的部分進行詳細的代碼審查,確保開發者了解潛在的內存風險並採取適當措施以防止錯誤。
- 測試:為不安全代碼編寫測試用例,確保其在不同場景下正常工作。測試可以幫助發現潛在的內存錯誤並驗證修復的有效性。
在 Zig 中編寫不安全代碼時,也需要執行類似於不安全 Rust 的安全抽象,並注意維護安全不變量和有效性不變量。
2. 作者提供了兩個示例來說明使用不安全 Rust 的困難
首先,使用 *mut T
和 *const T
已經失去了編譯器的安全保證,即使在 C 中,使用指針也需要開發者自己確保安全。然而,在 Zig 中,指針的使用與 C 類似,因此在這方面對 Zig 沒有特別的安全優勢。
作者還指出,在不安全 Rust 中,使用指針會導致代碼分散,例如 (*ptr).field
,因為指針不能調用方法。然而,在不安全 Rust 中有一個更好且更安全的解決方案:
impl Foo {
/// # 安全
/// 當調用此方法時,您必須確保 _要麼_ 指針為空 _要麼_ 所有以下條件都為真:
/// - 指針必須正確對齊。
/// - 指針必須指向已初始化的 `T` 實例。
unsafe fn as_ref<'a>(ptr: *const Foo) -> &'a Foo {
unsafe {
if let Some(foo) = ptr.as_ref() {
println!("我們得到了值:{foo}!");
foo
}
}
}
/// # 安全
/// 當調用此方法時,您必須確保 _要麼_ 指針為空 _要麼_ 所有以下條件都為真:
/// - 指針必須正確對齊。
/// - 指針必須指向已初始化的 `T` 實例。
unsafe fn as_mut<'a>(ptr: *mut Foo) -> &'a mut Foo {
unsafe {
if let Some(foo) = ptr.as_mut() {
println!("我們得到了值:{foo}!");
foo
}
}
}
}
標準庫為原始指針提供了 as_ref
和 as_mut
方法,以將其轉換為不可變和可變引用。通過為 Foo
實現這兩個方法,可以充分考慮安全條件,並且在不需要 (*ptr).field
類似的代碼的情況下,更方便地調用 Foo
的實例方法。
關於作者提到的數組問題,我提供了他們示例的改進版本:
#[derive(Debug)]
struct Value(i32);
unsafe fn do_stuff_with_array(values: *mut Value, len: usize) {
let values: &mut [Value] = std::slice::from_raw_parts_mut(values, len);
// 我可以使用迭代器的便利性!
for val in values.iter_mut() {
// ...
// 對每個類型為 &mut Value 的 `val` 執行操作
val.0 += 1;
}
}
fn main() {
// `do_stuff_with_array` 的示例用法
let mut values = vec![Value(1), Value(2), Value(3)];
unsafe { do_stuff_with_array(values.as_mut_ptr(), values.len()) };
println!("{values:?}");
}
我不認為這段代碼有任何問題。也許我錯過了作者的要點。至少我不同意他們建議避免使用引用作為解決此問題的方案。
作者將這些問題稱為 Rust 中的 “黑暗藝術”,但我有理由相信他們可能並不完全理解 Rust 的安全哲學以及不安全 Rust。
3. 作者偏愛 Zig 的內置安全策略
作者偏愛的 Zig 中的內置安全策略包括:
- 明確的分配策略和檢測內存錯誤的特殊分配器。
- 指針默認為非空,但其可空性可以用
?*Value
表示。 - 使用點運算符進行指針解引用,並區分單個和數組值指針。
從這個角度來看,Zig 對指針的安全措施與 Rust 的引用類似。Zig 可能更靈活地使用,就像 C 一樣,而不必考慮那麼多的安全因素。看起來 Zig 可能比不安全 Rust 更安全。
儘管不安全 Rust 的指針與 C 中的指針沒有區別,但其不當使用可能導致安全問題,就像 C 一樣。然而,不安全 Rust 的安全哲學允許開發者充分考慮與原始指針相關的安全問題。標準庫還提供了一些方法來幫助開發者更安全地使用指針。開發者可以完全將指針轉換為引用,或使用 NoNull<T>
使指針變為非空。此外,還有安全檢查工具,如 Miri 或 kani,對指針相關問題進行安全檢查。
不安全 Rust 對開發者的要求更高,其安全性可能更好,因為不安全 Rust 中的 unsafe
關鍵字可以傳播。對於開發者和審查者來說,從不安全 Rust 中抽象安全需要更多的努力。然而,這些努力是值得的。
Zig 的安全策略並不是 100% 安全的,仍然需要開發者考慮安全因素。例如,明確的內存管理允許開發者明確分配和釋放內存,從而更清楚地控制內存的生命週期,但這也意味著開發者必須承擔更多的責任,以確保不發生內存錯誤。
因此,沒有誰比誰更安全的說法。然而,根據作者的基準測試,Zig 的性能優於不安全 Rust。
總結#
本文試圖幫助讀者理解不安全 Rust 的安全哲學,並對最近在 Reddit 上討論的 Zig 和不安全 Rust 之間的安全性比較提供評論。然而,僅僅一篇文章不足以涵蓋不安全 Rust 安全抽象的所有各種細節。讀者還應注意後續不安全 Rust 系列文章。
最後,值得注意的是,本文的創作也得到了 GPT4 的幫助。
感謝您的閱讀。