Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

大幅重译 1.1 Safe 和 Unsafe 如何交互 #41

Merged
merged 1 commit into from
Feb 15, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 23 additions & 23 deletions src/safe-unsafe-meaning.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@

Safe Rust 和 Unsafe Rust 之间有什么关系?它们又是如何交互的?

Safe Rust 和 Unsafe Rust 之间的边界由`unsafe`关键字控制,`unsafe`是承接了它们之间交互的桥梁。这就是为什么我们可以说 Safe Rust 是一种安全的语言:所有不安全的部分都被限制在“unsafe”边界之内。如果你愿意,你甚至可以把`#![forbid(unsafe_code)]`扔进你的代码库,以静态地保证你只写安全的 Rust。
Safe Rust 和 Unsafe Rust 之间的边界由`unsafe`关键字控制,`unsafe`是承接了它们之间交互的桥梁。这就是为什么我们可以说 Safe Rust 是一种安全的语言:所有不安全的部分都被限制在“unsafe”边界之内。如果你愿意,你甚至可以把`#![forbid(unsafe_code)]`扔进你的代码库,以静态地保证你只写 Safe Rust。

`unsafe`关键字有两个用途:声明编译器不会保证这些代码的安全性,以及声明程序员已经确保这些代码是安全的。

你可以用`unsafe`来表示在 _函数_ 和 _trait 声明_ 这些行为不一定安全。对于函数,`unsafe`意味着函数的用户必须仔细阅读该函数的文档,以确保他们在使用该函数时能满足函数能安全运行的条件。对于 trait 声明,`unsafe`意味着 trait 的实现者必须仔细阅读 trait 文档,以确保他们的实现遵循了 trait 所要求条件
_函数_ 和 _trait 声明_ 上添加`unsafe`前缀表示其中存在未经检查的约束。对于函数,`unsafe`意味着函数的用户必须仔细阅读该函数的文档,以确保他们的使用方式遵循了该函数规定的约束。对于 trait 声明,`unsafe`意味着 trait 的实现者必须仔细阅读 trait 文档,以确保他们的实现遵循了该 trait 规定的约束

你可以在一个块上使用`unsafe`来声明在其中执行的所有不安全操作都经过了验证以保证操作的安全性。例如,当传递给[`slice::get_unchecked`][get_unchecked]的索引在边界内,这一行为就是安全的
在代码块上添加`unsafe`前缀可以声明在其中执行的所有不安全操作都经过了验证(遵循了内部不安全操作所规定的约束)。传递给[`slice::get_unchecked`][get_unchecked]的索引在边界内时,就是一个可以这样添加`unsafe`前缀的例子

你可以在 trait 的实现上使用`unsafe`来声明该实现满足了 trait 的条件。例如,实现[`Send`]说明这个类型移动到另一个线程是真正安全的
trait 实现上添加`unsafe`前缀可以声明该实现满足了 trait 所规定的约束。例如,当一个类型的值移动到另一个线程是真正安全的时,便可在[`Send`]的实现前添加`unsafe`前缀

标准库中有许多 unsafe 的函数,包括:

Expand All @@ -21,33 +21,33 @@ Safe Rust 和 Unsafe Rust 之间的边界由`unsafe`关键字控制,`unsafe`

从 Rust 1.29.2 开始,标准库定义了以下 unsafe trait(还有其他 trait,但还没有稳定下来,有些可能永远不会稳定下来):

- [`Send`] 是一个标记 trait(一个没有 API 的 trait),承诺实现了[`Send`]的类型可以安全地发送(移动)到另一个线程。
- [`Sync`] 是一个标记 trait,承诺线程可以通过共享引用安全地共享实现了[`Sync`]的类型。
- [`Send`] 是一个标记 trait(一个没有 API 的 trait),用于保证实现了[`Send`]的类型可以安全地发送(移动)到另一个线程。
- [`Sync`] 是一个标记 trait,用于保证线程间可以通过共享引用安全地共享实现了[`Sync`]的类型。
- [`GlobalAlloc`]允许自定义整个程序的内存分配器。

Rust 标准库的大部分内容也在内部使用了 Unsafe Rust。这些实现一般都经过严格的人工检查,所以建立在这些实现之上的 Safe Rust 接口可以被认为是安全的。
Rust 标准库也有很多地方在内部使用了 Unsafe Rust。这些实现一般都经过严格的人工检查,所以建立在这些实现之上的 Safe Rust 接口可以被认为是安全的。

我们需要将它们分离,是因为 Safe Rust 的一个基本属性,即*健全性属性*。
之所以要像这样分离 Safe 和 Unsafe,归根到底在于 Safe Rust 的一个根本属性,即*可靠性*。
PureWhiteWu marked this conversation as resolved.
Show resolved Hide resolved

**无论怎样,Safe Rust 都不会导致未定义行为。**
**无论怎样,Safe Rust 都不能导致未定义行为。**

Safe 与 Unsafe 分离的设计意味着 Safe Rust 和 Unsafe Rust 之间存在着不对等的信任关系。一方面, Safe Rust 本质上必须相信它所接触的任何 Unsafe Rust 都是正确编写的。另一方面,Unsafe Rust 在信任 Safe Rust 时必须非常小心。

例如,Rust 有[`PartialOrd`]和[`Ord`] trait 来区分“部分序”比较的类型和“全序”比较的类型(这意味着比较行为必须是合理的)。
例如,Rust 有[`PartialOrd`]和[`Ord`] trait 来区分“偏序”比较的类型和“全序”比较的类型(前者仅能进行比较而未必得出大小关系,而后者意味着每一个比较都有合理的结果)。

[`BTreeMap`]对于`PartialOrd`的类型来说并没有实际意义,因此它要求其 key 实现`Ord`。然而,`BTreeMap`在其实现中包含了 Unsafe 的代码,所以无法接受马虎的(可以用Safe编写的)`Ord`实现,因为这会导致未定义行为。因此,BTreeMap 中的 Unsafe 代码必须被编写成对实际上不完全的`Ord`实现具有鲁棒性——尽管我们要求`Ord`是正确实现的
[`BTreeMap`]以没有定义全序关系的类型作为 key 是没有意义的,因此它要求其 key 实现`Ord`。然而,`BTreeMap`的实现中包含了 Unsafe 的代码。由于(用 Safe 代码就能写出的)不靠谱的`Ord`实现导致未定义行为是不可接受的,因此,BTreeMap 中的 Unsafe 代码必须健壮到这个地步:对于实际上并非全序关系的`Ord`实现也不会导致未定义行为——尽管我们指定`Ord`约束就是为了得到全序关系

Unsafe Rust 代码不能相信 Safe Rust 代码会被正确编写。也就是说,如果你输入了没有正确实现全序排序的值,`BTreeMap`仍然会表现得完全不正常。它只是不会导致未定义行为
Unsafe Rust 代码不能信任 Safe Rust 代码逻辑无误。话虽如此,如果你输入的值,其类型并没有全序关系,`BTreeMap`仍然会变得乱七八糟。上一段只是说明它不会导致未定义行为

有人可能会问,如果`BTreeMap`不能信任`Ord`,因为它是安全的,那么它为什么能信任*任何*安全的代码呢?例如,`BTreeMap`依赖于整数和 slice 的正确实现。这些也是安全的,对吗
有人可能会问,如果`BTreeMap`不能基于“它是 Safe 代码编写的”这一理由而信任`Ord`,那还有*什么* Safe 代码是能信任的呢?例如,`BTreeMap`依赖于整数和切片的正确实现。这些也是 Safe Rust 编写的,不是么

区别在于范围的不同。当`BTreeMap`依赖于整数和分片时,它依赖于一个非常具体的实现。这是一个可以衡量的风险,可以与收益相权衡。在这种情况下,风险基本上为零;如果整数和 slice 被破坏,那么*所有人*都会被破坏。而且,它们是由维护`BTreeMap`的人维护的,所以很容易对它们进行监控。
区别在于范围的不同。当`BTreeMap`依赖于整数和切片时,它依赖于一个完全特定的实现。这里的风险经过评估可以与收益相权衡。在这个特定场景下,风险基本为零;如果整数和切片出了问题,*什么东西*都会出问题,因此不可能被忽视。而且,它们和`BTreeMap`是由同一批人维护的,所以很容易对它们进行监控。

另一方面,`BTreeMap`的 key 类型是泛型的。信任它的`Ord`实现意味着信任过去、现在和未来的每一个`Ord`实现。这里的风险很高:有人会犯错误,把他们的`Ord`实现搞得一团糟,甚至直接撒谎说提供了一个完整的排序,因为“它看起来很有效”。`BTreeMap`需要做好准备应对这种情况的发生
另一方面,`BTreeMap`的 key 类型是泛型的。信任它的`Ord`实现意味着信任过去、现在和未来的每一个`Ord`实现。这里的风险很高:总有人会犯错误,把`Ord`实现坏,甚至直接谎称提供了一个全序关系,因为“这个实现看上去够用”。对于这种情况,`BTreeMap`需要有备无患

同样的逻辑也适用于信任一个传递给你的闭包会有正确的行为
同样的逻辑也适用于信任一个传递给你的闭包的行为是正确的

`unsafe` trait 就是用来解决泛型的信任问题。理论上,`BTreeMap`类型可以要求 key 实现一个新的 trait,称为`UnsafeOrd`,而不是`Ord`,它可能看起来像这样:
问题是能否无限信任泛型类型参数?`unsafe` trait 应运而生。理论上,`BTreeMap`类型可以要求 key 实现一个新的 trait,称为`UnsafeOrd`,而不是`Ord`,它可能看起来像这样:

```rust
use std::cmp::Ordering;
Expand All @@ -57,17 +57,17 @@ unsafe trait UnsafeOrd {
}
```

然后,一个类型将使用`unsafe`来实现`UnsafeOrd`,表明他们已经确保他们的实现满足了该 trait 所期望的任何条件。在这种情况下,`BTreeMap`内部的 Unsafe Rust 有理由相信 key 类型的`UnsafeOrd`实现是正确的。如果不是这样,那就是 unsafe trait 实现的错,这与 Rust 的安全保证是一致的。
然后,为一个类型实现`UnsafeOrd`就要带上`unsafe`前缀,表明开发者已经确保他们的实现遵循了该 trait 所预期的任何约束。在这种情况下,`BTreeMap`内部的 Unsafe Rust 有理由相信 key 类型的`UnsafeOrd`实现是正确的。否则错就在 unsafe trait 的实现,这与 Rust 的安全保证是一致的。

是否将一个 trait 标记为`unsafe`是一个 API 设计。一个 safe trait 更容易实现,但任何依赖它的 Unsafe 代码都必须抵御不正确的行为。将 trait 标记为`unsafe`会将这个责任转移到实现者身上。Rust 习惯于避免将 trait 标记为`unsafe`,因为它使 Unsafe Rust 普遍存在,这并不可取
是否将 trait 标记为`unsafe` API 设计取舍的问题。Safe trait 实现起来更轻松,但任何依赖它的 Unsafe 代码面临不正确的实现也不能引发未定义行为。将 trait 标记为`unsafe`会将这个责任转移到实现者身上。按照 Rust 传统,往往避免将 trait 标记为`unsafe`,否则 Unsafe Rust 会无处不在,我们并不想看到这个结果

`Send`和`Sync`被标记为 unsafe,是因为线程安全是一个*基本*的属性,unsafe 代码不可能像抵御一个有缺陷的`Ord`实现那样去抵御它。同样地,`GlobalAllocator`是对程序中所有的内存进行记录,其他的东西如`Box`或`Vec`都建立在它的基础上。如果它做了一些奇怪的事情(当它还在使用的时候,把同一块内存给了另一个请求),就没有机会检测到并采取任何措施了
`Send`和`Sync`被标记为 unsafe,是因为线程安全是一个*根本的属性*,要像应对一个有缺陷的`Ord`实现一样应对线程安全问题,对 unsafe 代码来说是不可能的。同理,`GlobalAlloc`被用于管理程序中所有的内存分配,诸如`Box`或`Vec`都建立在它的基础上。如果`GlobalAlloc`不正常了(例如把一块还被占用着的内存返回给了另一个请求),是绝无可能靠检测来补救的

决定是否将你自己的 trait 标记为unsafe”,也是出于同样的考虑。如果unsafe”的代码不能抵御 trait 的错误实现,那么将 trait 标记为unsafe”就是一个合理的选择
是否将你自己的 trait 标记为`unsafe`,也要基于类似的考虑做出决定。如果`unsafe`代码无法有效应对 trait 的错误实现,那么将 trait 标记为`unsafe`合情合理

顺便说一下,虽然`Send`和`Sync`是`unsafe`的特性,但它们*也是*自动实现的类型,当这种派生可以证明是安全的。`Send`是自动派生的,只适用于一个类型下所有类型都实现了`Send`。`Sync`是自动派生的,只适用于一个类型下所有类型都实现了`Sync`。这最大限度地减少了使这两个 trait unsafe” 的危险。而且,没有多少人会去*实现*内存分配器(或者针对这个问题而言,直接使用它们)。
顺便一提,虽然`Send`和`Sync`是`unsafe` trait,但是当类型系统可以证明派生`Send`/`Sync`安全时,它们*也会*被自动实现。每个字段类型都满足`Send`的类型会自动派生`Send`。每个字段类型都满足`Sync`的类型会自动派生`Sync`。通过这种方式,这两个 trait 扩散`unsafe`的影响被控制到最小。而对于内存分配器,没多少人会去*实现*它们(说起来,直接使用内存分配器的人都很少)。

这就是 Safe Rust 和 Unsafe Rust 之间的平衡。这种分离是为了使 Safe Rust 的使用尽可能符合人体工程学,但在编写 Unsafe Rust 时需要额外的努力和小心。本书的其余部分主要是讨论必须采取的谨慎,以及Unsafe Rust 必须坚持的契约
上文展示了 Safe Rust 和 Unsafe Rust 之间的平衡。将两者分离的设计,目的是让使用 Safe Rust 尽可能符合工效,反过来在编写 Unsafe Rust 时则需要额外的努力和细心。本书的其余部分主要是讨论需要什么形式的细心,以及Unsafe Rust 必须遵循什么约束

[`Send`]: https://doc.rust-lang.org/std/marker/trait.Send.html
[`Sync`]: https://doc.rust-lang.org/std/marker/trait.Sync.html
Expand Down
Loading