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

时间:2015-04-14 13:28:46

标签: pointers rust memory-safety

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

然而,据我所知,Rust语言要求指针检查完全在编译时完成(没有与指针相关的未定义行为,"安全"指针至少,以及没有"无效指针"或"空指针"运行时异常也是。)

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

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

3 个答案:

答案 0 :(得分:8)

免责声明:我有点匆忙,所以这有点蜿蜒。随意清理它。

语言设计师讨厌的一个狡猾的伎俩基本上是这样的: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绑定,它恰好指向的堆分配。

答案 1 :(得分:7)

  

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

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

  

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

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

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

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

  

另一种可能性是Rust指针不是&#34;通用&#34;或者在某种意义上受限制,以便编译器可以在编译时完全推断出它们的属性,但它们并不像e那样有用。 G。 C中的原始指针或C ++中的智能指针。

C中可能出现一些问题:

  1. 悬空指针
  2. double free
  3. 空指针
  4. 野性指针
  5. 这些不会发生在安全的Rust中。

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

      1. C中可能出错的一切
      2. SmartPointers只会帮助您忘记致电free。您仍然可以创建悬空参考:auto x = make_unique<int>(42); auto& y = *x; x.reset(); y = 99;
      3. Rust修复了那些:

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

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

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

            

          直觉上这并不奇怪,因为在这种情况下,我们能够在编译时推断出程序的运行时行为,类似于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

答案 2 :(得分:1)

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

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

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

然而,仍然可以使用原始指针,但是你必须在'a块中包含处理它们的代码,告诉编译器“相信我,我知道我在这里做什么”。这就是一些特殊的智能指针在内部执行的操作,例如RefCell,它允许您在运行时而不是编译时检查这些规则,以获得表达性。