Crear y dereferenciar: de la referencia al puntero y de vuelta
De una referencia a un puntero con &x as *const T, y el operador moderno &raw const que toma la dirección sin fabricar una referencia intermedia. Leer y escribir con * dentro de unsafe, las variantes read/write y read_unaligned, y cómo tocar memoria sin inicializar con MaybeUninit.
Un puntero crudo casi nunca nace de la nada: nace de un valor que ya posees. El camino habitual es partir de una referencia —&x as *const T— y dejar que decaiga a puntero. Pero ese camino esconde una sutileza que el nivel entero gira en torno a ella: escribir &x afirma que x es un lugar válido, alineado e inicializado, y hay situaciones —memoria sin inicializar, un campo desalineado de un struct empaquetado— donde esa afirmación es falsa y forjar la referencia intermedia es, en sí mismo, comportamiento indefinido. Por eso Rust 2024 promueve un operador que toma la dirección sin pasar por una referencia: &raw const y &raw mut. Dominar cuándo cada uno, y cómo leer y escribir a través del resultado, es aprender a hablar de un sitio en memoria sin prometer nada sobre lo que vive en él.
- Convertir una referencia en puntero crudo con
asy con coerción implícita. - Usar los operadores
&raw consty&raw mutpara tomar direcciones sin fabricar referencias. - Leer y escribir a través de un puntero con
*,read/writey sus variantes desalineadas. - Inicializar memoria cruda con
MaybeUninitsin leer jamás el contenido previo.
De una referencia a un puntero
La vía clásica aprovecha que una referencia decae a puntero por coerción. Puedes ser explícito con as o dejar que el tipo de destino guíe la conversión.
let x = 10_i32;
let p: *const i32 = &x; // coercion implicita
let p2 = &x as *const i32; // conversion explicita
let mut y = 20_i32;
let pm = &mut y as *mut i32; // desde una referencia mutable
El problema aparece cuando el lugar cuya dirección quieres no es una referencia válida. Escribir &s.campo construye una & a ese campo, y para que esa referencia exista el campo debe estar alineado e inicializado. En un struct con #[repr(packed)] los campos pierden su alineación natural, así que tomar una referencia a uno es directamente un error de compilación o UB latente. Aquí entra el operador de referencia cruda, &raw, que produce el puntero sin materializar la referencia intermedia:
#[repr(packed)]
struct Paquete { bandera: u8, valor: u32 }
let s = Paquete { bandera: 1, valor: 7 };
// let r = &s.valor; // ERROR: campo potencialmente desalineado
let p = &raw const s.valor; // OK: puntero crudo, no exige alineacion
let v = unsafe { p.read_unaligned() };
assert_eq!(v, 7);
&raw const lugar da un *const T; &raw mut lugar da un *mut T. Son la sintaxis estabilizada que sustituye a las viejas macros addr_of! y addr_of_mut!, y en la edición 2024 son la forma canónica de tomar la dirección de algo que quizá no puedas —o no quieras— prometer válido todavía: campos empaquetados, memoria sin inicializar, o un static mut cuyo acceso por referencia la edición 2024 restringe con dureza.
Si el lugar es un valor corriente, vivo y alineado, &x as *const T es perfecto. Si el lugar podría estar desalineado, sin inicializar o ser un static mut, usa &raw const o &raw mut: tomar la dirección nunca debe exigir que exista una referencia válida cuando lo que precisamente no tienes es un valor válido.
Leer y escribir a través del puntero
Con un puntero en mano, la lectura y la escritura viven dentro de unsafe. El operador * es la vía directa; read y write son las variantes por método, con semántica de movimiento y sin ejecutar el Drop del valor previo.
let mut n = 0_i32;
let p: *mut i32 = &raw mut n;
unsafe {
*p = 99; // escritura directa
let leido = *p; // lectura directa
assert_eq!(leido, 99);
p.write(123); // mueve 123 al destino, sin dropear lo anterior
let x = p.read(); // copia bit a bit el valor, sin dropear el origen
assert_eq!(x, 123);
}
La distinción entre *p = v y p.write(v) es fina pero real: la asignación con * dropea el valor que había en el destino antes de poner el nuevo, mientras que write lo sobrescribe crudo, ideal cuando el destino aún no contiene un valor legítimo que dropear. Para memoria desalineada existen read_unaligned y write_unaligned; para registros de hardware que no deben optimizarse ni reordenarse, read_volatile y write_volatile. Cada *p que ejecutes debe cumplir, sin excepción, cinco condiciones.
No nulo y alineado
La dirección no puede ser null y debe respetar la alineación de T, salvo que uses las variantes _unaligned.
Dentro de una asignación
Debe apuntar a memoria viva de una única asignación con espacio para un T completo; nada de rangos que crucen dos objetos.
Inicializado y del tipo
Para leer, ahí debe vivir un valor válido de T: ni bytes basura, ni un patrón imposible para el tipo.
Aliasing respetado
Si de ese puntero derivas referencias, deben cumplir la exclusividad de &mut y la compartición de &.
Para mover bloques enteros —no un valor sino muchos de golpe— están ptr::copy (permite solapamiento, como memmove) y ptr::copy_nonoverlapping (regiones disjuntas, como memcpy). Son el motor de todo desplazamiento masivo de memoria: insertar en mitad de un Vec, compactar un buffer, realojar los elementos cuando la colección crece.
let origen = [1_u8, 2, 3, 4];
let mut destino = [0_u8; 4];
unsafe {
std::ptr::copy_nonoverlapping(origen.as_ptr(), destino.as_mut_ptr(), 4);
}
assert_eq!(destino, [1, 2, 3, 4]);
Memoria sin inicializar: MaybeUninit
El caso límite es escribir donde todavía no hay nada. No puedes leer memoria sin inicializar ni siquiera para copiarla, y no puedes tomar una &mut a ella porque afirmaría un valor válido inexistente. La herramienta correcta es MaybeUninit<T>: una caja del tamaño de T que el compilador sabe que puede contener basura, de la que obtienes un *mut T para escribir, y que solo declaras inicializada con assume_init cuando tú sabes que ya lo está.
use std::mem::MaybeUninit;
let mut hueco = MaybeUninit::<i32>::uninit();
let p = hueco.as_mut_ptr(); // *mut i32 a memoria sin inicializar
unsafe {
p.write(7); // inicializa sin leer basura previa
let listo = hueco.assume_init(); // ahora si es un i32 valido
assert_eq!(listo, 7);
}
Fíjate en la coreografía: as_mut_ptr da el puntero, write (no *p =, que intentaría dropear la basura previa) lo inicializa, y solo entonces assume_init promete que el contenido es un T legítimo. Este patrón es el latido de todo constructor de estructura de datos que reserva capacidad antes de rellenarla.
flowchart TD L[Un lugar en memoria] --> V[El lugar es valido vivo y alineado] V -->|Si| A[Usa ref x as const T] V -->|No| B[Usa raw const x o raw mut x] A --> D[Puntero crudo] B --> D D --> W[Escribir con write o asterisco dentro de unsafe] D --> R[Leer con read o asterisco dentro de unsafe] style A fill:#a6e3a1,color:#11111b style B fill:#f9e2af,color:#11111b style D fill:#89b4fa,color:#11111b style W fill:#cba6f7,color:#11111b style R fill:#cba6f7,color:#11111b
La existencia misma del operador &raw encierra una de las ideas más profundas del modelo de memoria de Rust: la diferencia entre un lugar y un valor. Cuando escribes &x, no estás señalando una posición neutra en el espacio: estás afirmando que en esa posición vive un valor completo, inicializado y bien formado de su tipo, porque eso es lo que una referencia promete y lo que el compilador tiene derecho a asumir de ella. La referencia no es un puntero con buenos modales; es un puntero que jura. Y ese juramento, que casi siempre es cierto y por eso pasa desapercibido, se vuelve mentira en los márgenes: la memoria que acabas de reservar y aún no has escrito, el campo cuya alineación el empaquetado destruyó, el static mut cuya sola lectura por referencia puede solaparse con otra. En esos márgenes necesitas un modo de decir “dame la dirección de este sitio” sin añadir “y te garantizo lo que hay en él”, porque justamente lo que no tienes es esa garantía. Eso es &raw: la capacidad de hablar de una posición sin comprometerte con su contenido, de nombrar el lugar sin afirmar el valor. Que Rust haya elevado esto a un operador del lenguaje, y no lo deje escondido en una macro, revela hasta qué punto la distinción es fundamental. Programar con punteros crudos es, en el fondo, aprender a separar dos preguntas que las referencias fusionaban en una sola: dónde está, y qué hay ahí. La primera siempre tiene respuesta; la segunda, en el territorio unsafe, es una promesa que emites tú, línea a línea, con cada dereferencia.
De una referencia sale un puntero por coerción o as; de un lugar quizá inválido —campo empaquetado, static mut, memoria sin inicializar— sale con &raw const o &raw mut, que toman la dirección sin fabricar una referencia que afirme validez. Leer y escribir vive en unsafe: *p = v dropea el destino previo, write no; read_unaligned salva la desalineación y copy_nonoverlapping mueve bloques enteros. Inicializar memoria cruda es tarea de MaybeUninit: escribe con el puntero y solo entonces assume_init. Cada *p debe cumplir las cinco condiciones sin excepción.
- Convierte una
&i32a*const i32de las tres formas: coerción,as, y comprobando que ambas dan el mismo tipo. - Declara un
#[repr(packed)]con un campou32e intenta&s.valor; observa el error y arréglalo con&raw const s.valormásread_unaligned. - Sobre un
let mut, escribe con*p = vy luego conp.write(v); describe la diferencia de comportamiento respecto alDropdel valor previo. - Reserva un
MaybeUninit::<[u8; 4]>::uninit, escribe sus cuatro bytes a través del puntero y llamaassume_init; razona por qué no podías leerlo antes. - Explica con tus palabras por qué
&raw constexiste apelando a la distinción entre un lugar y un valor.