Pin y Unpin en Rust: por qué existen y cómo manejar futures auto-referenciales

En Rust, mover un valor es normal: cambias de propietario y los bytes del valor se copian a la nueva ubicación en memoria. Para la mayoría de tipos esto es perfectamente seguro. Pero hay una situación especial donde mover un valor en memoria lo invalida: cuando el valor contiene un puntero que apunta a sí mismo.

Esto es exactamente lo que pasa con los futures async en Rust. Considera esta función:

async fn procesar() {
    let datos = vec![1, 2, 3];
    let referencia = &datos;
    alguna_operacion_async().await;
    println!("{:?}", referencia);  // usa la referencia después del await
}

El compilador desugara esta función async a una máquina de estados que implementa el trait Future. Esa máquina de estados contiene tanto datos como referencia (un puntero a datos). Si el compilador moviera esta estructura a otra ubicación en memoria, referencia apuntaría a una dirección incorrecta. El future sería auto-referencial.

Pin: garantizar que el valor no se mueve

Pin<P> es un tipo wrapper que envuelve un puntero P y garantiza que el valor al que apunta no será movido después de ser pinned. Una vez que un valor está pinned, solo puedes acceder a él a través del pin, y el pin no permite obtener una &mut T que permita mover el valor.

use std::pin::Pin;

// Crear un Pin en el heap (la forma más común)
let mi_future = Box::pin(alguna_funcion_async());

// Ahora el future está "pinned" en el heap y no se puede mover
// El runtime async puede ejecutarlo de forma segura

El trait Unpin

Unpin es un trait marcador (como Send y Sync) que significa "este tipo es seguro de mover incluso cuando está pinned". La mayoría de tipos implementan Unpin automáticamente, porque para ellos Pin no tiene efecto: pueden moverse libremente.

use std::pin::Pin;

// i32 implementa Unpin: Pin<&mut i32> se puede convertir a &mut i32 sin restricciones
let mut x = 42i32;
let pinned = Pin::new(&mut x);
let mutable: &mut i32 = Pin::into_inner(pinned);  // OK para Unpin

// Los futures que contienen referencias propias NO implementan Unpin
// Intentar hacer Pin::into_inner con ellos no compila

Cuando un tipo no implementa Unpin, está diciendo "si me pinneas, te comprometes a no moverme". El compilador lo fuerza.

Box::pin: pinning en el heap

La forma más sencilla de pin un future es con Box::pin(), que lo aloja en el heap y devuelve un Pin<Box<T>>:

use std::pin::Pin;

async fn future_largo() -> String {
    tokio::time::sleep(std::time::Duration::from_secs(1)).await;
    "Resultado".to_string()
}

// En el stack (requiere que el tipo implemente Unpin, o usar pin!)
// let f = future_largo();

// En el heap (funciona con cualquier future)
let f: Pin<Box<dyn Future<Output = String> + Send>> = Box::pin(future_largo());

// Ahora puedes guardarlo en una struct, pasarlo a funciones que esperan Pin<Box<...>>, etc.

Usar Box::pin implica una allocación en el heap, pero es la forma más portátil y la que usan la mayoría de APIs.

La macro pin! para el stack

Para evitar la allocación cuando el future vive dentro de una sola función, puedes usar la macro pin! que fija el valor en el stack:

use tokio::pin;

async fn ejemplo() {
    let mi_future = future_largo();
    pin!(mi_future);  // Ahora mi_future está pinned en el stack

    // Puedes usarlo con select! o ejecutarlo directamente
    let resultado = (&mut mi_future).await;
    println!("{}", resultado);
}

La macro pin! de Tokio (o std::pin::pin! desde Rust 1.68) reemplaza la binding original con una versión pinned usando un shadow binding. Es "magia" del macro system, pero es segura.

Pin en futures combinados con select!

El caso más habitual donde necesitas Pin explícito es con la macro select! de Tokio cuando el mismo future se usa en múltiples iteraciones del loop:

use tokio::pin;

async fn proceso() {
    let operacion_1 = tarea_larga_1();
    let operacion_2 = tarea_larga_2();

    pin!(operacion_1);
    pin!(operacion_2);

    loop {
        tokio::select! {
            resultado = &mut operacion_1 => {
                println!("Operación 1 lista: {:?}", resultado);
                break;
            }
            resultado = &mut operacion_2 => {
                println!("Operación 2 lista: {:?}", resultado);
                break;
            }
        }
    }
}

Sin el pin!, select! movería el future en cada iteración del loop, lo que invalidaría el estado interno del future.

Implementar Future manualmente con Pin

Cuando implementas el trait Future manualmente, el método poll recibe Pin<&mut Self>:

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

struct TiempoPasado {
    segundos: u64,
    iniciado_en: Option<std::time::Instant>,
}

impl Future for TiempoPasado {
    type Output = ();

    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
        // Para tipos que implementan Unpin, get_mut() es seguro
        let this = self.get_mut();

        let inicio = this.iniciado_en.get_or_insert_with(std::time::Instant::now);

        if inicio.elapsed().as_secs() >= this.segundos {
            Poll::Ready(())
        } else {
            // Decirle al runtime que nos despierte más tarde
            let waker = cx.waker().clone();
            let segundos = this.segundos;
            let inicio = *inicio;
            std::thread::spawn(move || {
                let restante = std::time::Duration::from_secs(segundos)
                    .checked_sub(inicio.elapsed())
                    .unwrap_or_default();
                std::thread::sleep(restante);
                waker.wake();
            });
            Poll::Pending
        }
    }
}

Tipos auto-referenciales y unsafe

Si necesitas crear una struct que se auto-referencia (un puntero a sí misma), necesitas unsafe y cuidado con Pin. La alternativa segura es usar crates como ouroboros o self_cell, que usan macros para gestionar los invariantes de seguridad:

[dependencies]
self_cell = "1"
use self_cell::self_cell;

self_cell!(
    struct DatosParseados {
        owner: String,
        #[covariant]
        dependent: ParsedResult,
    }
    impl { Debug }
);

// Permite tener un ParsedResult que apunta al String propietario
// sin unsafe ni gestión manual de Pin

Para la mayoría de código async en producción, no necesitas pensar explícitamente en Pin: los runtimes como Tokio y los futures generados por async fn lo gestionan de forma transparente. Solo aparece en la interfaz cuando escribes código de bajo nivel, implementas tus propios futures o trabajas con futures que deben guardarse entre iteraciones de select!.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP