El problema central que da origen a Pin<P> es la autorreferencialidad en las estructuras de datos. En Rust, la suposición fundamental es que cualquier valor puede ser movido en la memoria (mediante una copia de sus bytes, como ocurre en una asignación o al pasar un argumento por valor). Sin embargo, cuando escribimos código asíncrono con async/await, el compilador transforma nuestras funciones en máquinas de estado representadas como estructuras (structs).
Si una tarea asíncrona guarda una referencia local a una variable dentro de sí misma (por ejemplo, un &str que apunta a una String definida en la misma función), esa estructura resultante es “autorreferencial”. Si esta estructura se mueve de su ubicación original en el stack hacia el heap, o de una posición en el stack a otra, el puntero interno seguirá apuntando a la dirección de memoria vieja, que ahora es inválida. Esto resulta en un puntero colgante (dangling pointer) y comportamiento indefinido.
Pin<P> es un “wrapper” que envuelve un puntero (como &mut T) y garantiza que el valor al que apunta no podrá ser movido de su dirección de memoria mientras ese Pin exista. Es, esencialmente, una promesa de inmovilidad. Para que esto sea útil, Rust distingue entre tipos que son seguros de mover (que implementan el trait Unpin) y aquellos que no lo son. La mayoría de los tipos (como i32 o String) son Unpin, por lo que Pin<&mut T> se comporta como un simple &mut T. Sin embargo, los futuros generados por async fn son !Unpin (no implementan Unpin) precisamente porque pueden contener estas referencias internas peligrosas. Por esta razón, el método poll de la trait Future recibe Pin<&mut Self> en lugar de un simple &mut Self: esto obliga al implementador a garantizar que el futuro no se mueva una vez que se ha comenzado a procesar.
use std::pin::Pin;
use std::future::Future;
use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker};
// Un tipo normal que implementa Unpin automáticamente.
// Es seguro moverlo de un lugar a otro.
struct Simple(u32);
// Un tipo que simula una estructura autorreferencial (!Unpin).
// Si movemos este objeto, 'ptr' dejará de apuntar a 'data'.
struct SelfReferential {
data: u32,
ptr: *const u32,
}
impl SelfReferential {
fn new(val: u32) -> Self {
let mut s = Self { data: val, ptr: std::ptr::null() };
s.ptr = &s.data; // La referencia apunta a un campo interno
s
}
}
// Implementamos Future para entender la necesidad de Pin en el método poll.
impl Future for SelfReferential {
type Output = u32;
fn poll(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Self::Output> {
// Para acceder a los campos de un tipo !Unpin, necesitamos "Pin Projection".
// Usamos unsafe para obtener un &mut Self sin mover el objeto.
let this = unsafe { self.get_unchecked_mut() };
// Verificamos si la autorreferencia sigue siendo válida.
if this.ptr == &this.data {
Poll::Ready(this.data)
} else {
panic!("Error de memoria: El objeto se movió y la referencia es inválida!");
}
}
}
fn main() {
// 1. Tipos Unpin: Se pueden pinear directamente con Pin::new.
let mut s = Simple(10);
let mut p_s: Pin<&mut Simple> = Pin::new(&mut s);
println!("Simple: {}", p_s.0);
// 2. Tipos !Unpin: Pin::new(sr) fallaría en compilación.
let sr = SelfReferential::new(42);
// 3. Pinning en el Heap con Box::pin.
// Al mover el valor al heap, su dirección de memoria es estable.
// Box<T> es Unpin, pero el contenido (SelfReferential) permanece fijo en el heap.
let mut pinned_heap = Box::pin(SelfReferential::new(100));
// 4. Pinning en el Stack con la macro std::pin::pin!
// Esta macro asegura que el futuro se mantenga en una posición fija del stack.
let mut pinned_stack = std::pin::pin!(async {
let x = 5;
let y = &x; // Referencia local (autorreferencial en el bloque async)
println!("Stack async - x: {}, y: {}", x, y);
*y
});
// Mock de Waker para poder ejecutar el poll manualmente en este ejemplo.
let waker = unsafe { Waker::from_raw(create_raw_waker()) };
let mut cx = Context::from_waker(&waker);
// Ejecución de los futuros.
let _ = pinned_stack.as_mut().poll(&mut cx);
let _ = pinned_heap.as_mut().poll(&mut cx);
}
// Funciones auxiliares para crear un Waker vacío y permitir la compilación.
fn create_raw_waker() -> RawWaker {
fn no_op(_: *const ()) {}
fn clone(_: *const ()) -> RawWaker { create_raw_waker() }
let vtable = &RawWakerVTable::new(clone, no_op, no_op, no_op);
RawWaker::new(std::ptr::null(), vtable)
}
Explicación del Código
struct Simple(u32): Es un tipo que implementa automáticamente el traitUnpin, lo que significa que es seguro moverlo incluso si está envuelto en unPin.struct SelfReferential: Representa un tipo!Unpin. Contiene el campodatay el campoptr, que es un puntero que apunta adata. Si estestructse mueve de dirección,ptrquedará apuntando a basura.impl Future for SelfReferential: Aquí vemos por quépollrequierePin<&mut Self>. Sipollaceptara&mut Self, un usuario podría usarstd::mem::swappara mover el objeto entre llamadas apoll, corrompiendo el punteroptr.self.get_unchecked_mut(): Dentro depoll, usamos este métodounsafepara realizar “Pin Projection”. Esto nos permite obtener un&mut SelfReferentialpara leer sus campos sin violar la garantía de inmovilidad.Box::pin(SelfReferential::new(100)): Esta es la forma de “pinear” en el heap.Box::pinmueve el objeto al heap y nos devuelve unPin<Box<T>>. Aunque elBoxen el stack se mueva, el contenido en el heap permanece en la misma dirección de memoria.std::pin::pin!(async { ... }): Esta macro de la librería estándar realiza un pinning en el stack. Es extremadamente eficiente ya que no requiere una asignación en el heap, permitiendo que las variables locales de un bloqueasyncsean seguras.pinned_stack.as_mut().poll(&mut cx): Dado quepinned_stackes unPin<&mut T>, debemos usaras_mut()para obtener la referencia necesaria para invocar el métodopollde la traitFuture.
N° 86