AlexZhang

AlexZhang

从不安全到安全 | 对不安全 Rust 的全面理解

介绍#

众所周知,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 中执行的操作:

  1. 原始指针操作:可以创建、解引用和操作原始指针(*const T*mut T)。这允许您直接访问内存地址,执行内存分配、释放和修改等操作。
  2. 调用不安全函数:不安全 Rust 可以调用标记为unsafe的函数。这些函数可能导致未定义行为,因此需要在unsafe代码块中调用。这些函数通常用于实现低级操作,例如内存管理、硬件访问等。
  3. 实现不安全特性:可以实现标记为unsafe的特性。这些特性可能包含潜在的风险操作,在实现时需要明确标记为unsafe
  4. 访问和修改可变静态变量:在不安全 Rust 中,可以访问和修改具有全局生命周期的可变静态变量。这些变量在整个程序执行过程中保持活动状态,可能导致潜在的数据竞争问题。
  5. 处理Union类型。由于多个字段共享相同的内存位置,使用union存在一定风险。访问union字段时,编译器无法保证类型安全,因为它无法确定当前存储值属于哪个字段。为了确保安全访问union字段,需要在unsafe代码块中执行操作。
  6. 禁用运行时边界检查:不安全 Rust 允许您绕过数组边界检查。通过使用get_uncheckedget_unchecked_mut方法,可以在不执行边界检查的情况下访问数组和切片元素,从而提高性能。
  7. 内联汇编:在不安全 Rust 中,可以使用内联汇编(asm!宏)直接编写处理器指令。这使您能够实现特定于平台的优化和操作。
  8. 外部函数接口(FFI):不安全 Rust 允许您与用其他编程语言(如 C/C++)编写的代码进行交互。这通常涉及本机指针操作、类型转换和调用不安全函数。

需要注意的是,使用不安全 Rust 时需要谨慎。尽可能优先使用安全 Rust 编写代码。尽管它提供了强大的功能,但也可能导致未定义行为和内存安全问题。因此,Rust 的官方标准库源代码实现和官方不安全代码指南都包含不安全 Rust 的安全哲学,以维护其安全性。

不安全 Rust 的安全哲学#

不安全 Rust 的安全哲学是允许开发者在受限条件下执行低级操作和性能优化,同时确保整体代码保持安全

安全抽象:维护安全不变式和有效性不变式#

整体代码保持安全意味着什么?不安全 Rust 有一个术语称为安全不变式专门用于定义这一点。

安全 Rust 具有编译器安全检查,以确保内存安全和并发安全,但对于不安全 Rust 的那些专门操作场景,Rust 编译器无法提供帮助,因此开发者需要自行确保代码的内存安全和并发安全。不安全 Rust 是开发者对 Rust 编译器的安全承诺:“安全由我来守护!”

为了遵守这一安全承诺,在编写不安全 Rust 代码时,开发者必须始终维护安全不变式。安全不变式是指在整个程序执行过程中必须保持的条件,以确保内存安全。这些条件通常包括指针有效性、数据结构完整性、数据访问同步等。安全不变式主要关注程序的正确执行和避免未定义行为。在使用不安全 Rust 时,开发者负责确保满足这些安全不变式,以避免内存安全问题。

在维护安全不变式的过程中,另一个需要理解的概念是有效性不变式。有效性不变式是指某些数据类型和结构在其生命周期内必须满足的条件。有效性不变式主要关注数据类型和结构的正确性。例如,对于引用类型,有效性不变式包括非空指针和指向有效内存。编写和使用不安全 Rust 代码时,开发者需要确保这些有效性不变式得以维护。

安全不变式和有效性不变式之间有一定的关联,因为它们都关注代码的正确性和安全性。维护有效性不变式通常有助于确保满足安全不变式。例如,确保引用类型指针的有效性(有效性不变式)可以防止空指针解引用(安全不变式)。尽管它们相关,但安全不变式和有效性不变式的关注领域不同。安全不变式主要关注整个程序的内存安全和避免未定义行为,而有效性不变式主要关注特定数据类型和结构的正确性。

它们之间的关系可以总结为以下几个方面:

  1. 目的:安全不变式主要关注内存安全和数据完整性,以防止未定义行为。有效性不变式关注类型实例在其生命周期内必须满足的条件,以确保正确使用。
  2. 范围:安全不变式通常涉及整个程序或模块的内存安全,而有效性不变式则特定于类型约束。在某种程度上,安全不变式可以被视为全局限制,而有效性不变式是局部限制。
  3. 层次:安全不变式和有效性不变式可以在不同层次上相互作用。通常,维护有效性不变式是实现安全不变式的基础。换句话说,一个类型的有效性不变式通常是实现更高级别安全不变式所必需的。
  4. 依赖性:安全不变式依赖于有效性不变式。当一个类型的有效性不变式得到满足时,有助于确保实现安全不变式所需的条件。例如,在 Rust 中,安全引用访问依赖于底层类型的有效性不变式得到满足。

因此,当不安全 Rust 代码违反安全不变式或有效性不变式时,我们称该代码为不安全的。不安全代码可能导致未定义行为、内存泄漏、数据竞争等问题。Rust 试图通过编译器和类型系统确保大多数代码的内存安全,尤其是避免未定义行为,从而避免不安全的情况。

了解更多:两种不变式:安全和有效性

未定义行为是指程序执行结果不可预测的情况。这可能是由于越界内存访问、空指针解引用、数据竞争和其他错误引起的。当遇到未定义行为时,程序可能崩溃、产生错误结果或表现出其他意外行为。

然而,有人认为在像 Zig 和 Rust 这样的新语言中使用 “未定义行为” 这个术语可能不够准确

在 Rust 中,特别注意可能导致未定义行为的情况:

  1. 数据竞争:数据竞争发生在多个线程同时访问同一内存位置,并且至少有一个线程正在执行写操作。Rust 的所有权系统和借用检查器在编译时防止大多数数据竞争,但在不安全 Rust 中,开发者需要格外小心以避免数据竞争。
  2. 无效指针解引用:解引用无效指针(例如,空指针、指向已释放内存的指针)会导致未定义行为。
  3. 整数溢出:在某些情况下,整数溢出(例如,整数加法、减法、乘法等)可能导致程序出现意外情况。Rust 在调试模式下默认启用整数溢出检查,但在发布模式下,整数溢出行为是定义的,见RFC 0560
  4. 访问未初始化内存:访问未初始化内存会导致未定义行为。这包括读取或写入未初始化内存或将未初始化内存传递给外部函数。
  5. 错误的类型转换:强制将一种类型的指针转换为另一种类型的指针,然后解引用它可能导致未定义行为。这通常发生在不安全 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。现在让我们分析如何满足安全不变式和有效性不变式:

  1. 安全不变式:
    • 内存分配和释放:当长度等于容量时,push方法调用reallocate方法(此处省略详细实现),确保分配足够的内存。这确保不会发生越界内存访问。
    • 数据访问同步:在这个例子中,push方法是一个可变引用(&mut self),确保在调用时没有其他可变或不可变引用。这确保没有数据竞争。
  2. 有效性不变式:
    • 指针有效性: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 进行低级操作和优化,同时确保安全不变式和有效性不变式得到满足,实现安全和高性能的抽象。这种方法也应用于许多其他标准库类型和方法,例如StringHashMap等。这使得开发者能够利用低级操作和性能优化,同时编写安全的高级代码。

String#

在标准库的String类型中,有一对类似的方法:from_utf8from_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 实践中的指南可以有效实现不安全安全抽象:

  1. 最小化不安全代码:不安全 Rust 的使用应限制在必要时使用。大多数特性应使用安全 Rust 实现,以确保内存安全并避免未定义行为。尽可能将不安全 Rust 代码封装在安全 API 中,以为用户提供安全接口。
  2. 明确标记为不安全:不安全 Rust 必须明确标记为unsafe,以便开发者可以清楚地识别潜在的安全风险。当遇到unsafe关键字时,开发者应特别注意并仔细审查代码,以确保正确处理潜在的内存安全问题和未定义行为。开发者需要仔细考虑一个函数或特性是否应标记为unsafe,并且不应故意删除或不使用应标记为unsafe的标记。
  3. 审查和测试:不安全 Rust 代码需要更严格的审查和测试,以确保其正确性。这包括测试低级操作、内存分配和释放以及并发行为。确保不安全 Rust 代码的正确性至关重要,因为错误可能导致严重的内存安全问题和未定义行为。
  4. 文档和注释:不安全 Rust 代码应有良好的文档和注释,以便其他开发者理解其目的、操作和潜在风险。这有助于维护者和其他贡献者在修改代码时遵循正确的安全实践。
  5. 减少复杂性:不安全 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 有望成为系统编程领域的重要选择。

特性ZigRust
设计目标简单性、高性能、易用性、与 C 兼容安全性、并发性、高性能、内存安全
语法更接近 C 语言,更简单更接近 ML 系列语言,表达性类型系统
内存安全编译时检查,没有借用检查器和所有权系统借用检查器和所有权系统,编译时保证内存安全
性能高性能,接近 C 语言高性能,可与 C++ 相媲美
行级编译支持逐行编译,允许在编译时执行代码支持常量泛型,有限的编译时执行能力
错误处理错误返回值和错误联合类型,没有异常结果和选项类型,没有异常
FFI优秀的 C 语言兼容性,易于与现有 C 代码互操作良好的 FFI 支持,需要额外创建绑定
包管理内置包管理器Cargo 包管理器
运行时无运行时开销最小运行时,可以在没有 std 的情况下运行
社区和生态系统相对较新,社区正在发展成熟的社区和丰富的生态系统

请注意,虽然此表总结了主要差异,但每种语言在实践中都有其独特的特性,因此您可能需要更深入地了解每种语言的特征,然后再做出选择。

Zig 语言的设计重点是内存安全,但与 Rust 不同,它没有所有权系统和借用检查器。然而,Zig 通过一些编译时检查和语言特性增强了内存安全。以下是 Zig 语言实现内存安全的一些方式:

  1. 编译时检查:Zig 编译器在编译期间执行许多检查,以捕捉潜在的内存错误,例如数组越界访问、空指针解引用等。当 Zig 编译器检测到这些错误时,它会停止编译并报告错误。
  2. 错误处理:Zig 通过显式错误处理提高代码的鲁棒性。Zig 没有异常,而是使用错误返回值和错误联合类型来处理错误。这迫使开发者显式处理潜在错误,帮助减少由于未处理错误导致的内存安全问题。
  3. 可选类型:Zig 提供可选类型(Optionals)来表示可能为 null 的值。通过使用可选类型,可以显式处理 null 值情况,降低空指针解引用的风险。
  4. 定义行为:Zig 为许多与内存相关的操作设计了定义行为,以避免未定义行为带来的安全风险。例如,在解引用空指针时,Zig 确保发生明确的错误,而不是产生未定义行为。
  5. 内存管理:Zig 提供灵活的内存管理选项,包括手动内存管理、内置分配器和使用用户定义的分配器。通过显式的内存管理,开发者可以更好地控制内存使用,减少内存泄漏和内存错误的风险。

总之,Zig 语言像 C 一样,将内存管理托付给人类,完全信任人类的开发。然后,Zig 提供一些内存安全检查以确保内存安全。然而,它仍然缺乏 Rust 的所有权系统和借用检查器的严格编译时保证。因此,在编写 Zig 代码时,开发者需要更加注意潜在的内存安全问题,并确保正确处理错误和异常情况。

Zig 真的比不安全 Rust 更安全吗?#

与安全 Rust 相比,Zig 语言为开发者提供了更多自由,但不如安全 Rust 安全。然而,Zig 真的比不安全 Rust 更安全吗?

让我们回到 Reddit 文章当 Zig 比 Rust 更安全和更快时

1. 作者说使用不安全 Rust 很困难,完全依赖于 Miri 的检查。 这个说法似乎是正确的,但并不完全正确。

Miri 是一个具有许多功能的 MIR 解释器。其中之一是在不安全 Rust 中进行未定义行为检查。

首先,不安全 Rust 确实具有挑战性。在理解了前面提到的不安全 Rust 安全抽象的内容后,这种困难应该减半。至少,不安全 Rust 的使用并不像以前那样令人困惑,并且有了正确的方向。

未定义行为问题在 Zig 语言中也存在,Zig 也将面临不安全代码的问题。在 Zig 中编写不安全代码时,内存安全保证主要依赖于开发者的经验和编码实践。尽管 Zig 编译器提供了一些编译时检查,但在不安全代码中,这些检查可能不足以捕捉所有潜在的内存错误。为了确保在编写不安全代码时的内存安全,开发者可以遵循以下实践:

  1. 减少不安全代码的使用:尽量在不影响性能和功能的情况下最小化不安全代码的使用。将不安全代码限制在尽可能小的范围内,以便于审查和维护。
  2. 使用类型系统:充分利用 Zig 的类型系统来表示不同类型的数据和约束。类型系统可以帮助开发者在编译时捕捉潜在错误,降低内存错误的风险。
  3. 显式错误处理:确保在不安全代码中显式处理潜在错误,使用错误返回值和错误联合类型表示可能的错误情况。这有助于提高代码的鲁棒性,减少由于未处理错误导致的内存安全问题。
  4. 适当的封装和抽象:对于需要使用不安全代码的部分,考虑将其封装为安全抽象,隔离不安全代码。这确保其他部分在调用时不会接触到潜在的内存错误。
  5. 代码审查:对涉及不安全代码的部分进行详细的代码审查,确保开发者理解潜在的内存风险,并采取适当措施防止错误。
  6. 测试:为不安全代码编写测试用例,确保其在不同场景下正常工作。测试可以帮助发现潜在的内存错误,并验证修复的有效性。

在 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_refas_mut方法,以将其转换为不可变和可变引用。通过为Foo实现这两个方法,可以充分考虑安全条件,并且在调用Foo实例的方法时变得更加方便,而无需使用(*ptr).field这样的代码。

关于作者提到的数组问题,我提供了他们示例的改进版本:

#[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 的帮助。

感谢您的阅读。

加载中...
此文章数据所有权由区块链加密技术和智能合约保障仅归创作者所有。