Saltar al contenido

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.

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.

  • 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:

Rust
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:

Rust
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 sí 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:

Rust
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!.

Proyección estructural: cuándo creas un combinador y pin-project

Hay un escenario donde no puedes escapar de Pin: cuando escribes un combinador propio o un struct que envuelve otro futuro (por ejemplo, un Timeout<F>, un Retry<F> o un buffer personalizado).

Imagina que tienes un struct con dos campos:

Rust
struct Timeout<F> {    futuro: F,       // ¡debe permanecer pinchado para llamar a su poll!    expirado: bool,  // dato corriente, no necesita estar pinchado}

Si tu método poll recibe self: Pin<&mut Timeout<F>>, te encuentras con un problema de tipos: no puedes acceder directamente a self.futuro como Pin<&mut F>. Necesitas proyectar el pin desde el contenedor hacia sus campos. Como aprendiste en la lección anterior, la operación cruda del compilador es map_unchecked_mut, pero usarla a mano exige garantizar bajo palabra tres invariantes críticas:

  1. El contenedor solo puede ser Unpin si todos sus campos pinchados son Unpin.
  2. El destructor (Drop) no puede mover ningún campo pinchado antes de liberarlo.
  3. No debe existir ningún método seguro que entregue un &mut normal del campo pinchado.

Romper cualquiera de estas reglas es comportamiento indefinido inmediato. Por eso, en el código de producción real nunca se escribe esa proyección con unsafe manual. El ecosistema entero estandarizó una solución: el crate pin-project (y su variante ligera sin dependencias de proc-macros, pin-project-lite).

Rust
use pin_project::pin_project;use std::future::Future;use std::pin::Pin;use std::task::{Context, Poll};#[pin_project]pub struct Timeout<F> {    #[pin]    futuro: F,        // este campo hereda el estado de inmovilidad    expirado: bool,   // este campo se proyecta como &mut bool normal}impl<F: Future> Future for Timeout<F> {    type Output = Option<F::Output>;    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {        // .project() consume el Pin y devuelve una vista proyectada        let this = self.project();        if *this.expirado {            return Poll::Ready(None);        }        // `this.futuro` es ahora un `Pin<&mut F>` seguro:        match this.futuro.poll(cx) {            Poll::Ready(v) => Poll::Ready(Some(v)),            Poll::Pending => Poll::Pending,        }    }}

La macro #[pin_project] genera por ti un tipo auxiliar interno (TimeoutProjection) donde cada campo marcado con #[pin] se expone como Pin<&mut F> y los demás como &mut T planos. Además, la macro implementa automáticamente el trait Unpin condicional correcto y prohíbe que escribas un impl Drop ingenuo que viole la inmovilidad. Es la proyección estructural formalizada como patrón de ingeniería seguro.

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.