rust语言学习笔记(指针十二)Pin(固定内存地址)

Pin 是一个用于处理‌自引用结构(Self-referential structs)‌和‌异步编程‌的核心工具。它的核心作用是:‌**保证被包裹的值在内存中的地址不会发生改变(即"不可移动")**‌,从而防止因对象移动导致的悬垂指针问题。

Pin<P> 是一个包装类型,它保证被包装的指针 P 所指向的值不会被移动。具体来说,当我们拿到 Pin<&mut T>Pin<Box<T>> 时,无法通过 safe 代码拿到 &mut T,从而无法使用 swapreplace 等会移动数据的方法,除非 T 实现了 Unpin

12.1 Unpin 与 !Unpin

  • Unpin 是一个自动 trait,几乎所有的标准类型(i32StringVec<T> 等)都实现了它。实现了 Unpin 的类型,即使被 Pin 包裹,也可以安全地移动,Pin 对它们没有任何限制。
  • 没有实现 Unpin 的类型(通常用 !Unpin 表示)才是真正需要被"钉住"的类型。在 Pin 包裹下,它们无法被移动,从而保证了内部指针的稳定。

如何让一个类型变为 !Unpin?最简单的办法是加入一个 PhantomPinned 字段:

rust 复制代码
use std::marker::PhantomPinned;

struct MyStruct {
    data: String,
    _pinned: PhantomPinned,
}

PhantomPinned 不实现 Unpin,因此包含它的结构体也不会自动实现 Unpin

12.2 安全构造 Pin 的常见方式

12.2.1 Pin::new ------ 仅适用于 Unpin 类型

rust 复制代码
let value = 42;
let pinned = Pin::new(&value); // value 是 i32,实现了 Unpin

对于 !Unpin 类型,Pin::new 无法使用,因为该方法要求 T: Unpin

12.2.2 Box::pin ------ 将值固定在堆上(推荐)

rust 复制代码
let mut pinned = Box::pin(MyStruct {
    data: String::from("hello"),
    _pinned: PhantomPinned,
});

这样 pinned 里的值就被固定在堆上,不会被移动。

12.2.3 Pin::new_unchecked ------ 不安全构造

rust 复制代码
let mut value = MyStruct { data: String::from("hello"), _pinned: PhantomPinned };
let pinned = unsafe { Pin::new_unchecked(&mut value) };

需要开发者自己保证该值在 Pin 存在期间不会被移动,通常不推荐。

12.3 安全的自引用结构体

rust 复制代码
use std::marker::PhantomPinned;
use std::pin::Pin;
use std::ptr::NonNull;

struct SelfRefNode {
    data: String,
    // 指向 data 内部字节的指针
    self_ref: Option<NonNull<u8>>, // 指向 data 内部字节的指针, 用于在 Pin 内部访问 data
    _pinned: PhantomPinned,        // 结构体实现 !Unpin
}

impl SelfRefNode {
    /// 创建并固定一个自引用节点
    pub fn new(data: String) -> Pin<Box<Self>> {
        let mut boxed = Box::pin(SelfRefNode {
            // 并将其固定到 Pin 中
            data,
            self_ref: None,
            _pinned: PhantomPinned, // 结构体实现 !Unpin
        });

        // 在 Pin 的上下文中初始化自引用指针
        let self_ptr: *const u8 = boxed.data.as_ptr(); // 获取 data 内部字节的指针
        unsafe {
            let mut_ref = Pin::as_mut(&mut boxed).get_unchecked_mut(); // 获取 Pin 内部的可变引用
            mut_ref.self_ref = NonNull::new(self_ptr as *mut u8); // 初始化 self_ref 指向 data 内部字节的指针
        }

        boxed
    }

    /// 安全读取数据
    pub fn data(self: Pin<&Self>) -> &str {
        &self.get_ref().data // 安全读取 data 字段
    }

    /// 通过自引用指针读取数据,验证自引用的正确性
    pub fn data_via_ref(self: Pin<&Self>) -> &str {
        let this = self.get_ref(); // 获取 Pin 内部的引用
        let ptr = this.self_ref.expect("指针未初始化"); // 获取 self_ref 指向的指针
        unsafe {
            let slice = std::slice::from_raw_parts(ptr.as_ptr(), this.data.len()); // 从指针创建一个原始切片
            std::str::from_utf8(slice).unwrap() // 从原始切片创建一个字符串
        }
    }
}

fn main() {
    let node = SelfRefNode::new("hello pin".to_string());

    println!("直接读取: {}", node.as_ref().data());
    println!("通过自引用指针读取: {}", node.as_ref().data_via_ref());

    // 尝试移动 node 会导致编译错误,因为 Pin<Box<SelfRefNode>> 没有实现 Unpin
    // let moved = node; // 错误!
}

12.4 异步编程中的 Pin 实例

Future trait 的 poll 方法签名是 fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>。这让异步运行时可以安全地轮询一个可能具有自引用状态的 Future

下面是一个简单的异步计时器,它内部需要跨 await 保持引用,因此必须使用 Pin

rust 复制代码
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
use std::time::{Duration, Instant};

struct Timer {
    end_time: Instant, // 定时器结束时间
    started: bool,     // 定时器是否已启动
}

impl Timer {
    fn new(duration: Duration) -> Self {
        Timer {
            end_time: Instant::now() + duration, // 计算定时器结束时间
            started: false,                      // 初始化定时器为未启动状态
        }
    }
}

impl Future for Timer {
    // 实现 Future trait
    type Output = (); // 定时器完成时返回的类型

    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
        // 实现 poll 方法
        // 如果定时器未启动,启动定时器
        if !self.started {
            self.started = true; // 标记定时器为已启动
            // 通知 waker,在指定时间后唤醒
            let waker = cx.waker().clone(); // 克隆 waker,确保在子线程中唤醒
            let end_time = self.end_time; // 记录定时器结束时间
            std::thread::spawn(move || {
                std::thread::sleep(end_time - Instant::now()); // 等待定时器结束时间
                waker.wake(); // 唤醒任务
            });
            Poll::Pending // 定时器未到,返回 Pending
        } else if Instant::now() >= self.end_time {
            Poll::Ready(()) // 定时器到,返回 Ready(())
        } else {
            Poll::Pending // 定时器未到,返回 Pending   
        }
    }
}

// 使用示例
#[tokio::main]
async fn main() {
    let timer = Timer::new(Duration::from_secs(2)); // 创建一个 2 秒的定时器
    timer.await; // 等待定时器到
    println!("2 秒后打印");
}

在这个例子中,Timer 虽然本身不包含自引用,但它的 poll 方法接收 Pin<&mut Self>,符合 Future 规范。对于那些包含跨 await 引用的 FuturePin 会保证它们在轮询过程中不被移动,从而保护内部指针安全。

12.5 总结

  • Pin 解决的是自引用结构体因移动导致指针悬垂的问题,在异步编程中尤为关键。
  • Unpin 类型可以安全移动,Pin 对它们透明;!Unpin 类型在 Pin 包裹下被禁止移动。
  • 构造 Pin 的常用方式是 Box::pin(堆上固定)或 Pin::new_unchecked(需自行保证安全)。
  • Future 的实现中,poll 方法接收 Pin<&mut Self>,保证了状态机内部跨 await 引用的有效性。

通过上面这些机制和实例,你应该对 Pin 的设计意图和实际用法有了比较清晰的认识。