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


我已经读过某个地方,在一个具有指针的语言中,编译器无法在编译时完全决定是否所有指针都被正确使用和/或有效(参考活动对象)由于各种原因,因为那样做基本上构成解决停止问题。直觉上这并不奇怪,因为在这种情况下,我们能够在编译时推断程序的运行时行为,类似于在 这个相关的问题

但是,据我所知,Rust语言要求指针检查完全在编译时完成(没有与指针相关的未定义行为,至少是“安全”指针,并且没有“无效指针”或“空指针”运行时例外)。

假设Rust编译器无法解决暂停问题,那么谬误在哪里呢?

  • 是这样的吗? 指针检查不是完全在编译时完成的, 而且,与C中的原始指针相比,Rust的智能指针仍会引入一些运行时开销?
  • 或者Rust编译器可能无法做出完全正确的决策,有时需要Just Trust The Programmer,可能使用其中一个生命周期注释(带有 <'lifetime_ident> 句法)?在这种情况下,这是否意味着指针/内存安全保证不是100%,仍然依赖程序员编写正确的代码?
  • 另一种可能性是Rust指针在某种意义上是非“通用的”或受限制的,因此编译器可以在编译时完全推断它们的属性,但它们并不像e那样有用。 G。 C语言中的原始指针或C ++中的智能指针。
  • 或者它可能是完全不同的东西,我误解了一个或多个
      { "pointer", "safety", "guaranteed", "compile-time" }

9829
2018-04-14 13:28


起源

“在具有指针的语言中,编译器不可能在编译时完全决定是否所有指针都被正确使用和/或有效(参考活动对象)由于各种原因,因为这基本上构成了解决停止问题。“我对这一陈述的真实性提出质疑。也许是C风格的指针,因为它们缺少各种好东西,并且有各种不好的东西,比如指针算术,但对于实际的人来说 需要 从指针,Rust及其引用是声明无效的完美例子。 - Chris Morgan
@ChrisMorgan我明白了,谢谢。 - The Paramagnetic Croissant


答案:


放弃:我有点匆忙,所以这有点蜿蜒。随意清理它。

语言设计师讨厌的一个狡猾的伎俩基本上是这样的: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 不是魔术,我们必须告诉编译器如何将它变成常规的,运行的磨机借用指针。这是什么的 DerefDerefMut 特征是为了。这提出了一个问题:究竟是什么,它的生命周期 heap_ref

对此的答案在于定义 DerefMut (从记忆中因为我赶时间):

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

就像我之前说的那样,Rust 绝对不能 谈论“堆生命周期”。相反,它必须绑定堆分配的生命周期 i32 到它手边唯一的其他生命周期:生命周期 Box

这意味着“复杂”的东西没有明确的生命周期,因而必须这样做 拥有 他们管理的东西。当您将复杂的智能指针/句柄转换为简单的借用指针时,  是你必须引入一生的那一刻,你通常只使用句柄本身的生命周期。

实际上,我应该澄清:通过“句柄的生命周期”,我的意思是“当前存储句柄的变量的生命周期”:生命周期真的是 存储, 不是为了 。这通常是为什么Rust的新人在他们无法解决为什么他们不能做以下事情时会被绊倒的原因:

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

“但是......我知道这个盒子会继续存在,为什么编译器说它没有?!”  因为Rust无法推断堆生存期和 必须 诉诸于平息 &x 到了 变量  x 恰好指向的堆分配。


7
2018-04-14 14:06





是不是指针检查不是完全在编译时完成的,而Rust的智能指针仍然会引入一些运行时开销,比如C中的原始指针?

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

或者Rust编译器可能无法做出完全正确的决策,有时需要Just Trust The Programmer,可能使用了一个生命周期注释(具有<'lifetime_ident>语法的注释)?在这种情况下,这是否意味着指针/内存安全保证不是100%,仍然依赖程序员编写正确的代码?

如果编译器无法做出正确的决定,则会出现编译时错误,告诉您编译器无法验证您正在执行的操作。这也可能会限制你知道正确的东西,但编译器却没有。你总是可以去 unsafe 那种情况下的代码。但正如你正确假设的那样,编译器部分依赖于程序员。

编译器会检查函数的实现,看看它是否完全符合生命周期的说法。然后,在函数的调用站点,它检查程序员是否正确使用该函数。这类似于类型检查。 C ++编译器会检查您是否返回了正确类型的对象。然后,如果返回的对象存储在正确类型的变量中,它将在调用站点进行检查。函数的程序员决不会违背承诺(除非是 unsafe使用,但你总是可以让编译器强制执行no unsafe 在你的项目中使用)

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

另一种可能性是Rust指针在某种意义上是非“通用的”或受限制的,因此编译器可以在编译时完全推断它们的属性,但它们并不像e那样有用。 G。 C语言中的原始指针或C ++中的智能指针。

在C中有一些可能出错的事情:

  1. 晃来晃去的指针
  2. 双免费
  3. 空指针
  4. 狂野的指针

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

  1. 您永远不会有指向不再位于堆栈或堆上的对象的指针。这在生命周期的编译时证明了。
  2. 您没有在Rust中进行手动内存管理。用一个 Box 分配你的对象(类似但不等于a unique_ptr 在C ++中)
  3. 再次,没有手动内存管理。 Boxes自动释放内存。
  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。这在编译时进行检查,并且不能被更多级别的间接或结构所规避。

我已经读过某个地方,在一个具有指针的语言中,编译器无法在编译时完全决定是否所有指针都被正确使用和/或有效(参考活动对象)由于各种原因,因为那样做基本上构成解决停止问题。

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

直觉上这并不奇怪,因为在这种情况下,我们能够在编译时推断程序的运行时行为,类似于在 这个相关的问题

重新讨论关于暂停问题的引用问题中的示例:

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)。如果 bar 是 const,证明是否变得微不足道 self.a 被设定为 1 在编译时。除此之外,没有 pure 功能或其他功能内容的限制,你永远无法证明 self.a 被设定为 1 或不。

Rust目前不关心您的代码是否被调用。它关心的是记忆 self.a 在任务期间仍然存在。 self.bar() 永远不会毁灭 self (除了 unsafe 码)。为此 self.a 将永远在里面可用 if 科。


7
2018-04-14 13:48



优秀的解释。只有一件事对我来说并不完全清楚:你是什么意思“Rust不能证明你们所有的指针都被正确使用了”?当然,人们可以写“假”程序,我。即那些应该做某事但实际上做了不同的事情,给我们不正确的结果。通过“正确使用指针”,我的意思是“你不能因无效/空/悬空指针而导致内存错误”。 - The Paramagnetic Croissant
是。你仍然可以写 42 在您刚刚阅读的整数上,然后想知道为什么输入的内容无关紧要,您的整数为42.这是一个逻辑错误,而不是内存错误。 - oli_obk


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

  • 如果你有一个const引用(&),你可以克隆这个引用并传递它,但不能创建一个mutable &mut 引用它。
  • 如果是可变的(&mut)对象存在的引用,不存在对该对象的其他引用。
  • 引用不允许超过它引用的对象,并且所有函数都操纵引用 必须 声明如何使用生命周期注释链接来自其输入和输出的引用(如 'a)。

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

然而,仍然可以使用原始指针,但你必须将处理它们的代码括在一个 unsafe { /* ... */ } 阻止,告诉编译器“相信我,我知道我在这里做什么”。这就是一些特殊的智能指针在内部做的事情,比如 RefCell,它允许您在运行时而不是编译时检查这些规则,以获得表达能力。


1



“所有操作引用的函数都必须声明它们的输入和输出的引用是如何链接的” - 是的,那些正是我所指的生命周期注释。这是否意味着如果我的生命周期注释错误,我可以取消引用无效指针并崩溃? - The Paramagnetic Croissant
不,因为编译器实际上检查您的注释是否正确。这有时会迫使程序员做一些不寻常的事情,以便编译器看到注释是正确的。 - Levans
但另一方面,如果你做错了什么 unsafe 块,任何事情都可能发生。 - Levans
当然,任何事情都可能发生在 unsafe 块,因此我对它们一点也不感兴趣。所以答案是“如果编译器不能证明它是正确的,那就是非法的。”谢谢! - The Paramagnetic Croissant