wandres.dev
PIN Y AUTO-REFERENCIAS · estructuras que se apuntan a sí mismas

Pin en la práctica: `Future::poll`, `pin!` y `Box::pin`

`Future::poll` recibe `Pin<&mut Self>` porque un futuro puede ser auto-referencial y se sondea muchas veces: la firma exige que no se haya movido desde el sondeo anterior, para que sus punteros internos sigan válidos. Anclar un futuro se hace con `Box::pin` (en el heap) o con la macro `pin!` (en el stack). Casi nunca lo tocas a mano porque `async`/`await` lo hace por ti, pero entenderlo es la puerta al async del nivel 33.

⏱ 20 min

Toda la teoría del nivel converge en una sola firma, la del método más importante del async de Rust. Future::poll no recibe un &mut self corriente, sino un Pin<&mut Self>, y ahora tienes todo lo necesario para entender por qué esa P extra no es un capricho sino una necesidad estructural. Un futuro puede ser auto-referencial; un ejecutor lo sondea muchas veces con suspensiones en medio; y entre dos sondeos, si el futuro se moviera, sus punteros internos quedarían colgantes. Pin<&mut Self> es la firma diciéndole al ejecutor: para sondearme, prométeme en el sistema de tipos que no me has movido desde la última vez. Esta lección cierra el círculo: la firma, las dos formas de anclar un futuro —Box::pin y pin!— y por qué, pese a todo, apenas escribirás Pin con tus propias manos.

🎯 Al terminar esta lección sabrás
  • Leer la firma de Future::poll y explicar por qué self es Pin<&mut Self>.
  • Anclar un futuro en el heap con Box::pin y en el stack con la macro pin!.
  • Entender por qué async/await y el ejecutor hacen el pinchado por ti.
  • Reconocer los errores de Unpin/Pin en código async y saber qué te están diciendo.

La firma que lo explica todo

Aquí está el trait Future, reducido a su esencia:

use std::pin::Pin;
use std::task::{Context, Poll};

pub trait Future {
    type Output;
    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}

Lo insólito es el self: Pin<&mut Self>: un self type explícito. En vez de tomar &mut self, poll toma un self que llega pinchado. Reconstruye el porqué con lo que ya sabes. Un async fn genera un futuro que puede ser auto-referencial (lección 2), es decir !Unpin (lección 4). Ese futuro no se ejecuta de una vez: el ejecutor llama a poll, el futuro avanza hasta el siguiente await, devuelve Poll::Pending y se suspende conservando su estado —incluidos sus punteros internos—; más tarde el ejecutor vuelve a llamar a poll, y así hasta Poll::Ready.

El peligro está en el entre. Si el ejecutor pudiera mover el futuro entre dos poll —meterlo en un Vec que realoja, pasarlo por valor—, invalidaría sus punteros internos y el siguiente poll leería basura. Al exigir Pin<&mut Self>, la firma hace imposible que el ejecutor tenga un &mut Futuro con el que moverlo: recibe un puntero pinchado, del que —si el futuro es !Unpin— no puede extraer un &mut para reubicarlo. La regla queda grabada en el tipo: una vez que empiezas a sondear un futuro, ya no puede moverse jamás. Pin es el mecanismo que traslada esa regla, sin excepción posible, a través de cada ejecutor que exista.

Anclar un futuro: Box::pin y pin!

Para poder llamar a poll necesitas primero un futuro pinchado. Hay dos herramientas, según dónde quieras que viva:

use std::pin::pin;
use std::future::Future;

async fn trabajo() -> u32 { 42 }

fn main() {
    // 1) En el stack, sin asignar heap: pin! ancla un local oculto.
    let en_stack = pin!(trabajo());
    // en_stack: Pin<&mut impl Future<Output = u32>>

    // 2) En el heap, como objeto de trait: Box::pin asigna y ancla.
    let en_heap: Pin<Box<dyn Future<Output = u32>>> = Box::pin(trabajo());

    let _ = (en_stack, en_heap);
}

Box::pin(fut) mueve el futuro al heap y te devuelve un Pin<Box<F>>. Como el valor vive en una dirección estable del heap, mover el Pin<Box> mueve solo el puntero, no el futuro; este se queda quieto. Cuesta una asignación, pero es la vía más flexible: es como obtienes un Pin<Box<dyn Future>> para guardar futuros heterogéneos en un Vec o devolverlos de una función.

La macro std::pin::pin!(fut) —estable desde Rust 1.68— ancla el futuro en el stack, sin asignar nada. Crea un local escondido y te entrega un Pin<&mut F> a él; el borrow checker garantiza que ese local no se pueda mover mientras el pin viva. Es más barato que Box::pin —cero asignaciones— a cambio de menos flexibilidad: el futuro vive atado al scope actual.

📦

Box::pin

Ancla en el heap y produce Pin<Box<F>>. Cuesta una asignación, pero es lo más flexible: apto para dyn Future, para guardar el futuro en una estructura o devolverlo de una función.

📌

pin!

Ancla en el stack y produce Pin<&mut F>. Cero asignaciones, a cambio de quedar atado al scope actual: ideal para conducir un único futuro aquí y ahora.

💡
Cuál usar

Regla práctica: usa pin! cuando conduces un único futuro dentro de un scope y quieres evitar la asignación; usa Box::pin cuando necesitas un Pin<Box<dyn Future>> —almacenar el futuro, moverlo entre estructuras, devolverlo, o meter futuros de tipos distintos en la misma colección—. Ambos encapsulan por dentro el unsafe de prometer inmovilidad, así que tú nunca escribes new_unchecked a mano.

Por qué casi nunca lo tocas, pero debes entenderlo

Y ahora la paradoja que resuelve el nivel: después de todo este aparato, en tu código async cotidiano no escribirás Pin casi nunca. Cuando escribes algo.await, el compilador y el ejecutor orquestan todo el pinchado por debajo: el async fn genera el futuro, el runtime (Tokio y compañía, nivel 33) lo ancla antes de sondearlo, y la maquinaria de await mantiene la invariante sin que tú la menciones. Pin es como la fontanería tras la pared: sostiene el edificio, y precisamente por funcionar bien no la ves.

Solo asoma cuando bajas por debajo del .await: al conducir un futuro a mano llamando a poll, al almacenar un Pin<Box<dyn Future>>, al implementar un combinador como un select o un Stream, o al integrar código async de bajo nivel. Así se ve cuando lo tocas —el esqueleto de un ejecutor que sondea un futuro hasta que termina—, y fíjate en que pin! y as_mut son justo lo que hace falta para poder llamar a poll:

use std::future::Future;
use std::pin::pin;
use std::task::{Context, Poll};

fn bloquear_hasta_listo<F: Future>(fut: F) -> F::Output {
    let mut fut = pin!(fut);                    // anclamos en el stack: Pin<&mut F>
    let waker = crear_waker();                  // un Waker que reprograma el poll
    let mut cx = Context::from_waker(&waker);
    loop {
        match fut.as_mut().poll(&mut cx) {      // poll exige Pin<&mut F>: as_mut lo entrega
            Poll::Ready(valor) => return valor, // el futuro produjo su Output
            Poll::Pending => aparcar(&waker),   // suspendido: esperar la notificacion
        }
    }
}

Sin pin! no tendrías un Pin<&mut F> con el que satisfacer la firma de poll; sin as_mut no podrías volver a sondear en la siguiente vuelta del bucle sin consumir el pin. Todo un runtime como Tokio es, en esencia, una versión industrial de este bucle. Y sobre todo, Pin asoma en los mensajes de error. El día que veas algo como the trait Unpin is not implemented for … o X cannot be unpinned en un contexto async, no será un jeroglífico: sabrás que el compilador te está diciendo que intentas mover —o pedir un &mut de— un futuro auto-referencial que ha de quedarse quieto, y que la salida suele ser Box::pin o pin!. Entender Pin no es para escribirlo a diario; es para que el async del nivel 33 sea un mecanismo comprensible en vez de magia que a veces falla con errores incomprensibles.

flowchart TD
A[async fn devuelve un futuro] --> B{Donde lo anclas}
B -->|pin en el stack| C[Pin de ref mut al futuro sin heap]
B -->|Box pin en el heap| D[Pin de Box del futuro con una asignacion]
C --> E[El ejecutor llama a poll con Pin de ref mut a Self]
D --> E
E --> F[Entre sondeos el futuro no puede moverse]
F --> G[Sus punteros internos siguen validos]
G --> H[async y await orquestan esto por ti en el nivel 33]
style A fill:#89b4fa,color:#11111b
style E fill:#cba6f7,color:#11111b
style F fill:#f9e2af,color:#11111b
style H fill:#a6e3a1,color:#11111b
Pin es la clave de bóveda que hace posible el async de coste cero

Da un paso atrás y contempla la cadena entera, porque Pin solo se entiende como el último eslabón de una construcción larga y de una coherencia notable. Rust quería async de coste cero: sin recolector, sin asignación forzosa por tarea, con futuros que fueran simples máquinas de estados sobre el stack. Para eso, un async fn debe compilarse a un struct que guarde sus locales, y en cuanto un local prestado sobrevive a un await, ese struct tiene que contener un puntero a sí mismo —no hay alternativa, es la esencia de suspender y reanudar—. Pero un valor auto-referencial se rompe si se mueve, y el move de Rust es un memcpy ciego que puede ocurrir en cualquier asignación. Cada pieza del nivel es un eslabón para resolver esa tensión sin traicionar el coste cero: el problema (mover invalida punteros internos), su origen práctico (async genera auto-referencia), el contrato (Pin retira el &mut con el que moverías), y el permiso (Unpin hace que el contrato no grave a nadie más). Future::poll con Pin<&mut Self> es donde toda esa teoría se vuelve una firma que millones de líneas de código respetan sin saberlo, porque el ejecutor está obligado por el tipo a no mover un futuro sondeado. Y la belleza final es la invisibilidad: como async/await teje el pinchado por ti, cobras el dividendo —futuros seguros, sin GC, sin data races sobre su estado interno— sin pagar el precio de gestionarlo. Pin es la clave de bóveda: la pieza que casi no se ve, encajada en lo alto del arco, sin la cual toda la estructura del async se derrumbaría. No la escribirás casi nunca. Pero el día que un error de Unpin te frene, o que bajes a implementar un runtime, la diferencia entre pelear a ciegas y arreglarlo en un minuto será exactamente esto que ahora entiendes: que un futuro es un valor que se apunta a sí mismo, y que Pin es la promesa, verificada por el compilador, de que nadie lo moverá mientras se apunte.

📝
Lo esencial de Pin en la práctica

Future::poll recibe Pin<&mut Self> porque un futuro puede ser auto-referencial y se sondea muchas veces; la firma prohíbe al ejecutor moverlo entre sondeos, manteniendo válidos sus punteros internos. Para anclarlo: Box::pin lo pone en el heap y da un Pin<Box<F>> —flexible, apto para dyn Future, cuesta una asignación—; pin! lo ancla en el stack y da un Pin<&mut F> —sin asignar, atado al scope—. Ambos encapsulan el unsafe por ti. En el día a día async/await y el runtime hacen el pinchado solos, así que rara vez escribes Pin; pero entenderlo descifra los errores de Unpin y abre la puerta al async del nivel 33.

⚔️ Ancla y conduce un futuro
  1. Escribe la firma de Future::poll de memoria y explica, en dos frases, por qué self llega como Pin<&mut Self> y no como &mut self.
  2. Ancla trabajo() de dos maneras, con pin! y con Box::pin, y anota el tipo exacto que produce cada una.
  3. Explica con un ejemplo qué le pasaría a un futuro auto-referencial si el ejecutor lo moviera entre dos llamadas a poll.
  4. Argumenta cuándo elegirías Box::pin sobre pin! a partir de la necesidad de un Pin<Box<dyn Future>>.
  5. Provoca o imagina un error the trait Unpin is not implemented en código async y traduce, con lo aprendido en el nivel, qué te está diciendo y cómo se arregla.