recent question正在寻找构建自我参照结构的能力。在讨论该问题的可能答案时,一个潜在的答案包括使用 UnsafeCell 进行内部可变性,然后通过 transmute “丢弃”可变性。

这是这种想法付诸实践的一个小例子。我对示例本身并不十分感兴趣,但是它很复杂,需要像transmute这样的更大的锤子,而不是仅使用 UnsafeCell::new 和/或 UnsafeCell::into_inner :

use std::{
    cell::UnsafeCell, mem, rc::{Rc, Weak},
};

// This is our real type.
struct ReallyImmutable {
    value: i32,
    myself: Weak<ReallyImmutable>,
}

fn initialize() -> Rc<ReallyImmutable> {
    // This mirrors ReallyImmutable but we use `UnsafeCell`
    // to perform some initial interior mutation.
    struct NotReallyImmutable {
        value: i32,
        myself: Weak<UnsafeCell<NotReallyImmutable>>,
    }

    let initial = NotReallyImmutable {
        value: 42,
        myself: Weak::new(),
    };

    // Without interior mutability, we couldn't update the `myself` field
    // after we've created the `Rc`.
    let second = Rc::new(UnsafeCell::new(initial));

    // Tie the recursive knot
    let new_myself = Rc::downgrade(&second);

    unsafe {
        // Should be safe as there can be no other accesses to this field
        (&mut *second.get()).myself = new_myself;

        // No one outside of this function needs the interior mutability
        // TODO: Is this call safe?
        mem::transmute(second)
    }
}

fn main() {
    let v = initialize();
    println!("{} -> {:?}", v.value, v.myself.upgrade().map(|v| v.value))
}

该代码似乎可以打印出我所期望的内容,但这并不意味着它是安全的或使用已定义的语义。

UnsafeCell<T>转换为T内存是否安全?它会调用未定义的行为吗?在相反的方向上从T转换为UnsafeCell<T>怎么办?

最佳答案

(我对SO还是陌生的,不知道“好吧,也许”是否可以作为答案,但是,您来了。)

免责声明:此类事情的规则尚未确定。因此,尚无确切答案。我将基于(a)LLVM会/我们最终想要进行哪种编译器转换,以及(b)我脑中拥有哪种模型来定义对此的答案,来做出一些猜测。

另外,我看到了两部分:数据布局透视图和别名透视图。布局问题是,原则上NotReallyImmutable可以具有与ReallyImmutable完全不同的布局。我对数据布局了解不多,但是随着UnsafeCell成为repr(transparent)且这是两种类型之间的唯一区别,我认为这样做的目的是。但是,从某种意义上讲,您依赖repr(transparent)是“结构性的”,它应该允许您替换较大类型的内容,但我不确定在任何地方都明确写下了该内容。听起来像是对后续RFC的建议,该建议适当扩展了repr(transparent)保证?

就别名而言,问题在于违反了&T的规则。我想说的是,只要您在编写&T时从未在任何地方有实时的&UnsafeCell<T>,那么您就很好了-但我认为我们尚不能保证。让我们更详细地看。

编译器角度

这里的相关优化是利用&T为只读的优化。因此,如果您对最后两行(transmute和赋值)进行了重新排序,则该代码很可能是UB,因为我们可能希望编译器能够“预取”共享引用背后的值,并在以后重新使用该值(即内联后)。

但是在您的代码中,我们只会在noalias返回之后才发出“只读”注释(LLVM中的transmute),并且从那里开始确实是只读数据。因此,这应该很好。

内存模型

我的内存模型中“最积极”的本质上是asserts that all values are always valid,我认为即使该模型也适合您的代码。 &UnsafeCell在该模型中是一个特例,其有效性刚刚停止,并且没有提及该引用背后的内容。 transmute返回的那一刻,我们会抓取它指向的内存并将其全部设置为只读,即使我们通过Rc(以递归方式)执行了该操作(我的模型没有这样做,但这仅仅是因为我无法弄清楚这样做的一种好方法),您会很好的,因为在transmute之后不再进行任何变异。 (您可能已经注意到,这与编译器透视图中的限制相同。毕竟,这些模型的目的是允许编译器优化。)

(作为一个旁注,我真的希望miri目前处于更好的状态。似乎我必须尝试并进行验证才能再次在其中运行,因为那样的话我可以告诉您只在miri中运行您的代码,它可以告诉您你,如果我的模型版本与你在做什么就可以:D)

我正在考虑目前仅检查“访问中”情况的其他模型,但尚未确定该模型的UnsafeCell故事。此示例显示的是,该模型可能必须包含一些方法,以便使存储的“相变”首先是UnsafeCell,但随后需要具有只读保证的正常共享。感谢您提出来,这将为您提供一些很好的例子!

因此,我想我可以说(至少从我这方面来说)有允许这种代码的意图,并且这样做似乎并不能阻止任何优化。我无法预测我们是否会设法找到每个人都可以同意的模型,并且仍然允许这样做。

相反的方向:T -> UnsafeCell<T>
现在,这更有趣。问题是,如上所述,通过&T编写代码时,您不得直播UnsafeCell<T>。但是,“活着”在这里是什么意思?这是一个很难的问题!在我的某些模型中,这可能会像“该类型的引用存在于某处且生命周期仍处于事件状态”一样弱,即与引用是否实际使用无关。 (这很有用,因为它使我们能够进行更多优化,例如即使无法证明循环曾经运行过,也将负载移出循环,这会引入对未使用的引用的使用。)由于&TCopy,因此您甚至也不能真正摆脱这样的引用。因此,如果您有x: &T,那么在let y: &UnsafeCell<T> = transmute(x)之后,旧的x仍然存在,并且其生命周期仍然有效,因此通过y进行写入很可能是UB。

我认为您必须以某种方式限制&T允许的别名,非常小心地确保没有人仍然拥有这样的引用。我不会说“这是不可能的”,因为人们总是让我感到惊讶(尤其是在这个社区中;),但是TBH我想不出一种方法来完成这项工作。如果您有一个示例,但您认为这是合理的,我会很好奇。

关于rust - 在T和UnsafeCell <T>之间转换是安全且定义好的行为吗?,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/50431702/

10-10 20:03