Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

不安全 X

到目前为止,我们讨论的所有代码都在编译时强制执行 X 语言的安全保证。但是 X 语言有第二个语言隐藏在表面之下,它不强制执行这些安全保证:这被称为不安全 X(Unsafe X),它的工作方式与常规 X 语言一样,但给了我们额外的超能力。

不安全 X 存在是因为静态分析本质上是保守的。当编译器试图确定代码是否持有某些保证时,它拒绝一些有效的程序,同时接受一些无效的程序。在这种情况下,我们可以使用不安全代码,因为我们知道程序是正确的。

另一个原因是底层计算机硬件本身就是不安全的。如果 X 语言不允许我们做不安全的操作,我们根本无法完成某些任务。X 语言需要能够进行系统级编程,例如直接与操作系统交互,甚至编写你自己的操作系统!这也是不安全 X 语言存在的原因之一。

让我们看看不安全 X 是什么以及如何使用它。在本章中,我们将介绍:

  • 什么是不安全操作
  • 如何使用不安全操作
  • 为什么不安全操作存在
  • 何时使用不安全操作

让我们开始!

不安全超能力

你可以通过使用 unsafe 关键字切换到不安全 X,并开始一个新的不安全代码块。不安全 X 给你的超能力是:

  • 解引用原始指针
  • 调用不安全函数或方法
  • 访问或修改可变静态变量
  • 实现不安全 trait

这些就是不安全超能力!让我们看看每一个。

在我们深入研究之前,让我们先记住这一点:不安全代码不会关闭借用检查器或禁用 X 语言的任何其他安全检查:如果你在不安全代码中使用引用,它仍然会被检查。不安全关键字只给你访问这四个功能,然后不被编译器检查。你仍然会在不安全代码中获得一定程度的安全性。

此外,不安全不一定意味着代码是危险的或必然会有内存安全问题:其意图是作为程序员,你将确保不安全块内的代码以有效的方式访问内存。

人们会犯错误,错误会发生,但通过要求这四个操作被包含在一个标有 unsafe 的块中,你将知道任何内存安全错误都在该块内。保持 unsafe 块尽可能小;稍后,当你调查内存错误时,你会很高兴这样做。

为了尽可能隔离不安全代码,最好将不安全代码包含在一个小的不安全块中,然后提供一个安全的 API。我们稍后会看到这一点,因为在我们了解了四个不安全超能力之后,我们会看一些例子,其中不安全代码被包装在一个安全的抽象后面。

解引用原始指针

在第 4 章中,我们看到了智能指针,并简要提到了原始指针(raw pointer)。类似于引用,原始指针是不可变的或可变的,分别写为 *const T*mut T。星号不是解引用运算符;它是类型名称的一部分。在原始指针的上下文中,不可变意味着指针在被赋值后不能直接改变。

原始指针与引用和智能指针的区别在于:

  • 允许忽略借用规则,同时拥有不可变和可变指针,或多个指向同一位置的可变指针
  • 不保证指向有效内存
  • 允许为空
  • 不实现任何自动清理

通过选择不拥有 X 语言给你的任何保证,你以放弃安全保证为代价获得了更好的性能或与其他语言或硬件接口的能力。

让我们看一下如何从引用创建原始指针,这是一个安全操作:

let mut x = 5
let r1 = &x as *const integer
let r2 = &mut x as *mut integer

我们正在从引用创建 *const integer*mut integer 原始指针。我们可以这样做,因为我们使用 as 将不可变和可变引用强制转换为它们相应的原始指针类型。直接从引用创建原始指针是安全的;我们还没有对原始指针做任何危险的事情。

注意,我们不需要在这里用 unsafe 标记这个代码。我们可以在安全代码中创建原始指针;我们只是不能在不安全块外解引用原始指针,正如我们稍后会看到的那样。

我们也可以直接从字面量创建原始指针,而不是从引用创建它们,但这很危险,所以让我们等到展示了 unsafe 块后再这样做。

我们刚刚展示了你可以在安全代码中创建原始指针。让我们看看当我们有一个原始指针时,我们可以用它做什么:解引用它。解引用原始指针需要 unsafe,如下所示:

let mut x = 5
let r1 = &x as *const integer
let r2 = &mut x as *mut integer

unsafe {
  println("r1 指向: {}", *r1)
  println("r2 指向: {}", *r2)
}

这段代码将打印出 r1 指向: 5r2 指向: 5。通过在 unsafe 块内解引用原始指针,我们选择承担责任,如果有问题,那是我们的错,不是 X 语言的错。

因为原始指针可以为空、无效或别名,所以使用它们时很容易导致内存安全问题。如果可能,请尽量避免使用原始指针!

调用不安全函数或方法

第二个要求 unsafe 块的操作是调用不安全函数。不安全函数和方法看起来就像普通函数和方法,只是它们在定义的其余部分之前有一个额外的 unsafe。这里的 unsafe 关键字表示该函数在被调用时有我们需要遵守的要求,因为 X 语言不能保证我们已经满足了这些要求。

让我们看看一个不安全函数:

unsafe function dangerous() {
  // 一些危险的代码
}

unsafe {
  dangerous()
}

不安全函数必须在 unsafe 块内调用。如果我们尝试在没有 unsafe 块的情况下调用 dangerous,我们会得到一个错误。

不安全函数体实际上是一个不安全块,因此我们可以在不安全函数中执行其他不安全操作,而无需添加另一个 unsafe 块。

创建一个不安全函数的安全抽象是一个常见的模式。让我们看一下标准库中的一个例子:split_at_mut。这个函数在列表上工作,它是安全的。让我们看看为什么它是安全的,但它使用了不安全代码。这个方法接受一个列表和一个索引,并将列表分成两部分:该索引之前的所有内容和该索引之后的所有内容。

let mut v = [1, 2, 3, 4, 5, 6]
let (left, right) = v.split_at_mut(3)
println!("{:?}, {:?}", left, right)

我们不能用安全 X 实现这个函数。让我们尝试思考一下:

// 这不会编译!
function split_at_mut(slice: &mut [integer], mid: integer) -> (&mut [integer], &mut [integer]) {
  let len = slice.len()
  assert!(mid <= len)
  (&mut slice[..mid], &mut slice[mid..])
}

这无法编译,因为借用检查器不允许我们对同一个切片进行两次可变借用。然而,我们知道我们正在做的事情是安全的;我们正在借用切片的两个独立部分。为了告诉 X 语言这一点,我们需要使用不安全代码。让我们看看 split_at_mut 的实际实现:

use core::slice;

function split_at_mut(slice: &mut [integer], mid: integer) -> (&mut [integer], &mut [integer]) {
  let len = slice.len()
  let ptr = slice.as_mut_ptr()

  assert!(mid <= len)

  unsafe {
    (
      slice::from_raw_parts_mut(ptr, mid),
      slice::from_raw_parts_mut(ptr.offset(mid as isize), len - mid)
    )
  }
}

我们正在使用我们知道的一些不安全操作,并且我们通过将不安全代码包装在安全函数中来安全地抽象它。

好的,所以调用不安全函数是第二个不安全操作。让我们继续第三个:访问可变静态变量。

访问或修改可变静态变量

我们还没有谈论过全局变量(global variables),X 语言确实支持它们,但它们由于借用规则而有问题。如果两个线程正在访问同一个可变全局变量,可能会导致数据竞争。

在 X 语言中,全局变量称为静态(static)变量。让我们看一个例子:

static HELLO_WORLD: &str = "Hello, world!"

function main() {
  println("名称是: {}", HELLO_WORLD)
}

静态变量类似于常量。我们用 static 而不是 const 来声明它们,并且它们的类型总是被注解(所以我们不能省略 &'static str)。静态变量的名称是 SCREAMING_SNAKE_CASE,按照惯例。

静态变量和常量之间的区别在于静态变量在内存中有一个固定的地址;使用该值将始终访问相同的地址。另一方面,常量在每次使用时都可以复制它们的数据。另一个区别是静态变量可以是可变的。访问和修改可变静态变量是不安全的。让我们看看:

static mut COUNTER: integer = 0

function add_to_count(inc: integer) {
  unsafe {
    COUNTER = COUNTER + inc
  }
}

function main() {
  add_to_count(3)
  unsafe {
    println("COUNTER: {}", COUNTER)
  }
}

这段代码将打印 COUNTER: 3。这工作正常,但访问 COUNTER 需要 unsafe。使用可变静态变量很难实现数据竞争安全,因此 X 语言认为它们是不安全的。

实现不安全 trait

最后一个我们可以在 unsafe 块内执行的操作是实现不安全 trait。当至少有一个方法具有编译器无法验证的不变量时,trait 就是不安全的。我们可以通过在 trait 之前添加 unsafe 并在实现该 trait 时也添加 unsafe 来将 trait 声明为不安全:

unsafe trait Foo {
  // 方法
}

unsafe impl Foo for integer {
  // 方法实现
}

通过使用 unsafe impl,我们承诺我们将维护编译器无法验证的不变量。

这就是四个不安全超能力!让我们回顾一下:

  • 解引用原始指针
  • 调用不安全函数或方法
  • 访问或修改可变静态变量
  • 实现不安全 trait

好的,关于不安全 X 就讲到这里!我们不会在本书中进一步使用不安全 X,但现在你知道它是什么以及如何使用它了。