You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Pin与Box对比:为什么仅使用Box无法保障Rust内存安全

Box的安全缺陷与Pin的防护逻辑

先澄清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本质上不是新的内存分配方式,它是一层指针权限包装,核心规则非常简单:

  • 对于实现了Unpin trait的普通类型,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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.31 20:15:44