1583 字
8 分钟
Rust Pin/Unpin 与 Move trait 争议

本文整理一种对 Move trait 的质疑:不可移动性更适合描述为值在特定内存位置上的状态。若将其作为类型分类,可能增加泛型约束和原地初始化的复杂度。

来源#

核心问题#

Rust 的 Pin<P> 机制是为了解决自引用结构、异步 Future 等对象在被观察到真实地址后不能再 relocation 的问题。争议在于:

  • 当前机制把“固定”放在指针包装类型 Pin<P> 上;
  • Roadmap 中讨论的 Move trait / !Move 方向倾向于把“能否移动”提升为类型分类;
  • 原文认为,更准确的语义应是 某个具体 Place 在运行流程中进入 pinned 状态,而不是类型天生属于 Move 或 !Move 家族。

Pin 的现状:库层机制#

Pin 的核心思想是:在不改变 Rust 底层 move 语义的前提下,用包装类型限制访问通路。

维度当前 Pin<P> 方案
位置标准库/类型层包装
语义Pin<P> 不给 !Unpin 目标暴露普通 &mut T
好处编译器底层 move 语义无需大改,兼容性好
坏处形成 &mut T 与 Pin<&mut T> 两套平行引用体系;field projection 依赖 pin_project! 等宏

简化理解:类型 T 自身不变,变化的是“你通过什么路径访问它”。当一个指针被包装进 Pin,且目标没有实现 Unpin 时,安全 API 不再允许拿到可移动的 &mut T。

Move trait 方向的问题#

如果引入默认 auto trait Move,并把不可移动性建模成 !Move,就等于把 Rust 类型划分为两个互斥集合:

U=FMove⊎FImmovable\mathcal{U} = \mathcal{F}_{\text{Move}} \uplus \mathcal{F}_{\text{Immovable}}

原文主要提出了三个问题。

1. 不可移动性不是类型的先天属性#

自引用 Future 刚被创建时通常仍是普通值,可以被移动到堆上、放进容器或传给执行器;只有在被 poll()、地址被见证(witness)之后,它才进入不可移动状态。

也就是说,一个对象常经历如下状态转移:

T.own(b)→Witness / Address WitnessedT.pin(p)T.\text{own}(b) \xrightarrow{\text{Witness / Address Witnessed}} T.\text{pin}(p)

其中:

状态作用对象含义
T.own(b)值 / 字节序列尚未绑定物理地址,可 relocation
T.shr(a, p)生命周期内共享指针在生命周期 a 内保持共享不变性
T.pin(p)物理指针 / Place指针 p 指向的位置托管一个已固定的实例

关键点:own(b) 作用于值,pin(p) 作用于物理位置。不可移动性更像特定 Place 上的 typestate,而不是 T 本身的永久分类。

2. 泛型污染可能比 ?Sized 更严重#

Rust 曾默认给泛型参数加 T: Sized,后来为了动态大小类型引入 ?Sized。如果 Move 也成为默认约束,生态中大量 API 可能被迫写成:

pub struct Vec<T: ?Move> { ... }
pub trait Iterator {
type Item: ?Move;
fn next(&mut self) -> Option<Self::Item>;
}
pub trait Future {
type Output: ?Move;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}

但 Move 比 Sized 更底层:函数调用、返回值、闭包捕获、容器存储、栈帧搬移都涉及 move 语义。!Move 作为值如何传参、压栈、弹栈、返回,会迫使语言引入复杂的隐式引用/指针传递机制。

3. !Move 需要配套的原地初始化机制#

在现行 Pin 体系下,异步任务可以先作为普通值构造、移动、装箱,最后在执行器里固定:

async fn my_task() { ... }
fn spawn_task() {
let fut = my_task(); // 普通值
let boxed_fut = Box::new(fut); // 可移动到堆上
let mut pinned_fut = Box::into_pin(boxed_fut); // 固定后 poll
pinned_fut.as_mut().poll(...);
}

若 my_task() 直接返回 !Move 类型,那么 Box::new(fut)、函数返回、容器存放都可能被禁止。为了解决构造问题,语言必须支持更复杂的原地初始化(in-place initialization)与 panic 时部分初始化字段的 drop glue,这会显著增加编译器与用户心智负担。

替代方向:Typestate on Places#

原文更认可 Pinned places 思路:把不可移动性作为借用检查器追踪的 Place 状态。

在 MIR 层面,一个 Place 是局部变量加投影(如 x.y、*x)。可以为每个 Place 维护:

State(x)∈{Unpinned,Pinned}\text{State}(x) \in \{\text{Unpinned}, \text{Pinned}\}

规则大致为:

规则含义
let mut x: T初始为 Unpinned
&pin mut x 或 let pin mut x触发 witness,状态单向转为 Pinned
State(x) == Pinned禁止移动 x,禁止普通 &mut x
&pin mut x允许重复获取固定借用
Drop(x)必须 in-place 运行析构

前端语法可以想象为:

let mut my_task = async_task();
let my_task = pass_around(my_task);
let pin mut my_task = my_task;
let error = my_task; // 编译期报错:Pinned place 不能再移动

同时引入原生 &pin mut T:

fn poll_future<T>(fut: &pin mut T) { ... }

Field Projection 的意义#

如果 &pin mut T 成为原生引用,编译器可以内建 field projection:

struct MyStruct {
unpin_field: i32,
pinned_field: MySelfReferential,
}
fn process(s: &pin mut MyStruct) {
let f1: &mut i32 = &mut s.unpin_field;
let f2: &pin mut MySelfReferential = &pin mut s.pinned_field;
}

解释:

  • i32: Unpin,即使外部结构体被固定,这个字段仍可安全降级为普通 &mut i32;
  • MySelfReferential: !Unpin,字段地址与外部结构体绑定,必须投影为 &pin mut MySelfReferential;
  • 这样可以减少 pin-project 宏和手写 unsafe 投影。

设计取舍#

原文主张由编译器追踪 Place 的固定状态,以描述自引用对象从可移动到不可移动的过程。相比新增 Move 类型分类,这可能减少泛型和调用约定的改动;具体取舍仍取决于提案设计。

与相关概念的关系#

概念与本文关系
Pin<P>当前库层机制,用包装指针限制可变访问
Unpin标记类型可从 pinned 状态平凡退回 movable/owned 语义
Move traitRoadmap 讨论方向,可能把 moveability 类型化
Typestate本文主张:pinning 更像 Place 的状态转移
Pinned places更接近本文推荐方向:由借用检查器追踪 pinned Place
&pin mut T可能替代 Pin<&mut T> 的原生固定可变引用

后续问题#

  • RFC issue #3709 的后续讨论是否仍沿着 &pin mut T / pinned places 方向推进。
  • Rust roadmap 中 Move trait 的真实设计是否确实会引入 ?Move 风格泛型污染,还是有其他缓解方案。
  • Ralf Jung 的 pinning formal model 与 RustBelt/Iris 体系如何精确定义 T.own、T.shr、T.pin。
Rust Pin/Unpin 与 Move trait 争议
https://blog.lpkt.cn/posts/concepts/rust-pinning-move-trait/
作者
lollipopkit
发布于
2026-07-07
许可协议
CC BY-NC-SA 4.0