Rust 如何实现仅编译时指针安全?

How does Rust achieve compile-time-only pointer safety?

我在某处读到,在一种以指针为特色的语言中,编译器不可能在编译时完全决定是否所有指针都被正确使用 and/or 是否有效(指的是一个活着的对象)出于各种原因,因为这基本上构成了解决停机问题。直觉上这并不奇怪,因为在这种情况下,我们将能够在编译时推断程序的运行时行为,类似于 this related question.

中所述

然而,据我所知,Rust 语言要求指针检查完全在编译时完成(没有与指针相关的未定义行为,至少 "safe" 指针,并且没有 "invalid pointer" 或 "null pointer" 运行时异常)。

假设Rust编译器没有解决停机问题,谬误在哪里?

大部分 Rust 引用的安全性都由严格的规则保证:

  • 如果您拥有一个 const 引用 (&),您可以克隆该引用并传递它,但不能从中创建一个可变的 &mut 引用。
  • 如果存在对对象的可变 (&mut) 引用,则不能存在对该对象的其他引用。
  • 一个引用不允许比它引用的对象活得更久,所有操作引用的函数必须声明它们的输入和输出中的引用是如何链接的,使用生命周期注解 (喜欢 'a).

因此,就表达能力而言,我们实际上比使用普通原始指针更受限制(例如,仅使用安全引用不可能构建图形结构),但这些规则可以在编译时有效地完全检查-时间。

然而,仍然可以使用原始指针,但你必须将处理它们的代码封装在一个 unsafe { /* ... */ } 块中,告诉编译器 "Trust me, I know what I am doing here"。这就是一些特殊的智能指针在内部所做的事情,例如 RefCell,它允许您在运行时而不是编译时检查这些规则,以获得表现力。

免责声明:我有点赶时间,所以有点曲折。随意清理一下。

Language Designers Hate™ 的一个偷偷摸摸的把戏基本上是这样的:Rust 可以 只能 解释 'static 生命周期(用于全局变量和其他整个程序生命周期的事情)和堆栈的生命周期(局部)变量:它不能表达或推理分配的生命周期。

这意味着一些事情。首先,所有处理堆分配的库类型( Box<T>Rc<T>Arc<T>)都是拥有 他们指向的东西。因此,它们实际上 需要 生命周期才能存在。

需要生命周期的地方是您访问智能指针的内容时。例如:

let mut x: Box<i32> = box 0;
*x = 42;

第二行幕后发生的事情是这样的:

{
    let box_ref: &mut Box<i32> = &mut x;
    let heap_ref: &mut i32 = box_ref.deref_mut();
    *heap_ref = 42;
}

换句话说,因为 Box 不是魔法,我们必须告诉编译器如何把它变成一个常规的,运行 of the mill borrowed pointer。这就是 DerefDerefMut 特征的用途。这就提出了一个问题:heap_ref 的生命周期究竟是多少?

这个问题的答案在DerefMut的定义中(因为赶时间,凭记忆):

trait DerefMut {
    type Target;
    fn deref_mut<'a>(&'a mut self) -> &'a mut Target;
}

就像我之前说的,Rust 绝对不能谈论"heap lifetimes"。相反,它必须将堆分配的 i32 的生命周期与其现有的唯一其他生命周期联系起来:Box.

的生命周期

这意味着 "complicated" 事物没有可表达的生命周期,因此必须 拥有 它们管理的事物。当你把一个复杂的智能pointer/handle转换成一个简单的借用指针时,那个就是你必须引入生命周期的时刻,而你通常只使用句柄本身的生命周期.

实际上,我应该澄清一下:"lifetime of the handle",我的意思是 "the lifetime of the variable in which the handle is currently being stored":生命周期实际上是为了 存储,而不是为了 。这通常就是为什么 Rust 的新手在无法理解为什么他们不能做类似的事情时会被绊倒的原因:

fn thingy<'a>() -> (Box<i32>, &'a i32) {
    let x = box 1701;
    (x, &x)
}

"But... I know that the box will continue to live on, why does the compiler say it doesn't?!" 因为 Rust 无法推断堆的生命周期并且 必须 求助于绑定 [=25 的生命周期=]到变量x不是它恰好指向的堆分配。

Is it the case that pointer checking isn't done entirely at compile-time, and Rust's smart pointers still introduce some runtime overhead compared to, say, raw pointers in C?

对于无法在编译时检查的内容,有特殊的运行时检查。这些通常位于 cell 板条箱中。但一般来说,Rust 在编译时检查所有内容,并且应该生成与在 C 中相同的代码(如果你的 C 代码没有做未定义的事情)。

Or is it possible that the Rust compiler can't make fully correct decisions, and it sometimes needs to Just Trust The Programmer™, probably using one of the lifetime annotations (the ones with the <'lifetime_ident> syntax)? In this case, does this mean that the pointer/memory safety guarantee is not 100%, and still relies on the programmer writing correct code?

如果编译器无法做出正确的决定,您会收到一个编译时错误,告诉您编译器无法验证您在做什么。这也可能会限制您使用您知道是正确的东西,但编译器不会。在这种情况下,您可以随时转到 unsafe 代码。但是正如您正确假设的那样,编译器部分依赖于程序员。

编译器检查函数的实现,看它是否完全按照生命周期所说的那样做。然后,在函数的调用点,它会检​​查程序员是否正确使用了函数。这类似于类型检查。 C++ 编译器会检查您是否返回了正确类型的对象。然后它在调用点检查返回的对象是否存储在正确类型的变量中。函数的程序员在任何时候都不能违背承诺(除非使用 unsafe,但您始终可以让编译器强制您的项目中不使用 unsafe

Rust 不断改进。一旦编译器变得更智能,更多的东西可能在 Rust 中变得合法。

Another possibility is that Rust pointers are non-"universal" or restricted in some sense, so that the compiler can infer their properties entirely during compile-time, but they are not as useful as e. g. raw pointers in C or smart pointers in C++.

在 C:

中有一些可能出错的地方
  1. 悬挂指针
  2. 双倍免费
  3. 空指针
  4. 野指针

这些不会发生在安全的 Rust 中。

  1. 您永远不能拥有指向不再位于堆栈或堆上的对象的指针。这在编译时通过生命周期得到证明。
  2. 您在 Rust 中没有手动内存管理。使用 Box 分配对象(类似于但不等于 C++ 中的 unique_ptr
  3. 同样,没有手动内存管理。 Box会自动释放内存。
  4. 在安全的 Rust 中,您可以创建指向任何位置的指针,但不能取消引用它。您创建的任何引用始终绑定到一个对象。

在 C++ 中有一些可能出错的地方:

  1. C 中可能出错的所有内容
  2. SmartPointers 只帮助您不忘记调用 free。您仍然可以创建悬挂引用:auto x = make_unique<int>(42); auto& y = *x; x.reset(); y = 99;

Rust 修复了这些:

  1. 见上文
  2. 只要y存在,就不能修改x。这是在编译时检查的,不能通过更多级别的间接寻址或结构来规避。

I have read somewhere that in a language that features pointers, it is not possible for the compiler to decide fully at compile time whether all pointers are used correctly and/or are valid (refer to an alive object) for various reasons, since that would essentially constitute solving the halting problem.

Rust 并不能证明你所有的指针都被正确使用了。您仍然可以编写伪造的程序。 Rust 证明您没有使用无效指针。 Rust 证明你永远不会有空指针。 Rust 证明你永远不会有两个指向同一个对象的指针,除非所有这些指针都是不可变的(const)。 Rust 不允许您编写任何程序(因为这将包括违反内存安全的程序)。现在 Rust 仍然阻止你编写一些有用的程序,但有计划允许用安全的 Rust 编写更多(合法)程序。

That is not surprising, intuitively, because in this case, we would be able to infer the runtime behavior of a program during compile-time, similarly to what's stated in this related question.

重新审视您提到的有关停机问题的示例:

void foo() {
    if (bar() == 0) this->a = 1;
}

上面的 C++ 代码在 Rust 中看起来是两种方式之一:

fn foo(&mut self) {
    if self.bar() == 0 {
        self.a = 1;
    }
}

fn foo(&mut self) {
    if bar() == 0 {
        self.a = 1;
    }
}

对于任意 bar 你无法证明这一点,因为它可能会访问全局状态。 Rust 很快获得 const 函数,可用于在编译时计算内容(类似于 constexpr)。如果 barconst,证明 self.a 是否在编译时设置为 1 变得微不足道。除此之外,如果没有pure函数或者函数内容的其他限制,你永远无法证明self.a是否设置为1

Rust 目前不关心你的代码是否被调用。它关心赋值时self.a的内存是否还存在。 self.bar() 永远不会破坏 selfunsafe 代码除外)。因此 self.a 将始终在 if 分支中可用。