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

时间:2018-05-20 04:36:02

标签: rust undefined-behavior unsafe

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内存是否安全?它会调用未定义的行为吗?那么从相反的方向转换,从TUnsafeCell<T>

1 个答案:

答案 0 :(得分:6)

(我还是新手,并且不确定“好吧,也许”是否有资格作为答案,但在这里你去。;)

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

另外,我看到两个部分:数据布局透视图和别名透视图。布局问题是NotReallyImmutable原则上可能具有与ReallyImmutable完全不同的布局。我对数据布局了解不多,但UnsafeCell成为repr(transparent)并且这是两种类型之间的唯一区别,我认为 intent 是为了工作。但是,你应该依赖于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,但后来与只读保证正常共享。感谢您提出这一点,这将有一些很好的例子可以考虑!

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

反方向:T -> UnsafeCell<T>

现在,这更有趣。问题在于,正如我上面所说,在通过&T写作时,您不能有UnsafeCell<T>个直播。但“生活”在这里意味着什么?这是个难题!在我的一些模型中,这可能像“某种类型的引用存在于某处并且生命周期仍然活跃”一样弱,即,它可能与引用是否实际使用无关 。 (这很有用,因为它允许我们做更多的优化,比如将负载移出循环,即使我们无法证明循环运行 - 这将引入使用其他未使用的引用。)并且因为&TCopy,你甚至不能真正摆脱这样的引用。所以,如果您有x: &T,那么在let y: &UnsafeCell<T> = transmute(x)之后,旧的x仍然存在并且其生命周期仍然有效,因此通过y写入可能是UB。

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