Keyword unsafe

source ·
Expand description

类型系统无法验证其 内存安全 的代码或接口。

unsafe 关键字有两种用途:

  • 声明存在编译器无法检查的契约 (unsafe fnunsafe trait),
  • 并声明客户端已检查这些合同是否得到维护 ( 不安全 {}unsafe impl,还有 unsafe fn - 见下文)。

它们不是互斥的,正如在 unsafe fn 中可以看到的那样: 默认情况下,unsafe fn 的主体被视为不安全块。可以启用 unsafe_op_in_unsafe_fn lint 来改变它。

不安全的能力

安全 Rust 无论如何都不会导致未定义行为。这被称为 可靠性: 一个良好类型的程序实际上具有期望的属性。在 Nomicon 中,对此主题有更详细的说明。

为了确保可靠性,安全 Rust 受到足够的限制,可以自动检查。然而,有时,出于一些聪明得让编译器无法理解的原因,有必要编写正确的代码。在这种情况下,您需要使用不安全的 Rust。

除安全 Rust 之外,不安全生锈还具有以下功能:

但是,这种额外的权力还伴随着额外的责任:现在由你来确保健全性。unsafe 关键字有助于清楚地标记需要担心这一点的代码段。

unsafe 的不同含义

并非所有的 unsafe 用法都是等价的:有些用法是为了标记程序员必须检查契约的存在,有些用法是为了表示 “我已经检查了契约,继续并执行此操作吧”。以下 关于 Rust 内部的讨论 对此有更深入的说明,但这里是要点的总结:

  • unsafe fn: 调用这个函数意味着遵守编译器无法强制执行的契约。
  • unsafe trait: 实现 trait 意味着遵守编译器无法强制执行的契约。
  • unsafe {}: 调用块内操作所必需的契约已经由程序员检查过,并保证得到遵守。
  • unsafe impl: 实现 trait 所必需的契约已经过程序员的检查,并保证得到遵守。

默认情况下,unsafe fn 也像函数中的代码周围的 unsafe {} 块一样。这意味着它不仅仅是给调用者的一个信号,而且还保证函数内部操作的先决条件得到支持。 混合这两种含义可能会令人困惑,因此可以启用 unsafe_op_in_unsafe_fn lint 来警告这一点,并且即使在 unsafe fn 内部也需要明确的不安全块。

有关详细信息,请参见 RustonomiconReference

Examples

将元素标记为 unsafe

unsafe 可用在函数上。请注意,在 extern 块中声明的函数和静态变量被隐式标记为 unsafe (而不是声明为 extern "something" fn ... 的函数)。 无论在何处声明,可变静态变量始终是不安全的。方法也可以被声明为 unsafe

static mut FOO: &str = "hello";

unsafe fn unsafe_fn() {}

extern "C" {
    fn unsafe_extern_fn();
    static BAR: *mut u32;
}

trait SafeTraitWithUnsafeMethod {
    unsafe fn unsafe_method(&self);
}

struct S;

impl S {
    unsafe fn unsafe_method_on_struct() {}
}
Run

Traits 也可以被声明为 unsafe

unsafe trait UnsafeTrait {}
Run

由于 unsafe fnunsafe trait 表示存在编译器无法强制执行的安全保证,因此对其进行记录很重要。标准库提供了许多示例,例如以下示例,它摘录自 Vec::set_len# Safety 部分解释了安全调用函数时必须履行的契约。

/// 将 vector 的长度强制为 `new_len`。
/// 这是一个低级操作,不维护该类型的任何正常不变量。
/// 通常,使用安全操作之一 (例如 `truncate`,`resize`,`extend` 或 `clear`) 来更改 vector 的长度。
/// # Safety
/// - `new_len` 必须小于或等于 `capacity()`。
/// - `old_len..new_len` 上的元素必须初始化。
pub unsafe fn set_len(&mut self, new_len: usize)
Run

使用 unsafe {} 块和 impls

执行 unsafe 操作需要一个 unsafe {} 块:

#![deny(unsafe_op_in_unsafe_fn)]

/// 解引用给定的指针。
/// # Safety
/// `ptr` 必须对齐,并且不能是悬垂的。
unsafe fn deref_unchecked(ptr: *const i32) -> i32 {
    // SAFETY: 调用者需要确保 `ptr` 对齐且可解引用。
    unsafe { *ptr }
}

let a = 3;
let b = &a as *const _;
// SAFETY: `a` 尚未丢弃,并且引用始终对齐,因此 `b` 是有效地址。
unsafe { assert_eq!(*b, deref_unchecked(b)); };
Run

unsafe 和 traits

unsafe 和 traits 的相互作用可能令人惊讶,所以让我们用两个例子来对比 unsafe trait 中的安全 fn 和安全 trait 中的 unsafe fn 的两种组合:

/// # Safety
/// `make_even` 必须返回一个偶数。
unsafe trait MakeEven {
    fn make_even(&self) -> i32;
}

// SAFETY: 我们的 `make_even` 总是返回一些东西。
unsafe impl MakeEven for i32 {
    fn make_even(&self) -> i32 {
        self << 1
    }
}

fn use_make_even(x: impl MakeEven) {
    if x.make_even() % 2 == 1 {
        // SAFETY: 这永远不会发生,因为所有 `MakeEven` 实现都确保 `make_even` 返回一些东西。
        unsafe { std::hint::unreachable_unchecked() };
    }
}
Run

注意 trait 的安全保证是如何被实现维护的,它本身是用来维护 use_make_even 调用的不安全函数 unreachable_unchecked 的安全保证。 make_even 本身是一个安全的函数,因为它的callers 不必担心任何契约,只需要 MakeEvenimplementation 来维护某个契约。 use_make_even 是安全的,因为它可以使用 MakeEven 实现的 promise 来维护它调用的 unsafe fn unreachable_unchecked 的安全保证。

也可以在常规安全 trait 中包含 unsafe fn:

#![deny(unsafe_op_in_unsafe_fn)]

trait Indexable {
    const LEN: usize;

    /// # Safety
    /// 调用者必须确保 `idx < LEN`。
    unsafe fn idx_unchecked(&self, idx: usize) -> i32;
}

// `i32` 的实现不需要做任何契约推理。
impl Indexable for i32 {
    const LEN: usize = 1;

    unsafe fn idx_unchecked(&self, idx: usize) -> i32 {
        debug_assert_eq!(idx, 0);
        *self
    }
}

// 数组的实现利用函数契约在切片上使用 `get_unchecked` 并避免运行时检查。
impl Indexable for [i32; 42] {
    const LEN: usize = 42;

    unsafe fn idx_unchecked(&self, idx: usize) -> i32 {
        // SAFETY: 根据此 trait 的文档,调用者确保 `idx < 42`.
        unsafe { *self.get_unchecked(idx) }
    }
}

// never type 的实现声明长度为 0,这意味着永远不能调用 `idx_unchecked`。
impl Indexable for ! {
    const LEN: usize = 0;

    unsafe fn idx_unchecked(&self, idx: usize) -> i32 {
        // SAFETY: 根据这个 trait 的文档,调用者确保 `idx < 0`,这是不可能的,所以这是死代码。
        unsafe { std::hint::unreachable_unchecked() }
    }
}

fn use_indexable<I: Indexable>(x: I, idx: usize) -> i32 {
    if idx < I::LEN {
        // SAFETY: 我们已经检查了 `idx < I::LEN`。
        unsafe { x.idx_unchecked(idx) }
    } else {
        panic!("index out-of-bounds")
    }
}
Run

这一次,use_indexable 是安全的,因为它使用运行时检查来解除 idx_unchecked 的安全保证。 实现 Indexable 是安全的,因为在编写 idx_unchecked 时,我们不必担心: 我们的callers 需要履行证明义务 (就像 use_indexable 一样),但 get_unchecked 的*实现 * 没有证明义务可抗衡。 当然,Indexable 的实现可以选择调用其他不安全的操作,然后它需要一个 unsafe block 来表明它已经解除了被调用者的证明义务。 (我们启用了 unsafe_op_in_unsafe_fn,因此 idx_unchecked 的主体并不是隐含的不安全块。) 为此,它可以使用所有调用者必须支持的契约 –idx < LEN.

正式地说,trait 中的 unsafe fn 是一个具有preconditions 的函数,它超出了参数类型 (例如 idx < LEN) 编码的那些,而 unsafe trait 可以声明它的某些函数具有postconditions 超出那些编码在返回类型 (例如返回偶数)。

如果 trait 需要一个具有额外前置条件和额外后置条件的函数,那么它需要一个 unsafe trait 中的 unsafe fn