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

`Pin<P>`: el contrato de inmovilidad, garantizado por el sistema de tipos

`Pin<P>` no es un puntero nuevo: es un envoltorio sobre un puntero `P` que retira la única operación capaz de mover el valor apuntado, el `&mut T`. Al no exponer jamás un `&mut T`, ninguna función como `swap`, `replace` o `mem::take` puede reubicar el valor, y su dirección queda estable hasta el `drop`. Una garantía de tiempo de ejecución expresada, a coste cero, como la ausencia de una capacidad.

⏱ 20 min

Ya tienes el problema entero delante: hay valores —los futuros auto-referenciales— que se corrompen si se mueven después de empezar a ejecutarse, y el move de Rust es un memcpy ciego que puede ocurrir en cualquier asignación. Hace falta una forma de prometerle al compilador que cierto valor permanecerá en su dirección hasta que muera, y que esa promesa se verifique sin coste en tiempo de ejecución. Esa forma es Pin<P>. Lo fascinante es cómo lo consigue: no vigila el valor, no pone un candado en tiempo de ejecución, no hace ninguna llamada al sistema. Simplemente te niega el acceso a la única herramienta con la que podrías mover el valor. La inmovilidad no se impone: se obtiene retirando la capacidad de moverse. Es seguridad por ausencia, y es una de las ideas más afiladas del sistema de tipos de Rust.

🎯 Al terminar esta lección sabrás
  • Entender que Pin<P> es un envoltorio sobre un puntero, no un puntero nuevo ni datos nuevos.
  • Ver que la clave es no exponer nunca un &mut T seguro al valor pinchado.
  • Conectar la ausencia de &mut T con la imposibilidad de llamar a swap, replace o take.
  • Reconocer el papel de unsafe al construir un Pin y la cláusula de garantía hasta el drop.

Pin es un envoltorio que quita, no que añade

Empecemos por deshacer el malentendido más común. Pin<P> no es un puntero especial que clave el valor en la RAM, ni un tipo de dato que ocupe memoria extra. Su definición es casi decepcionante:

pub struct Pin<P> {
    pointer: P,   // eso es todo: envuelve un puntero P
}

Pin<Box<T>> tiene exactamente los mismos bits que un Box<T>; Pin<&mut T> los mismos que un &mut T. No hay coste en memoria ni en tiempo de ejecución: Pin es puramente un artefacto de tipos que desaparece al compilar. Lo que cambia no es la representación, sino la interfaz: qué te deja hacer Pin<P> con el puntero que envuelve. Y su diseño entero se resume en una frase: Pin<P> no te da un &mut T al valor apuntado.

ℹ️
Por qué Pin envuelve un puntero y no el valor

Fíjate en que siempre es Pin<Box<T>> o Pin<&mut T>, nunca Pin<T> a secas. La razón es honda: pinchar trata de una dirección, y solo un puntero tiene una dirección estable independiente de dónde vivas tú. Si Pin envolviera el valor por copia, mover el Pin<T> movería el propio T —justo lo que se quiere impedir—; sería inútil. Al envolver un puntero, mover el Pin<P> reubica solo el puntero, mientras el valor apuntado se queda quieto en su dirección del heap o del stack. Pin no ancla un valor: ancla lo que un puntero apunta.

¿Por qué obsesionarse con el &mut T? Porque el &mut T es la llave universal para mover un valor. Piensa en todas las funciones que reubican un valor: cada una necesita acceso mutable exclusivo.

std::mem::swap(&mut a, &mut b);       // exige &mut a los dos
std::mem::replace(&mut sitio, nuevo); // exige &mut al sitio
std::mem::take(&mut sitio);           // exige &mut al sitio
*sitio = otro_valor;                  // exige &mut al sitio

Todas piden un &mut T. Ninguna operación que mueva un valor puede prescindir de él. Por tanto, si un tipo consigue no entregar jamás un &mut T, cierra de un portazo todas las puertas por las que el valor podría escapar de su dirección. Eso es precisamente lo que hace Pin.

La garantía nace de lo que Pin no te deja hacer

Lo que Pin<&mut T> sí te ofrece es un acceso deliberadamente mutilado:

// A partir de un pin: `p: Pin<&mut T>`
let compartida: &T = p.as_ref().get_ref();  // referencia compartida: SÍ, no puede mover
// let exclusiva: &mut T = p.get_mut();      // &mut T seguro: solo si T es Unpin (lección 4)
let re: Pin<&mut T> = p.as_mut();            // otro pin, sigue restringido

Puedes obtener un &T —una referencia compartida, con la que puedes leer pero jamás mover, porque swap y compañía exigen &mut—. Puedes obtener otro Pin<&mut T> para encadenar. Lo que no puedes, salvo permiso especial que estudiarás en la lección siguiente, es obtener un &mut T desnudo. La garantía de inmovilidad es esa negación: es un espacio vacío en la API, no un mecanismo activo. El valor no se puede mover porque, sencillamente, nadie tiene en la mano la herramienta con la que se mueve.

💡
Cómo se construye un Pin: la promesa vive en el unsafe

Envolver un valor en un Pin sobre un puntero que permite mover requiere unsafe, porque prometes que a partir de ahí no lo moverás. Los constructores lo reflejan: Pin::new_unchecked(ptr) es unsafe —cargas con la invariante—, mientras que Pin::new(ptr) es seguro pero solo existe cuando mover es inocuo (tipos Unpin, lección 4). En la práctica casi nunca escribes ese unsafe: lo hacen por ti Box::pin y la macro pin! (lección 5), que combinan la asignación o el anclaje en el stack con la promesa de inmovilidad de forma segura.

Un mapa de la superficie de Pin

Toda la disciplina de Pin está codificada en qué métodos existen y con qué requisitos. Merece la pena verlos juntos, porque el patrón salta a la vista: la lectura siempre está permitida, la mutación seguras solo bajo Unpin, y lo demás vive tras unsafe.

impl<P> Pin<P> {
    // Construir un pin:
    //   Pin::new(ptr)            -> seguro, pero SOLO si el destino es Unpin
    //   Pin::new_unchecked(ptr)  -> unsafe: prometes tu no mover el destino
}

impl<'a, T> Pin<&'a mut T> {
    // Permitido siempre, sea o no Unpin:
    //   as_ref(&self)   -> Pin<&T>      reduce a una compartida pinchada
    //   as_mut(&mut)    -> Pin<&mut T>  reborrow pinchado, encadenable
    //   get_ref(self)   -> &T           lectura pura, jamas mueve
    // Permitido solo si T: Unpin:
    //   get_mut(self)   -> &mut T       recupera el mut, seguro porque mover no daña
    // Escotillas unsafe (proyectar a un campo, etc.):
    //   get_unchecked_mut(self)       -> &mut T
    //   map_unchecked_mut(self, f)    -> Pin<&mut Campo>
}

Lo que falta en la parte segura es justo lo revelador: no hay ningún camino seguro de Pin<&mut T> a &mut T cuando T no es Unpin. Ese hueco no es un olvido; es la garantía misma. Y map_unchecked_mut —la proyección— es unsafe precisamente porque prometer que un campo hereda la inmovilidad del todo es una afirmación que solo tú puedes respaldar.

👁️

Lo que Pin te concede

Leer el valor con get_ref y as_ref, y encadenar con as_mut. Todo lo que no reubica el valor está disponible sin fricción, aunque el tipo sea auto-referencial.

🔒

Lo que Pin te niega

Un &mut T seguro cuando T no es Unpin, y con él swap, replace, take y la asignación por deref. Sin esa llave, el valor no puede abandonar su dirección.

Hasta el drop, y ni un instante menos

Hay una cláusula fina pero crucial en el contrato de Pin: el valor permanece inmóvil en su dirección desde que se pincha hasta que se ejecuta su drop, incluido el propio drop. No es hasta que dejes de usarlo, sino hasta que se destruye. Esto importa porque un tipo auto-referencial normalmente tiene un Drop que quizá recorra sus punteros internos al liberar recursos; si el valor pudiera moverse justo antes de destruirse, esos punteros estarían colgantes dentro del propio destructor.

De ahí sale una regla que a veces sorprende: si un tipo es auto-referencial (!Unpin) y pinchado, su memoria no puede reutilizarse sin antes correr su drop. No puedes, por ejemplo, sobrescribir sus bytes ni desasignar el Box que lo contiene sin pasar por el destructor. El compromiso de inmovilidad cubre toda la vida del valor, porque un puntero interno colgante es igual de peligroso en el último microsegundo que en el primero.

flowchart TD
P[Pin de un puntero a T] --> Q[Puede dar una referencia compartida a T]
P --> R[Puede dar otro Pin de puntero a T]
P --> S[NO da un ref mut a T de forma segura]
Q --> T[Con solo lectura no se puede mover]
S --> U[Sin ref mut no hay swap ni replace ni take]
T --> V[La direccion de T permanece estable hasta el drop]
U --> V
style P fill:#89b4fa,color:#11111b
style S fill:#f38ba8,color:#11111b
style U fill:#f9e2af,color:#11111b
style V fill:#a6e3a1,color:#11111b
Una garantía de ejecución codificada como la ausencia de una capacidad

Interioriza el patrón, porque es de los más profundos del diseño de Rust y reaparece por todo el lenguaje. Pin tiene que garantizar una propiedad que suena inevitablemente dinámica: esta dirección de memoria no cambiará mientras el valor viva. Un ingeniero de otra tradición esperaría, para eso, un mecanismo en tiempo de ejecución: un flag booleano pinchado, una comprobación en cada operación, un registro de valores anclados, quizá una llamada al asignador para fijar la página. Pin no hace nada de eso. Su coste en tiempo de ejecución es exactamente cero, porque Pin<P> son los mismos bits que P y no ejecuta ni una instrucción extra. Toda la garantía vive en el sistema de tipos, y —esto es lo genial— se expresa no como algo que Pin hace, sino como algo que Pin no te deja hacer: obtener un &mut T. La inmovilidad es el hueco con forma de &mut T que falta en la API. Es seguridad por sustracción. Y no es un truco aislado: es el mismo principio que gobierna la referencia compartida &T, que garantiza nadie mutará esto con solo carecer de la capacidad de mutar; o el que hace que un valor movido sea inusable, con solo retirarle el nombre. Rust construye sus garantías más fuertes quitando operaciones, no añadiendo vigilancia, porque una capacidad que no existe no puede usarse mal, no tiene coste y no puede fallar en tiempo de ejecución. Cuando entiendas que Pin protege el valor negándote el &mut T —igual que un candado protege una puerta no con un guardia, sino por no tener manija por dentro—, habrás captado no solo Pin, sino la gramática con la que Rust escribe la seguridad: no impedir que hagas daño, sino no darte con qué hacerlo.

📝
Lo esencial de Pin

Pin<P> es un envoltorio sobre un puntero P, con sus mismos bits y coste cero; no es un puntero nuevo ni fija nada en tiempo de ejecución. Su diseño se reduce a no exponer nunca un &mut T seguro al valor apuntado. Como toda operación que mueve —swap, replace, take, la asignación por deref— exige &mut T, negarlo cierra todas las puertas por las que el valor podría reubicarse. Solo entrega &T y otros Pin<&mut T>. Construir un Pin sobre un puntero que permite mover requiere unsafe, porque prometes no moverlo; ese unsafe casi siempre lo encapsulan Box::pin y pin!. La garantía cubre desde el pinchado hasta el drop inclusive.

⚔️ Razona la garantía por sustracción
  1. Escribe la definición aproximada de Pin<P> y explica por qué tiene los mismos bits que P y coste cero.
  2. Lista tres funciones de std::mem que muevan un valor y subraya el argumento &mut T que todas comparten.
  3. Explica, en una frase, cómo la sola ausencia de &mut T hace imposible llamar a cualquiera de esas tres funciones sobre un valor pinchado.
  4. Justifica por qué construir un Pin con new_unchecked es unsafe mientras que Pin::new es seguro, y qué diferencia asumes en cada caso.
  5. Argumenta por qué la garantía debe extenderse hasta el drop inclusive, con el ejemplo de un tipo auto-referencial cuyo destructor recorre sus punteros internos.