Rust 中结构字段的多态更新
Polymorphic update on struct fields in Rust
假设我有一个多态类型 T<A>
:
#[repr(C)]
pub struct T<A> {
x: u32,
y: Box<A>,
}
以下是我的推理:
根据Memory Layout Section of std::boxed
:
So long as T: Sized
, a Box<T>
is guaranteed to be represented as a single pointer and is also ABI-compatible with C pointers (i.e. the C type T*
).
所以无论A
是什么,y
都应该有相同的布局;
另外考虑 T
上的 #[repr(C)]
attribute,我希望对于所有 A
,T<A>
将共享相同的布局;
因此我应该能够就地修改 y
,甚至是不同类型的值 Box<B>
.
我的问题是以下代码是否格式正确或具有未定义的行为? (下面的代码已经过编辑。)
fn update<A, B>(t: Box<T<A>>, f: impl FnOnce(A) -> B) -> Box<T<B>> {
unsafe {
let p = Box::into_raw(t);
let a = std::ptr::read(&(*p).y);
let q = p as *mut T<B>;
std::ptr::write(&mut (*q).y, Box::new(f(*a)));
Box::from_raw(q)
}
}
注:
以上代码旨在就地执行多态更新,以便x
字段保持原样。想象一下 x
不仅仅是一个 u32
,而是一些非常大的数据块。整个想法是在不触及字段 x
.
的情况下更改 y
的类型(及其值)
正如 Frxstrem 所指出的,下面的代码确实会导致未定义的行为。我犯了一个愚蠢的错误:我忘记为 f
生成的 B
重新分配内存。好像是the new code above passes Miri check.
fn update<A, B>(t: Box<T<A>>, f: impl FnOnce(A) -> B) -> Box<T<B>> {
unsafe {
let mut u: Box<T<std::mem::MaybeUninit<B>>> = std::mem::transmute(t);
let a = std::ptr::read::<A>(u.y.as_ptr() as *const _);
u.y.as_mut_ptr().write(f(a));
std::mem::transmute(u)
}
}
你把自己和整个 T
搞混了。这应该更容易分析。具体来说,阅读here.
use core::mem::ManuallyDrop;
use core::alloc::Layout;
unsafe trait SharesLayout<T: Sized>: Sized {
fn assert_same_layout() {
assert!(Layout::new::<Self>() == Layout::new::<T>());
}
}
/// Replaces the contents of a box, without reallocating.
fn box_map<A, B: SharesLayout<A>>(b: Box<A>, f: impl FnOnce(A) -> B) -> Box<B> {
unsafe {
B::assert_same_layout();
let p = Box::into_raw(b);
let mut dealloc_on_panic = Box::from_raw(p as *mut ManuallyDrop<A>);
let new_content = f(ManuallyDrop::take(&mut *dealloc_on_panic));
std::mem::forget(dealloc_on_panic);
std::ptr::write(p as *mut B, new_content);
Box::from_raw(p as *mut B)
}
}
然后简单地:
unsafe impl<A, B> SharesLayout<T<A>> for T<B> {}
fn update<A, B>(bt: Box<T<A>>, f: impl FnOnce(A) -> B) -> Box<T<B>> {
box_map(bt, |t| {
T { x: t.x, y: Box::new(f(*t.y))}
})
}
假设我有一个多态类型 T<A>
:
#[repr(C)]
pub struct T<A> {
x: u32,
y: Box<A>,
}
以下是我的推理:
根据Memory Layout Section of
std::boxed
:So long as
T: Sized
, aBox<T>
is guaranteed to be represented as a single pointer and is also ABI-compatible with C pointers (i.e. the C typeT*
).所以无论
A
是什么,y
都应该有相同的布局;另外考虑
T
上的#[repr(C)]
attribute,我希望对于所有A
,T<A>
将共享相同的布局;因此我应该能够就地修改
y
,甚至是不同类型的值Box<B>
.
我的问题是以下代码是否格式正确或具有未定义的行为? (下面的代码已经过编辑。)
fn update<A, B>(t: Box<T<A>>, f: impl FnOnce(A) -> B) -> Box<T<B>> {
unsafe {
let p = Box::into_raw(t);
let a = std::ptr::read(&(*p).y);
let q = p as *mut T<B>;
std::ptr::write(&mut (*q).y, Box::new(f(*a)));
Box::from_raw(q)
}
}
注:
以上代码旨在就地执行多态更新,以便x
字段保持原样。想象一下 x
不仅仅是一个 u32
,而是一些非常大的数据块。整个想法是在不触及字段 x
.
y
的类型(及其值)
正如 Frxstrem 所指出的,下面的代码确实会导致未定义的行为。我犯了一个愚蠢的错误:我忘记为 f
生成的 B
重新分配内存。好像是the new code above passes Miri check.
fn update<A, B>(t: Box<T<A>>, f: impl FnOnce(A) -> B) -> Box<T<B>> {
unsafe {
let mut u: Box<T<std::mem::MaybeUninit<B>>> = std::mem::transmute(t);
let a = std::ptr::read::<A>(u.y.as_ptr() as *const _);
u.y.as_mut_ptr().write(f(a));
std::mem::transmute(u)
}
}
你把自己和整个 T
搞混了。这应该更容易分析。具体来说,阅读here.
use core::mem::ManuallyDrop;
use core::alloc::Layout;
unsafe trait SharesLayout<T: Sized>: Sized {
fn assert_same_layout() {
assert!(Layout::new::<Self>() == Layout::new::<T>());
}
}
/// Replaces the contents of a box, without reallocating.
fn box_map<A, B: SharesLayout<A>>(b: Box<A>, f: impl FnOnce(A) -> B) -> Box<B> {
unsafe {
B::assert_same_layout();
let p = Box::into_raw(b);
let mut dealloc_on_panic = Box::from_raw(p as *mut ManuallyDrop<A>);
let new_content = f(ManuallyDrop::take(&mut *dealloc_on_panic));
std::mem::forget(dealloc_on_panic);
std::ptr::write(p as *mut B, new_content);
Box::from_raw(p as *mut B)
}
}
然后简单地:
unsafe impl<A, B> SharesLayout<T<A>> for T<B> {}
fn update<A, B>(bt: Box<T<A>>, f: impl FnOnce(A) -> B) -> Box<T<B>> {
box_map(bt, |t| {
T { x: t.x, y: Box::new(f(*t.y))}
})
}