Rust Pin:地址稳定性到底在保证什么
我一开始对 Pin 的理解是错的。我以为它会把内存锁住,后来发现它连「禁止移动」都不完全成立——Pin<Box<T>> 这个变量本身照样可以搬来搬去。
真正的结论只有一句:
Pin 不是锁内存,也不是禁止所有移动。它是一套类型系统约定,用来保证某些值在被固定期间不会被移动出原来的内存位置。
为什么需要这种保证?得从 Rust 的普通移动语义说起。
一、Rust 的「移动」到底做了什么
struct User {
name: String,
age: u32,
}
fn main() {
let a = User {
name: String::from("Alice"),
age: 18,
};
let b = a;
println!("{}", b.name);
}
执行 let b = a; 之后,a 的所有权转移给 b,a 不能再用了。
但这里有个关键问题:
移动一个 Rust 值,是不是意味着它拥有的堆内存也被搬走了?
不是。String 大致长这样:
String 对象
┌─────────────────┐
│ ptr │ ────────┐
│ len │ │
│ capacity │ │
└─────────────────┘ │
▼
堆内存
┌─────────────┐
│ Alice │
└─────────────┘
移动时 Rust 只需搬走那三个字的头,字符本身原地不动。
但如果结构体里有个指针指向自己,事情就不一样了。
1.1 自引用结构体的问题
struct SelfRef {
data: String,
ptr: *const String,
}
我想让 ptr 指向结构体自己的 data 字段:
SelfRef 位于地址 A
┌──────────────────────────┐
│ data: String │
│ │
│ ptr ─────────────────────┼──┐
└──────────────────────────┘ │
│
▼
data 字段
如果结构体被移到地址 B,而 ptr 还指着地址 A:
原地址 A 新地址 B
┌───────────────┐ ┌───────────────┐
│ 旧对象的内存 │ │ 新对象 │
│ │ │ data │
│ ptr ──────────┼──────┐ │ │
└───────────────┘ │ └───────────────┘
│
▼
原地址 A
原地址上的对象一旦失效,ptr 就变成悬垂指针。
注意这里有两个细节:
- 不是所有自引用结构体都会因为移动出错——关键在于内部引用是否依赖外层对象的固定地址。
- 上面这个
SelfRef只是展示问题的骨架,它并没有真的建立自引用关系(ptr还是空指针)。
二、为什么 &mut T 不够
既然有借用检查器,为什么还非要 Pin?
因为借用检查器管的是生命周期和别名,它并不会阻止值被搬到新地址。let b = a; 这种移动完全合法。
而自引用对象需要的是另一层保证:
对象被固定之后,在特定期间内,不能通过安全操作把它移动到别的内存位置。
这叫地址稳定性。
有些类型不在乎自己住哪:u64、String、Vec<u8> 都可以随便搬。另一些类型则依赖地址:
- 包含指向自身字段的原始指针的结构体;
- 某些底层异步状态机;
- 依赖固定地址的 FFI 对象;
- 侵入式链表节点;
- 保存指向自身内部数据的回调注册信息。
具体的冲突点在这里:
struct Data {
value: String,
}
let mut data = Data { value: String::from("hello") };
let r: &mut Data = &mut data;
r.value.push_str(" world"); // 改字段,没问题
*r = Data { value: String::from("new") }; // 替换整个对象
两种操作都合法。但一旦 Data 内部有指向自身的指针,第二种操作就可能让内部指针失效。
所以需要一种更强的引用:Pin<&mut Data>。它的意思不是「我有一个可变引用」,而是:
我有一个指向已固定对象的可变引用,并且在固定约定生效期间,不能通过安全操作把这个对象移出它的固定位置。
关键字是「移出对象」,不是「禁止修改对象」。固定住的对象照样可以有可变状态。
顺带说一句:Pin 不是实现地址稳定的唯一办法。你也可以把对象放在永不移动的堆分配里,并且不暴露会破坏不变量的接口。Pin 的价值在于能把这类约定写进类型系统。
三、Pin 到底是什么
use std::pin::Pin;
泛型参数 P 通常是指针类型:
Pin<Box<T>>
Pin<&mut T>
Pin<Arc<T>>| 类型 | 含义 |
|---|---|
Box<T> | 拥有堆上 T 的所有权 |
Pin<Box<T>> | 通过 Box 拥有一个被固定的 T |
&mut T | 对 T 的独占可变借用 |
Pin<&mut T> | 对一个被固定的 T 的独占可变借用 |
最重要的一句话:
Pin 固定的是指针所指向的值,不是指针对象自身的地址。
let value = Box::pin(42);栈
┌─────────────────┐
│ Pin<Box<i32>> │
└────────┬────────┘
│
▼
堆
┌─────────────────┐
│ i32 = 42 │
└─────────────────┘
Pin<Box<i32>> 通过 Box 指向堆上的整数。栈上那个 Pin<Box<i32>> 变量随便搬,堆上的 i32 不动。
但这里还有个坑:i32 是 Unpin 的,所以 Pin<Box<i32>> 并不会真的阻止你通过合法途径把它取出来。这就引出了 Unpin。
四、Unpin:什么时候不需要固定
pub auto trait Unpin {}
Unpin 是个 auto trait,含义很简单:
T: Unpin表示这个类型不依赖 Pin 提供的固定地址保证。
绝大多数普通类型都是 Unpin:
fn assert_unpin<T: Unpin>() {}
fn main() {
assert_unpin::<u32>();
assert_unpin::<String>();
assert_unpin::<Vec<u8>>();
}
这些类型随便移动都没问题,Pin 对它们而言形同虚设。
反过来,需要地址稳定的类型可以用 PhantomPinned 阻止自动实现 Unpin:
use std::marker::PhantomPinned;
struct SelfRef {
data: String,
ptr: *const String,
_pin: PhantomPinned,
}
PhantomPinned 是零大小标记类型,但作用和 PhantomData<T> 完全不同:
PhantomData<T>表达所有权、生命周期、型变这些类型系统关系;PhantomPinned只做一件事——阻止类型自动实现Unpin。
PhantomPinned 本身不会把对象固定在任何地址。 它只改变类型的 Unpin 属性。
let value = SelfRef {
data: String::from("hello"),
ptr: std::ptr::null(),
_pin: PhantomPinned,
};
这个对象仍然可以自由移动,因为还没有任何 Pin 参与。
两个机制要分清:
PhantomPinned
│
└── 阻止类型自动实现 Unpin
Pin<P>
│
└── 在安全约定成立时,固定指针指向的值
五、Pin<Box<T>> 与 Pin<&mut T>
5.1 Pin::new 与 Box::pin
这两个 API 经常被搞混。
use std::pin::Pin;
let mut value = 42;
let pinned = Pin::new(&mut value); // Pin<&mut i32>
Pin::new 不分配内存,它只是把现有引用包一层。标准库要求传入的指针目标必须 Unpin——因为只有这样,包装才不需要额外的保证。
let value = Box::pin(42); // Pin<Box<i32>>
Box::pin 会把值放到堆上再固定。对 !Unpin 类型,这是最常见的固定方式。
但固定不是单纯由「在堆上」决定的。普通 Box<T> 也能搬所有权、也能在满足规则时把 T 取出来;Pin<Box<T>> 对 !Unpin 类型施加的约束要强得多。
区别不只是堆分配,而是类型系统表达的契约。
5.2 移动的是指针所有者
let future1 = Box::pin(some_future());
let future2 = future1;栈上的指针对象
┌──────────────────┐
│ Pin<Box<Future>> │
└────────┬─────────┘
│
▼
堆上的 Future
┌──────────────────┐
│ Future 状态机 │
└──────────────────┘
移动 Pin<Box<Future>> 只是换了个变量持有这个指针,堆上的 Future 一动没动。
这也解释了为什么堆分配总跟 Pin 一起出现:搬指针所有者,不等于搬被指向的对象。
六、get_mut 与 get_unchecked_mut
6.1 get_mut 为什么要求 Unpin
pub fn get_mut(self) -> &mut T
where
T: Unpin,
为什么有这个约束?因为普通 &mut T 允许你替换整个对象:
*value = another_value;
如果 T 依赖固定地址,这个操作就可能毁掉内部自引用。
我实测过:给一个含 PhantomPinned 的类型调 get_mut(),编译器直接拒绝。
error[E0277]: `PhantomPinned` cannot be unpinned
所以标准库用 Unpin 把情况分成两类:
T: Unpin:可以安全地拿普通可变引用;T: !Unpin:安全 API 不给你这个引用。
这是编译期约束,不是运行时检查。
6.2 get_unchecked_mut 为什么是 unsafe
pub unsafe fn get_unchecked_mut(self) -> &mut T
它不要求 Unpin,代价是你得自己保证后续操作不会破坏固定地址约定。
危险在哪?拿到 &mut T 之后,这两件事都可能发生:
*value = another_value;
let moved = std::mem::replace(value, another_value);
都可能把对象整体换掉或搬走。
正确用法只有四步:
- 通过 unsafe 拿到普通可变引用;
- 只修改不会破坏固定约定的字段;
- 不把整个对象移走;
- 不通过其他路径绕过约定。
这和 ptr::write() 有点像:都能做底层操作,但调用者必须证明操作符合契约。区别是职责不同——ptr::write 管写入不管旧值析构,get_unchecked_mut 管绕过固定限制不管后续移动。
七、完整构造一个自引用类型
前面都是骨架,现在来一个能跑起来的。
use std::marker::PhantomPinned;
use std::pin::Pin;
use std::ptr;
struct SelfRef {
data: String,
ptr: *const String,
_pin: PhantomPinned,
}
impl SelfRef {
fn new(data: String) -> Pin<Box<Self>> {
let mut value = Box::pin(Self {
data,
ptr: ptr::null(),
_pin: PhantomPinned,
});
let self_ptr: *const String = &value.data;
// SAFETY: value 已经被 Box::pin 固定,
// 这里只修改 ptr 字段,不移动 SelfRef 本身。
unsafe {
value.as_mut().get_unchecked_mut().ptr = self_ptr;
}
value
}
fn data(self: Pin<&Self>) -> &str {
&self.get_ref().data
}
fn pointer_is_valid(self: Pin<&Self>) -> bool {
self.ptr == &self.data as *const String
}
}
fn main() {
let value = SelfRef::new(String::from("hello"));
assert_eq!(value.as_ref().data(), "hello");
assert!(value.as_ref().pointer_is_valid());
}
拆开看三步。
第一步:先分配,再固定
let mut value = Box::pin(Self {
data,
ptr: ptr::null(),
_pin: PhantomPinned,
});
Box::pin 把对象放到堆上并返回 Pin<Box<Self>>。因为 Self 含 PhantomPinned,它不会自动实现 Unpin,后续安全代码不能把它从固定位置取出来。
第二步:建立内部指针
let self_ptr: *const String = &value.data;
unsafe {
value.as_mut().get_unchecked_mut().ptr = self_ptr;
}
这里必须 unsafe。get_unchecked_mut() 不会替你保证后续操作遵守固定约定——我们只改 ptr 字段、不动 SelfRef 本身,所以安全。
第三步:只通过固定引用访问
fn data(self: Pin<&Self>) -> &str
方法用 Pin<&Self> 接收,表达「调用者访问的是一个已经被固定的对象」。
不过这个例子依然依赖实现者守规矩。Pin 不是运行时的内存保护,它不会检查你的裸指针是否还有效。
八、字段修改 vs 字段替换
这段我以前混淆过,值得单独说。
struct Data {
value: String,
}
对象已经固定,现在只想改 value。两种写法的语义不一样:
value.push_str("hello"); // A
self.value = String::from("new"); // B
- A 改的是
String管的堆缓冲区,可能触发扩容重新分配,但外层Data对象没动。 - B 替换了
String字段的值,外层Data也不必然移动,但会影响那些指向旧字段值或旧缓冲区的指针。
所以对自引用类型,不能只问「外层对象动没动」,还得问:
- 内部指针指向的是外层对象的字段地址,还是字段拥有的堆缓冲区?
- 被替换字段的地址稳定吗?
- 被指向的数据还有效吗?
- 这次修改会不会破坏别的字段维护的不变量?
举例:如果内部指针指向 String 对象本身,那么替换该字段就断了关系;如果指向字符缓冲区,那么 String 扩容时缓冲区换地址,指针同样失效。
Pin 固定的是外层对象的地址,不会自动固定它拥有的所有间接分配。 这点在 FFI 和自引用设计里特别关键。
九、Pin 与 Drop
对象最终总要销毁。被 Pin 住的对象当然也能 Drop。
use std::marker::PhantomPinned;
struct Resource {
value: String,
_pin: PhantomPinned,
}
impl Drop for Resource {
fn drop(&mut self) {
println!("Dropping: {}", self.value);
}
}
但 Drop::drop 收到的是 &mut self,不是 Pin<&mut Self>。这不是漏洞。
Pin 的契约要求是:如果类型依赖固定地址,它的安全实现不能在对象还处于 Pin 保证范围内时把对象整体移出原位置。
正常析构当然可以释放内部资源,析构结束后存储空间也可以被回收。但不能在析构过程中偷偷把整个 !Unpin 对象搬到别处,然后假装 Pin 保证还成立。
所以如果你写了一个依赖地址稳定的 !Unpin 类型,下面这些都得想清楚:
- 构造函数怎么建立自引用;
- 公共方法怎么避免移动对象;
- 内部修改怎么维护指针关系;
Drop怎么清理依赖固定地址的资源;- 有没有
mem::replace、ptr::read这类绕过路径。
这跟上几节 Vec、MaybeUninit、Drop Check 是同一类问题:你必须清楚对象在每个生命周期阶段归谁所有,哪些不变量必须成立。
十、安全性为什么依赖类型实现者
这是 Pin 最容易被忽略的一层。
struct MyType {
data: String,
}
就算你把它装进 Pin<Box<MyType>>,也不代表所有代码都遵守了固定约定。
假设这个类型提供了这样一个安全方法:
impl MyType {
fn dangerous(self: Pin<&mut Self>) {
// 如果这里内部通过 unsafe 移动整个对象,
// 就会破坏 Pin 的安全约定
}
}
调用者完全按安全 API 的规则用,也可能踩到内存安全问题。
所以 Pin 的安全性不只是调用方的事。类型实现者必须保证自己暴露的安全接口不会破坏 pinning 约定。
这和设计安全的 FFI wrapper 是同一个道理:不能要求调用者先理解一堆隐藏的 unsafe 不变量,才能正确使用一个声称安全的 API。
十一、Pin Projection
这是 Pin 进阶里最重要的一块。
11.1 为什么不能直接取字段
use std::marker::PhantomPinned;
struct Inner {
value: String,
_pin: PhantomPinned,
}
struct Outer {
inner: Inner,
name: String,
_pin: PhantomPinned,
}
把 Outer 固定住,得到 Pin<Box<Outer>>。现在我想取 Pin<&mut Inner>,也就是把:
Pin<&mut Outer> → Pin<&mut Inner>
问题是:父对象被 Pin 住,不代表它的所有字段都必须遵守 Pin 约束。
struct Outer {
movable: String,
_pin: PhantomPinned,
}
Outer 是 !Unpin,但 movable 字段仍然是 Unpin,它可以在不移动父对象的前提下被单独替换。
所以不能想当然地认为「父对象固定了,字段就都固定了」。这不是 Pin 的语义。
要做投影,必须先回答:Outer::inner 这个字段是否也必须遵守固定地址契约?
- 是 → 安全 API 就不能允许它被移出原位置;
- 否 → 它可以移动,但你不能假装它也拿到了 Pin 保证。
11.2 map_unchecked_mut
impl Outer {
fn project_inner(self: Pin<&mut Self>) -> Pin<&mut Inner> {
unsafe { self.map_unchecked_mut(|outer| &mut outer.inner) }
}
}
Pin::map_unchecked_mut 是 unsafe 的。原因:调用者必须保证投影出来的字段在被 Pin 期间不会被移出原位置。
它本身不会阻止任何事,只是把维护不变量的责任交给你。
所以手写投影不只是写一个转换函数。你得把整个 API 过一遍:
- 有没有安全方法能移动这个字段?
- 能不能通过
&mut Self替换字段? - 能不能用
mem::replace移出字段? Drop会不会破坏不变量?- 还有没有别的路径能绕过投影约束?
漏一个,单独正确的投影函数也救不了整个类型。
11.3 工程上怎么写
生产代码一般用现成的投影工具,比如 pin-project:
use pin_project::pin_project;
use std::marker::PhantomPinned;
#[pin_project]
struct Outer {
#[pin]
inner: Inner,
name: String,
_pin: PhantomPinned,
}
#[pin]标记的是 pinned 字段;- 没标记的
name是普通字段; - 宏根据声明生成投影接口。
概念上,使用者会拿到类似这样的东西:
struct OuterProjection<'a> {
inner: Pin<&'a mut Inner>,
name: &'a mut String,
}
这是解释语义的伪代码,不是宏真实生成的完整形式。实际使用请照宏的文档来,别自己猜接口。
重点只有一个:pinned 字段和普通字段的访问方式不同。 对 pinned 字段不能随手给出一个能把它移走的普通可变引用。
十二、Future:为什么 poll 收 Pin<&mut Self>
pub trait Future {
type Output;
fn poll(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Self::Output>;
}
为什么不是 &mut self?
因为某些 Future 在第一次被 poll 之后,可能建立依赖自身地址的内部状态。如果调用者之后随意移动它,内部关系就断了。
Pin<&mut Self> 给这些 Future 提供了固定地址语义。
但别因此认为所有 Future 都要固定地址。很多 Future 是 Unpin 的,根本不依赖地址稳定。poll 收 Pin<&mut Self> 是为了让整个接口能容纳那些确实需要固定的实现。
也就是说,Pin 不是为了让异步任务动不了,而是为了让可能自引用的 Future 也能被纳入统一接口。
12.1 .await 做了什么
async fn fetch_data() -> String {
String::from("hello")
}
let future = fetch_data();
调用 fetch_data() 不会立刻执行函数体并返回 String。它生成一个实现 Future 的状态机对象:
Future 状态机
┌──────────────────────┐
│ 当前执行状态 │
│ 局部变量 │
│ 等待中的子 Future │
│ 恢复执行所需的数据 │
└──────────────────────┘
当它在某个 .await 处暂停时,必须把恢复执行所需的全部状态保存下来。如果这些状态之间存在自引用关系,那它在被 poll 期间就不能随便移动。
这就是 Pin 在异步里的核心价值。
12.2 跨越 await 的局部变量
同步函数:
fn example() {
let value = String::from("hello");
do_something();
println!("{}", value);
}
value 通常就是当前栈帧的一部分。
异步函数:
async fn example() {
let value = String::from("hello");
some_async_operation().await;
println!("{}", value);
}
执行停在 .await 时,后面还要用 value,编译器就必须把它存进 Future 的状态里,将来恢复执行时再用。
所以异步函数里的局部变量,有时会变成 Future 对象的一部分。如果 Future 内部还要保存指向这些局部状态的引用,地址稳定性就变得重要了。
但也不要反推成「只要有 .await,Future 就一定是自引用结构体」。编译器会按实际代码生成不同的状态结构,很多 Future 完全不需要自引用。
Pin 提供的是一个通用保证:即使某个 Future 需要依赖地址稳定,也能在这个统一接口下安全运行。
12.3 手写一个最小 Future
先来个不需要地址稳定的:
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
struct ReadyFuture {
value: i32,
}
impl Future for ReadyFuture {
type Output = i32;
fn poll(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<i32> {
let this = self.get_mut();
Poll::Ready(this.value)
}
}
ReadyFuture 会自动实现 Unpin,所以这里可以直接 get_mut() 拿普通可变引用。
如果类型是 !Unpin,这个安全接口就用不了了。
那能不能直接写 unsafe { self.get_unchecked_mut() }?技术上可以,但你必须自己保证:
- 不把依赖固定地址的整个对象移走;
- 不把 pinned 字段移出原位置;
- 不通过其他安全接口破坏已建立的地址关系。
别因为函数收了 Pin<&mut Self>,就以为函数内部可以随便移动 Self。unsafe 不消除约束,它只是把责任交给你。
12.4 Waker 解决的是另一个问题
Future 没完成时返回 Poll::Pending。那执行器怎么知道什么时候该再 poll 一次?
靠 Context 里的 Waker。Future 暂时无法推进时,通过 Context 拿到 Waker,将来有进展时唤醒执行器。
Executor
│
▼
Future::poll(Pin<&mut Future>, Context)
│
├── Poll::Ready(value)
│ └── Future 完成
│
└── Poll::Pending
│
▼
等待外部事件
│
▼
Waker 唤醒执行器
│
▼
再次 poll
Waker 不负责固定 Future 的地址。它和 Pin 是两件事:
| 机制 | 解决的问题 |
|---|---|
Pin | 对象是否需要保持在原来的内存位置 |
Waker | Future 有进展时怎么通知执行器重新轮询 |
Executor | 什么时候、在哪个执行上下文里轮询 Future |
Future | 保存异步计算状态,通过 poll 推进 |
十三、Pin 与 FFI
void register_callback(void *context);
C 库会保存传入的 context,之后在回调里继续用它。
struct CallbackContext {
state: i32,
}
let context = Box::pin(CallbackContext { state: 42 });
Box::pin 把对象放到堆上并固定住地址,C 保存的指针就不会因为 Rust 侧搬移而失效。
但 Pin 单独搞不定 FFI 安全。C 代码还可能:
- 在 Rust 对象销毁后继续访问旧指针;
- 把指针传给另一个线程;
- 重复调用回调;
- 回调还没结束就释放资源;
- 保存未经授权的别名;
- 违反 Rust 侧规定的调用顺序。
一个可靠的 FFI 设计还得明确:
- 谁拥有对象?
- 对象何时销毁?
- C 可以保存指针多久?
- 回调会不会并发?
- 允许重入吗?
- 允许从其他线程调用吗?
- 谁负责注销?
- 注销后怎么保证不再访问旧指针?
关键是要先弄清 C 端到底保存了什么:
情况 A:C 保存的是独立的稳定句柄(比如句柄 ID)。这种根本不需要 Pin,用句柄表加引用计数就够了。
情况 B:C 保存的是 Rust 对象本身的地址。如果对象注册后不能移动、C 又会长期访问,那固定地址才成为必要条件,才需要 Pin<Box<Context>>。
用 Pin 之前可以先问自己:能不能用一个独立稳定的句柄把 Rust 对象的移动隔离开?很多问题这样一绕就简单了。
不是所有涉及指针的问题都要上 Pin。先弄清要维护的不变量,再挑复杂度最低的方案。
十四、Pin 不能保证什么
这部分我特意写清楚,因为它比「Pin 能做什么」更容易出错。
14.1 不等于内存不释放
let pinned = Box::pin(42);
Box 被销毁时,指向的对象照样正常销毁。Pin 约束的是「在契约期内不被随意移动」,不是「永远存在」。
14.2 不等于线程安全
Pin<Box<T>> 不会让 T 自动变成 Send 或 Sync。这两个 trait 的含义一点没变。
14.3 不等于内部数据都固定
struct Example {
text: String,
}
即使 Example 被 Pin 住,String 内部的字符缓冲区也不由 Pin 保证永远在同一个地址。如果你保存了指向该缓冲区的裸指针,那缓冲区扩容导致的失效得你自己分析。
14.4 不等于借用检查器的替代品
Pin 不会帮你解决:生命周期错误、数据竞争、悬垂指针、重复释放、无效的 FFI 所有权、裸指针别名、错误的线程同步。
它只解决一类很具体的问题:对象地址稳定性,以及由此产生的移动约束。
十五、五个常见误解
误解一:Pin 会锁住内存。
不是。它不是操作系统级的内存锁,也不会阻止别的代码访问那个地址。它只是类型系统层面的约定。
误解二:Box::pin(T) 之后什么都不能动。
不是。Pin<Box<T>> 这个指针所有者本身可以移动;如果 T: Unpin,连 T 都能安全取出。
误解三:PhantomPinned 会自动固定对象。
不是。它只阻止类型自动实现 Unpin。真正的固定要靠 Pin 加上正确的构造与访问接口。
误解四:Pin 能替代 FFI 的生命周期管理。
不能。它拦不住 C 端提前释放,也保证不了跨线程访问安全。
误解五:!Unpin 就已经固定了。
不是。!Unpin 只是「可能依赖地址稳定」的类型标记。没有 Pin 参与时,它照样能被 let b = a; 搬走。
十六、总结
整条链路是这样的:
Rust 普通移动语义
│
▼
值可以在满足所有权规则时移动
│
▼
某些对象依赖自身地址
│
▼
需要地址稳定性保证
│
├── PhantomPinned
│ 阻止自动实现 Unpin
│
└── Pin<P>
提供固定地址约定
│
├── 自引用结构体
├── 某些异步 Future
├── 侵入式数据结构
└── 部分 FFI 对象
三个定义:
Pin<P>:对指针指向的值提供固定地址语义,前提是相关安全约定得到遵守。Unpin:表示类型不依赖 Pin 提供的固定地址保证。PhantomPinned:阻止类型自动实现Unpin,但自身不固定任何地址。
最后一句总结:
Pin 的核心不是「对象永远不能移动」,而是对于依赖固定地址的
!Unpin类型,Pin 通过类型系统与安全 API 约束,保证对象在固定期间不会被安全地移出原位置。
要用对它,得同时理解:Unpin 决定约束是否必要,PhantomPinned 负责关掉自动实现,Pin<P> 表达约定,unsafe 操作维护不变量,Drop 也得守约——而 FFI 的所有权、生命周期、线程安全,仍然要单独管。