Los punteros gruesos de Rust (*const T y *mut T) son tipos primitivos que codifican únicamente una dirección de memoria sin semántica de propiedad. A diferencia de Box o Rc, no llevan ningún metadato respecto al tamaño de la asignación o las obligaciones de liberación. Cuando se aplica #[derive(Clone)] a una estructura que contiene un puntero bruto, el compilador genera una copia bit a bit de la dirección, creando dos instancias de estructura que aliased la misma asignación en el montón. Esta copia superficial conduce inevitablemente a una liberación doble cuando ambas instancias son eliminadas, ya que cada destructor intenta desasignar la misma región de memoria.
El problema principal radica en la brecha semántica entre el sistema de tipos y la gestión manual de memoria. El compilador de Rust no puede distinguir entre un puntero que posee memoria en el montón (que requiere una copia profunda) y uno que simplemente toma prestados datos externos. En consecuencia, implementar Clone manualmente se vuelve obligatorio para realizar una copia profunda: asignar nueva memoria, copiar el contenido del puntero fuente al nuevo búfer, y envolver la nueva dirección en una instancia de estructura distinta. Esta operación requiere inherentemente bloques inseguros porque desreferenciar punteros brutos para acceder a sus datos está fuera de las garantías de seguridad del verificador de préstamos.
La solución implica utilizar la API GlobalAlloc para reflejar la asignación original. La implementación debe almacenar el Layout utilizado durante la asignación inicial, invocar std::alloc::alloc para crear un nuevo búfer con el mismo tamaño y alineación, y usar ptr::copy_nonoverlapping para duplicar los bytes. Críticamente, el código debe manejar fallos de asignación a través de handle_alloc_error, asegurar que el nuevo puntero sea único para la instancia clonada, y garantizar que el original y el clon no compartan la propiedad del recurso subyacente.
use std::alloc::{alloc, handle_alloc_error, Layout}; use std::ptr::{self, NonNull}; struct RawBuffer { ptr: NonNull<u8>, layout: Layout, } impl Clone for RawBuffer { fn clone(&self) -> Self { unsafe { let new_ptr = alloc(self.layout); if new_ptr.is_null() { handle_alloc_error(self.layout); } let new_ptr = NonNull::new_unchecked(new_ptr); ptr::copy_nonoverlapping( self.ptr.as_ptr(), new_ptr.as_ptr(), self.layout.size() ); RawBuffer { ptr: new_ptr, layout: self.layout } } } }
En un motor gráfico de alto rendimiento que se integra con Vulkan, implementamos una estructura AlignedBuffer para gestionar la memoria visible para el dispositivo que requiere una alineación de 256 bytes para los búferes uniformes. La aplicación requería clonar estos búferes al iniciar tareas de cálculo asíncronas en segundo plano que necesitaban los mismos datos de vértices iniciales sin bloquear el hilo principal de renderizado. La restricción crítica era que Vec<u8> no podía garantizar la alineación específica exigida por el controlador gráfico, obligando el uso directo de std::alloc::alloc y punteros brutos.
Solución A: Derivar Clone. Este enfoque aplica #[derive(Clone)] a la estructura AlignedBuffer. Pros: Cero tiempo de desarrollo y sin bloques de código inseguros. Contras: Realiza una copia superficial del puntero bruto, causando que tanto el original como el clon apunten a la misma memoria; cuando ambos son eliminados, la aplicación se bloquea con una liberación doble o corrompe el montón del controlador de GPU.
Solución B: Convertir a Vec durante la clonación. Esto asigna un Vec<u8> con los datos, lo clona usando métodos seguros, y luego convierte de nuevo a un puntero bruto con la alineación correcta. Pros: Código Rust completamente seguro usando abstracciones de la biblioteca estándar. Contras: Requiere dos asignaciones y dos copias por clon, viola el requisito de alineación de 256 bytes de Vec, e introduce una latencia inaceptable en el camino de renderizado caliente.
Solución C: Copia profunda manual con inseguro. Implementamos Clone extrayendo el Layout almacenado, llamando a std::alloc::alloc, usando ptr::copy_nonoverlapping para duplicar los bytes, y construyendo un nuevo AlignedBuffer con guardas de ManuallyDrop para prevenir fugas durante pánicos. Pros: Mantiene la alineación requerida, realiza una única asignación por clon, y satisface la semántica de cero copias para la transferencia de datos. Contras: Requiere código inseguro, debe manejar manualmente las condiciones de falta de memoria, y corre el riesgo de fugas de memoria si el constructor genera un pánico después de asignar pero antes de almacenar el puntero.
Elegimos Solución C porque el contrato de alineación con el controlador de Vulkan no era negociable, y el presupuesto de rendimiento no permitía el tiempo de sobrecarga de conversión de Vec. La implementación manual utilizó cuidadosamente guardas de ManuallyDrop durante la construcción para asegurar la limpieza en caso de pánico. El resultado fue un bucle de renderizado estable a 60fps sin fugas de memoria detectadas en 48 horas de pruebas de estrés, superando exitosamente la validación de préstamos apilados de Miri.
¿Por qué el compilador permite #[derive(Clone)] en estructuras que contienen punteros brutos si crea un peligro de liberación doble?
El compilador de Rust trata los punteros brutos como tipos Copy, lo que significa que la duplicación bit a bit se define como la operación de clonación. Dado que Clone se implementa automáticamente para cualquier tipo Copy a través de la copia bit a bit, #[derive(Clone)] simplemente invoca esta copia superficial para el campo del puntero. El compilador carece de conocimiento semántico de que el puntero representa memoria en el montón; lo trata como una dirección entera opaca. Esta distinción entre "copiar el puntero" y "clonar la asignación" es completamente responsabilidad del desarrollador para codificar manualmente a través de una implementación personalizada.
¿Qué nos impide implementar el rasgo Copy en lugar de Clone para evitar escribir código inseguro?
Copy y Drop son rasgos mutuamente excluyentes en Rust. Si un tipo implementa Drop para desasignar la memoria del montón apuntada por el puntero bruto, no puede implementar Copy. Incluso si se levantara esta restricción, la semántica de Copy implica que la duplicación bit a bit crea dos copias independientes y válidas del valor. Para punteros brutos que poseen el montón, esto aún resultaría en liberaciones dobles porque ambas copias intentarían liberar la misma dirección de memoria cuando salgan de su ámbito. Copy está reservado estrictamente para tipos sin lógica de destrucción personalizada, como enteros o referencias inmutables.
¿Cómo mejora std::ptr::NonNull<T> sobre los punteros brutos al implementar Clone, y elimina la necesidad de bloques inseguros?
NonNull<T> proporciona un contenedor no nulo y covariante alrededor de *mut T, ofreciendo mejor seguridad de tipos y garantizando que el puntero nunca sea nulo. Esto permite optimizaciones del compilador como el llenado de valores nicho y elimina las comprobaciones de punteros nulos. Sin embargo, NonNull sigue siendo una abstracción de punteros brutos que no transmite información de propiedad ni gestión de memoria automática. Implementar Clone para una estructura que contiene NonNull<T> aún requiere bloques inseguros para desreferenciar el puntero y realizar la copia profunda. La ventaja radica en la claridad de la API y la corrección de variancias, pero el requisito fundamental de gestionar manualmente la asignación y evitar liberaciones dobles persiste sin cambios.