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

The*_*ant 15 pointers rust memory-safety

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

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

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

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

DK.*_*DK. 8

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

语言设计师讨厌的一个狡猾的伎俩基本上是这样的:Rust 只能推断'static生命周期(用于全局变量和其他整个程序的生命周期事物)和堆栈(本地)变量的生命周期:它无法表达或推理分配的生命周期.

这意味着一些事情.首先,所有以堆分配处理的库类型( Box<T>,Rc<T>,Arc<T>),都拥有它们指向的东西.结果,他们实际上并不需要生命来存在.

当你需要的寿命是当你所访问的内容,一个智能指针.例如:

let mut x: Box<i32> = box 0;
*x = 42;
Run Code Online (Sandbox Code Playgroud)

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

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

换句话说,因为Box它不是魔术,我们必须告诉编译器如何将它转换成磨机借用指针的常规运行.这就是特征DerefDerefMut特征.这提出了一个问题:究竟是什么样的生命周期heap_ref

对此的答案是DerefMut(从记忆中因为我匆忙)的定义:

trait DerefMut {
    type Target;
    fn deref_mut<'a>(&'a mut self) -> &'a mut Target;
}
Run Code Online (Sandbox Code Playgroud)

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

这意味着"复杂"的东西没有明确的生命周期,因此必须拥有他们管理的东西.当您将一个复杂的智能指针/处理成简单借来的指针,就是你必须引入一生的那一刻,你通常只使用手柄本身的寿命.

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

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

"但是......我知道盒子会继续存在,为什么编译器说它没有?!" 因为Rust无法推断堆生命周期,并且必须求助于将生命周期&x变量联系起来 x,而不是它恰好指向的堆分配.


oli*_*obk 7

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

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

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

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

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

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

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

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

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

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

  1. 您永远不会有指向不再位于堆栈或堆上的对象的指针.这在生命周期的编译时证明了.
  2. 您没有Rust中的手动内存管理.使用a Box分配对象(类似但不等于unique_ptrC++中的a)
  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;
}
Run Code Online (Sandbox Code Playgroud)

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

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

fn foo(&mut self) {
    if bar() == 0 {
        self.a = 1;
    }
}
Run Code Online (Sandbox Code Playgroud)

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

Rust目前不关心您的代码是否被调用.它关心self.a在分配期间是否仍然存在内存.self.bar()永远不会破坏self(unsafe代码除外).因此self.a,if分支机构内部始终可用.