Rust 学 Cpp Bug -- Future 自引用 与 Box::pin
Rust 中 Future 的语义
就是异步的函数对象
基本概念
在 Rust 里:
use std::future::Future;
async fn foo() -> i32 {
42
}
foo()返回的是一个 实现了Futuretrait 的匿名类型- Future 的语义:表示一个值可能在未来某个时间才准备好
- Future 本身是 惰性 的,不会立即执行,只有
.await或 executor 调用它时才运行
Future Trait
核心 trait 定义:
pub trait Future {
type Output;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}
关键点:
-
poll方法:每次调用推进 Future 的状态机 -
Pin<&mut Self>:必须固定在内存里,不能移动 -
Context:提供 Waker,让 Future 在准备好时被再次调度 -
返回值
Poll<Self::Output>:Poll::Pending→ 任务还没完成Poll::Ready(val)→ 任务完成,返回结果
内部语义:状态机
Rust 编译器把 async fn 转换成 状态机:
async fn foo() {
let x = async_op().await;
let y = other_op().await;
return x + y;
}
转换后(简化):
enum FooFuture {
Start,
WaitingX,
WaitingY,
Done(i32),
}
- 每次
.poll()调用 → 状态机推进到下一状态 - 局部变量(x, y)被存储在 Future 内部
- Future 内部 可能存在自引用(例如
x的引用被保存在 Future 内部状态)
这就是为什么 Future !Unpin:它的内存地址一旦移动,内部引用可能悬空 → UB。
自引用问题 - 内存

移动在内存上意味着什么
在 Rust 中:
let x = 42;
let y = x; // move
- 对于简单类型(i32、usize),move 只是拷贝(或者直接复用寄存器/栈空间),无副作用。
- 对于 复杂类型(struct 或 Future),move 会把内存块整块复制到新的地址。
汇编层面
假设有一个 struct:
struct MyStruct {
a: i32,
b: i32,
}
Rust 编译成 x86 汇编时,move s1 到 s2 可能就是:
mov eax, [rbp-8] ; 读取 s1.a
mov [rbp-16], eax ; 写入 s2.a
mov eax, [rbp-4] ; 读取 s1.b
mov [rbp-12], eax ; 写入 s2.b
这是安全的,因为内部数据只是值,没有指向自己内部的指针。
自引用结构的危险
假设我们有一个自引用 struct:
struct SelfRef {
data: String,
ptr: *const String,
}
impl SelfRef {
fn new(txt: &str) -> Self {
let s = String::from(txt);
Self { data: s.clone(), ptr: &s }
}
}
ptr指向data内部。- 如果 Rust 允许 移动整个 struct 到堆上或新地址:
old address: 0x1000
data -> 0x1008
ptr -> 0x1008
移动后:
new address: 0x2000
data -> 0x2008
ptr -> 0x1008 <-- 仍指向老地址!
ptr 指向了“旧地址”,已经悬空,产生 未定义行为。
Pin 如何解决这个问题
Pin<T> 的本质就是:
- 在堆上分配内存(Box)
- 禁止移动内部数据的地址
- 编译器通过类型系统阻止调用可能导致 move 的操作
汇编上:
Box::pin(my_self_ref):
heap_addr = malloc(sizeof(SelfRef))
copy data to heap_addr
return Pin<heap_addr>
之后 Rust 不允许再生成对 my_self_ref 的 move 汇编代码,否则编译报错。
- 实际上,move 就是汇编的
memcpy或寄存器重定向 - Pin 通过类型系统阻止
memcpy,确保指针ptr永远指向固定地址
Future 的例子(async/await)
Future 也是 自引用结构,状态机里存储了自己的局部变量的指针。
汇编上:
; 假设 Future 在栈上
mov rax, [rbp-32] ; 读取状态机内部引用
mov [rbp-64], rax ; 试图 move 状态机 -> 错误
- 如果允许这条汇编执行,内部指针就悬空了
Pin禁止生成这种 move 汇编
- 移动 = 内存复制/地址变更
- 普通类型:move 不会影响内部数据指针 → 安全
- 自引用类型或 !Unpin 类型:move 会破坏内部指针 → UB
- Pin:在堆上固定地址,并在类型层面禁止移动 → 汇编无法执行不安全的 move
所有权原来就支持对这种情况进行校验吗,还是后添加的检查
Rust 的所有权系统本身并没有直接解决自引用类型的移动问题,而是通过 Pin API 在语言层面“补充”了检查。
所有权系统的能力
Rust 的所有权/借用系统天生可以保证:
- 唯一性:每个可变借用(
&mut T)在任意时刻只有一个。 - 不可悬空:借用的生命周期内,引用总是有效。
- 防止悬垂指针:当值离开作用域,编译器不允许引用继续使用它。
但是,普通所有权系统无法知道“结构内部某个字段指向自己”:
struct SelfRef {
data: String,
ptr: *const String,
}
ptr是裸指针(*const T)- 所有权系统只知道
SelfRef的整体所有权 - 它不知道内部
ptr指向自己data
所以,如果你直接 move 这个 struct:
let s2 = s1; // move
- 编译器会允许,因为从所有权角度来看,这只是整体值的移动
- 但内部裸指针已经悬空 → 所有权系统无法直接阻止
Pin 是后添加的安全检查
Pin<T> 出现的背景:
- async/await 和 Future 需要 状态机自引用
- 早期 Rust 中,如果 Future 被移动,可能 UB
- Rust 团队添加了
PinAPI 来:
- 固定值在堆上(Box::pin)
- 通过类型系统禁止移动(编译器检查)
也就是说:
- Pin 是一种语言级别的额外约束,在所有权系统之上
- 它利用类型系统和标记 (
!Unpin) 来禁止潜在危险的移动操作
Cpp 也有,但是很难通过编译器少量标记去自动检查,需要自行处理
什么时候需要 Pin
async fn 的基本情况
在 Rust 中:
async fn foo() -> i32 {
42
}
-
foo()返回类型是impl Future<Output=i32> -
这个 Future 默认是 !Unpin 的类型(内部状态机可能自引用局部变量)
-
能否直接移动 Future:
- 如果你只是立即
.await,通常不需要 Box::pin - 因为 Future 还在栈上,Rust 编译器可以保证生命周期和借用安全
- 如果你只是立即
例如:
let result = foo().await; // ✅ 不需要 Pin
什么时候需要 Box::pin
你必须用 Box::pin 的场景:
-
Future 会被存储(长期保留)
- 例如放在 struct 里,或者交给 executor/调度器
- 移动 Future 会破坏自引用 → UB
-
需要类型擦除(trait 对象)
- 例如返回
Pin<Box<dyn Future<Output=()>>> async fn返回的是匿名类型impl Future,有些接口需要统一类型
- 例如返回
-
可能在多线程环境传递
- 堆分配 + Pin 可以保证地址固定、安全
典型例子:
let job = Job::new_async("0 0 * * * *", move |_, _| {
let db_client = db_client.clone();
Box::pin(async move {
db_client.delete_expired_files().await;
})
});
- JobScheduler 需要 可存储和重复 poll 的 Future
- 必须 Pin + Box
什么时候不用 Box::pin
- 你直接调用并
.await的 async fn:
async fn foo() -> i32 { 42 }
let val = foo().await; // ✅ 栈上 Future,直接 await 即可
- 临时创建的 Future,只在作用域内使用,不会移动,不需要 Pin
- 简单 async/await 使用几乎都不用手动 Box::pin
编译器主动被动
- unpin 可以移动,可以move
- !unpin 不可安全移动,可以move
- pin() 固定不可移动的,限制move
既然要主动锁住发挥效果,那编译器还有啥用,我一懒或忘了不就废了。有的。
日常开发:基本不会忘(编译器会逼你用 Pin)
底层/unsafe:可能会忘(需要你自己保证)
看一个最关键的设计:
fn poll(self: Pin<&mut Self>, ...)
像 Future 这样的核心 API:
- 必须要
Pin<&mut Self>才能调用 - 你如果没 Pin,连
.poll()都调不了
举个例子
let fut = async { 1 };
// ❌ 你拿不到 poll 所需的类型
// fut.poll(...) // 编译错误
接口或者库设计者会提前使用类型约束调用者。强制其 Pin。

浙公网安备 33010602011771号