Pin与Box对比:为什么仅使用Box无法保障Rust内存安全
先澄清PhantomPinned的认知误区
PhantomPinned本身没有编译期强制禁止移动的能力,它只是一个类型标记,用来告诉编译器:当前类型不满足「移动后仍能正常工作」的Unpin约束,当该类型被Pin包裹时,安全API需要阻止可导致内存移动的操作。
你给出的示例代码可以正常编译的原因非常直接:整个代码流程里从来没有把MyStruct包裹进Pin指针,只是在栈上以裸值的形式跨函数传递,此时PhantomPinned不会产生任何拦截效果,值的移动完全是合法操作。
裸Box的本质安全缺陷
Box确实会把内部的值存储在堆上,但这不代表堆上的值不会被移动。Box作为普通的所有权智能指针,会对外暴露完整的&mut T可变引用权限,你可以通过安全代码任意移动内部的T值,一旦T存在对自身内存地址的依赖,就会触发未定义行为。
最典型的场景就是自引用结构体,以下是可复现的问题代码:
struct SelfRef { data: String, // 存一个指向自身data字段的指针 data_ptr: *const String, } impl SelfRef { fn new(content: &str) -> Self { Self { data: String::from(content), data_ptr: std::ptr::null(), } } // 初始化自引用指针 fn init(&mut self) { self.data_ptr = &self.data; } fn get_content(&self) -> &str { unsafe { &*self.data_ptr } } } fn main() { let mut boxed = Box::new(SelfRef::new("test")); boxed.init(); println!("初始化后读取内容: {}", boxed.get_content()); // 正常输出test // 安全代码即可把Box内部的值移动到栈上 let moved_val = *boxed; // 此时原Box堆内存里的data已经被移走,data_ptr变成悬垂指针 println!("移动后读取原Box内容: {}", boxed.get_content()); // 触发未定义行为 }
除了直接把值移出Box,你还可以通过std::mem::replace、std::mem::swap等操作替换Box内部的内容,这些操作都会改变T实际存储的内存位置,而Box本身没有任何机制阻止这些操作。对于依赖固定内存地址的类型(自引用结构、侵入式链表节点、绑定固定物理地址的硬件映射结构),裸Box无法提供任何安全保证。
Pin的安全保障逻辑
Pin本质上不是新的内存分配方式,它是一层指针权限包装,核心规则非常简单:
- 对于实现了
Unpintrait的普通类型,Pin不做任何限制,和普通指针使用体验一致 - 对于没有实现
Unpin的类型(添加PhantomPinned标记就会让类型自动变成!Unpin),Pin的安全API永远不会暴露&mut T可变引用给用户,而没有&mut T,你就无法在安全代码里执行移动、替换、swap等会改变T内存位置的操作。
用Pin改造上面的自引用结构,就可以在安全层面阻止非法移动:
use std::marker::PhantomPinned; use std::pin::Pin; struct SafeSelfRef { data: String, data_ptr: *const String, _pin: PhantomPinned, } impl SafeSelfRef { fn new(content: &str) -> Self { Self { data: String::from(content), data_ptr: std::ptr::null(), _pin: PhantomPinned, } } // 初始化必须接收Pin包裹的可变自身引用,保证初始化后值不会被移动 fn init(self: Pin<&mut Self>) { // 这一步是unsafe,需要开发者自己保证初始化后不破坏Pin的约定 let this = unsafe { self.get_unchecked_mut() }; this.data_ptr = &this.data; } fn get_content(&self) -> &str { unsafe { &*self.data_ptr } } } fn main() { let mut pinned_box = Box::pin(SafeSelfRef::new("test")); pinned_box.as_mut().init(); println!("初始化后读取内容: {}", pinned_box.get_content()); // 正常输出test // 以下操作都会在编译期直接报错,安全代码无法执行 // let moved_val = *pinned_box; // std::mem::replace(&mut *pinned_box, SafeSelfRef::new("new")); }
需要额外说明的是,Pin不需要强制把值存在堆上,Pin<&mut T>也可以用来固定栈上的值,只是栈上Pin的生命周期约束更难维护,所以绝大多数场景下会搭配Box使用,确保被Pin住的值可以在程序里任意传递,同时内存地址始终保持固定。
内容的提问来源于stack exchange,提问作者unegare

