是否安全且定义了在T和UnsafeCell <T>之间转换的行为?

She*_*ter 8 unsafe undefined-behavior rust

一个最近的问题一直在寻找构建自引用结构的能力.在讨论问题的可能答案时,一个可能的答案涉及使用UnsafeCell内部可变性然后通过a"丢弃"可变性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))
}
Run Code Online (Sandbox Code Playgroud)

此代码似乎打印出我期望的内容,但这并不意味着它是安全的或使用定义的语义.

是从安全转换UnsafeCell<T>T内存吗?它会调用未定义的行为吗?如何在相反的方向转换,从a TUnsafeCell<T>

Ral*_*ung 6

(我还是新手,并不确定"好吧,也许"是否有资格作为答案,但是你走了.;)

免责声明:这些事情的规则尚未确定.所以,还没有确定的答案.我将基于(a)LLVM做什么样的编译器转换/我们最终想要做什么来做出一些猜测,以及(b)我脑子里有哪种模型可以定义答案.

另外,我看到了两个部分:数据布局透视图和别名透视图.布局问题NotReallyImmutable原则上可能具有完全不同的布局ReallyImmutable.我对数据布局知之甚少,但随着UnsafeCell成为repr(transparent)这两种类型之间的唯一区别,我认为其目的是为了实现这一点.然而,repr(transparent)你应该依赖于"结构",因为它应该允许你替换更大类型的东西,我不确定在任何地方都明确写下来了.听起来像后续RFC的提议,repr(transparent)适当地扩展了保证?

就别名而言,问题在于违反规则&T.我要说的是,只要你&T在写作时没有在任何地方生活过&UnsafeCell<T>,你就是好的 - 但我认为我们还不能保证这一点.让我们看一下更多细节.

编译器的角度

这里的相关优化是利用&T只读的优化.因此,如果您重新排序最后两行(transmute和赋值),那么该代码可能是UB,因为我们可能希望编译器能够"预取"共享引用后面的值并稍后重新使用该值(即在内联之后).

但是在你的代码中,我们只会noaliastransmute返回后发出"只读"注释(在LLVM中),并且数据确实是从那里开始的只读.所以,这应该是好的.

记忆模型

我的内存模型中"最具侵略性"的实质上断言所有值都是有效的,我认为即使该模型对你的代码也应该没问题.&UnsafeCell是有效性刚刚停止的模型中的一个特例,没有说明这个参考背后的内容.transmute返回的那一刻,我们抓住它所指向的内存并将其全部设为只读,即使我们通过"递归"执行了Rc(我的模型没有,但只是因为我无法找到一个好的这样做的方法)你会很好,因为你不会在之后发生变异transmute.(您可能已经注意到,这与编译器透视图中的限制相同.这些模型的重点是允许编译器优化.)

(作为旁注,我真的希望miri现在处于更好的状态.似乎我必须尝试再次验证才能在那里工作,因为那时我可以告诉你只需在miri中运行你的代码它就会告诉你如果我的模型的那个版本对你正在做的事情是好的:D)

我正在考虑目前只检查"访问时"的其他模型,但UnsafeCell尚未找到该模型的故事.这个例子显示的是模型可能必须包含内存首先存在"相变"的方式UnsafeCell,但后来与只读保证正常共享.感谢您提出这一点,这将有一些很好的例子可以考虑!

所以,我想我可以说(至少从我这边)有意图允许这种代码,这样做似乎并没有阻止任何优化.无论我们是否真的设法找到一个每个人都同意的模型,并且仍然允许这样,我无法预测.

相反的方向: T -> UnsafeCell<T>

现在,这更有趣.问题在于,正如我上面所说的那样,&T在写作时你不能有生活UnsafeCell<T>.但"生活"在这里意味着什么?这是个难题!在我的一些模型中,这可能像"某种类型的引用存在于某处且生命周期仍然活跃"一样弱,即它可能与实际使用的引用无关.(因为它可以让我们做更多的优化,如移动负载了一个循环的,即使我们不能证明环路愿意冒这是有用的-这将引入使用其他未使用的参考).既然&T就是Copy,你甚至不能真的摆脱了这样的参考.所以,如果你有x: &T,那么之后let y: &UnsafeCell<T> = transmute(x),旧x的仍然存在,它的生命仍然活跃,所以写作y很可能是UB.

我认为你必须以某种方式限制&T允许的别名,非常仔细地确保没有人仍然拥有这样的参考.我不会说"这是不可能的",因为人们总是让我感到惊讶(特别是在这个社区;)但TBH我想不出一种方法来使这项工作.如果你有一个例子,虽然你认为这是合理的,但我很好奇.