首页 文章

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

提问于
浏览
14

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

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

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

  • 与C语言中的原始指针相比, pointer checking isn't done entirely at compile-time, 和Rust的智能指针是否还会引入一些运行时开销?

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

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

  • 或许这是完全不同的东西,我误解了一个或多个
    { "pointer", "safety", "guaranteed", "compile-time" } .

3 回答

  • 8

    Disclaimer :我有点匆忙,所以这有点蜿蜒 . 随意清理它 .

    语言设计师仇恨的一个狡猾的伎俩基本上是这样的: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绝对不能谈论"heap lifetimes" . 相反,它必须将堆分配的生命周期 i32 绑定到它手头的唯一其他生命周期: Box 的生命周期 .

    这意味着"complicated"事物没有明确的生命周期,因此必须拥有他们管理的东西 . 当您将复杂的智能指针/句柄转换为简单的借用指针时,您必须引入生命周期,并且通常只使用句柄本身的生命周期 .

    实际上,我应该澄清:"lifetime of the handle",我的意思是"the lifetime of the variable in which the handle is currently being stored":生命时间真的是存储,而不是 Value . 这通常是为什么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无法推断堆生命周期,并且必须求助于将 &x 的生命周期绑定到变量 x ,而不是它恰好指向的堆分配 .

  • 7

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

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

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

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

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

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

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

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

    • 悬空指针

    • 双免费

    • 空指针

    • 野指针

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

    • 您永远不会有指向不再位于堆栈或堆上的对象的指针 . 这在生命周期的编译时证明了 .

    • Rust中没有手动内存管理 . 使用 Box 分配对象(类似但不等于C中的 unique_ptr

    • 再次,没有手动内存管理 . Box es自动释放内存 .

    • 在安全Rust中,您可以创建指向任何位置的指针,但不能取消引用它 . 您创建的任何引用始终绑定到对象 .

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

    • C中可能出错的一切

    • SmartPointers只会帮助您忘记调用 free . 您仍然可以创建悬空参考: auto x = make_unique<int>(42); auto& y = *x; x.reset(); y = 99;

    Rust修复了那些:

    • 见上文

    • 只要 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 ) . 如果 barconst ,则证明 self.a 在编译时是否设置为 1 变得微不足道 . 除此之外,如果没有 pure 函数或函数内容的其他限制,您永远无法证明 self.a 是否设置为 1 或不 .

    Rust目前不关心您的代码是否被调用 . 它关心在分配期间 self.a 的内存是否仍然存在 . self.bar() 永远无法销毁 selfunsafe 代码除外) . 因此 self.a 将始终在 if 分支内可用 .

  • 1

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

    • 如果你拥有一个const引用( & ),你可以克隆这个引用并传递它,但不能创建一个可变的 &mut 引用 .

    • 如果存在对对象的可变( &mut )引用,则不能存在对该对象的其他引用 .

    • 不允许引用超过它引用的对象,并且所有操作引用的函数必须使用生存期注释(如 'a )声明如何链接来自其输入和输出的引用 .

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

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

相关问题