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>
。但是,“活着”在这里是什么意思?这是一个很难的问题!在我的某些模型中,这可能会像“该类型的引用存在于某处且生命周期仍处于事件状态”一样弱,即与引用是否实际使用无关。 (这很有用,因为它使我们能够进行更多优化,例如即使无法证明循环曾经运行过,也将负载移出循环,这会引入对未使用的引用的使用。)由于&T
是Copy
,因此您甚至也不能真正摆脱这样的引用。因此,如果您有x: &T
,那么在let y: &UnsafeCell<T> = transmute(x)
之后,旧的x
仍然存在,并且其生命周期仍然有效,因此通过y
进行写入很可能是UB。
我认为您必须以某种方式限制&T
允许的别名,非常小心地确保没有人仍然拥有这样的引用。我不会说“这是不可能的”,因为人们总是让我感到惊讶(尤其是在这个社区中;),但是TBH我想不出一种方法来完成这项工作。如果您有一个示例,但您认为这是合理的,我会很好奇。
关于rust - 在T和UnsafeCell <T>之间转换是安全且定义好的行为吗?,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/50431702/