I puntatori raw di Rust (*const T e *mut T) sono tipi primitivi che codificano solo un indirizzo di memoria senza semantica di proprietà. A differenza di Box o Rc, non portano alcun metadato riguardante la dimensione di allocazione o gli obblighi di deallocazione. Quando si applica #[derive(Clone)] a una struct contenente un puntatore raw, il compilatore genera una copia bit a bit dell'indirizzo, creando due istanze della struct che condividono la stessa allocazione heap. Questa copia superficiale porta inevitabilmente a un double-free quando entrambe le istanze vengono rilasciate, poiché ogni distruttore tenta di deallocare la stessa regione di memoria.
Il problema principale deriva dal divario semantico tra il sistema di tipi e la gestione manuale della memoria. Il compilatore Rust non può distinguere tra un puntatore che possiede memoria heap (richiedendo una copia profonda) e uno che semplicemente prende in prestito dati esterni. Di conseguenza, implementare Clone manualmente diventa obbligatorio per eseguire una copia profonda: allocando nuova memoria, copiando i contenuti dal puntatore sorgente nel nuovo buffer e avvolgendo il nuovo indirizzo in un'istanza distinta della struct. Questa operazione richiede intrinsecamente blocchi unsafe poiché dereferenziare puntatori raw per accedere ai loro dati esula dalle garanzie di sicurezza del borrow checker.
La soluzione implica l'utilizzo dell'API GlobalAlloc per rispecchiare l'allocazione originale. L'implementazione deve memorizzare il Layout utilizzato durante l'allocazione iniziale, invocare std::alloc::alloc per creare un nuovo buffer con dimensione e allineamento identici e utilizzare ptr::copy_nonoverlapping per duplicare i byte. Criticamente, il codice deve gestire il fallimento dell'allocazione tramite handle_alloc_error, assicurarsi che il nuovo puntatore sia unico per l'istanza clonata e garantire che l'originale e il clone non condividano la proprietà della risorsa sottostante.
use std::alloc::{alloc, handle_alloc_error, Layout}; use std::ptr::{self, NonNull}; struct RawBuffer { ptr: NonNull<u8>, layout: Layout, } impl Clone per 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 } } } }
In un motore grafico ad alte prestazioni che integra Vulkan, abbiamo implementato una struct AlignedBuffer per gestire la memoria visibile dal dispositivo richiedendo un allineamento di 256 byte per i buffer uniformi. L'applicazione necessitava di clonare questi buffer quando si avviavano attività di calcolo asincrone in background che richiedevano i dati iniziali dei vertici identici senza bloccare il thread di rendering principale. Il vincolo critico era che Vec<u8> non poteva garantire il specifico allineamento richiesto dal driver grafico, costringendo all'uso diretto di std::alloc::alloc e puntatori raw.
Soluzione A: Derivare Clone. Questo approccio applica #[derive(Clone)] alla struct AlignedBuffer. Pro: Zero tempo di sviluppo e nessun blocco di codice unsafe. Contro: Esegue una copia superficiale del puntatore raw, causando sia l'originale che il clone a puntare alla stessa memoria; quando entrambi vengono rilasciati, l'applicazione va in crash con un double-free o corrompe il heap del driver GPU.
Soluzione B: Convertire a Vec durante il clone. Questo alloca un Vec<u8> con i dati, lo clona usando metodi sicuri, quindi converte nuovamente in un puntatore raw con l'allineamento corretto. Pro: Codice Rust completamente sicuro che utilizza astrazioni della libreria standard. Contro: Richiede due allocazioni e due copie per clone, viola il requisito di allineamento di 256 byte di Vec, e introduce latenza inaccettabile nel percorso caldo del rendering.
Soluzione C: Copia profonda manuale con unsafe. Implementiamo Clone estraendo il Layout memorizzato, chiamando std::alloc::alloc, utilizzando ptr::copy_nonoverlapping per duplicare i byte, e costruendo un nuovo AlignedBuffer con guardie ManuallyDrop per prevenire perdite durante il panic. Pro: Mantiene l'allineamento richiesto, esegue un'unica allocazione per clone e soddisfa la semantica zero-copy per il trasferimento dei dati. Contro: Richiede codice unsafe, deve gestire manualmente le condizioni di out-of-memory e rischia perdite di memoria se il costruttore va in panic dopo l'allocazione ma prima di memorizzare il puntatore.
Abbiamo scelto la Soluzione C perché il contratto di allineamento con il driver Vulkan era non negoziabile e il budget di prestazioni non permetteva margini per l'overhead della conversione in Vec. L'implementazione manuale ha utilizzato attentamente le guardie ManuallyDrop durante la costruzione per garantire la pulizia in caso di panic. Il risultato è stato un ciclo di rendering stabile a 60fps senza perdite di memoria rilevate in 48 ore di stress test, superando con successo la convalida dei prestiti accumulati di Miri.
Perché il compilatore consente #[derive(Clone)] su struct contenenti puntatori raw se crea un pericolo di double-free?
Il compilatore Rust tratta i puntatori raw come tipi Copy, il che significa che la duplicazione bit a bit è definita come l'operazione clone. Poiché Clone è implementato automaticamente per qualsiasi tipo Copy tramite copia bit a bit, #[derive(Clone)] semplicemente invoca questa copia superficiale per il campo puntatore. Il compilatore non ha conoscenze semantiche che il puntatore rappresenta memoria heap posseduta; tratta il puntatore come un indirizzo intero opaco. Questa distinzione tra "copiare il puntatore" e "clonare l'allocazione" è interamente responsabilità dello sviluppatore da codificare manualmente attraverso implementazioni personalizzate.
Cosa ci impedisce di implementare invece il tratto Copy invece di Clone per evitare di scrivere codice unsafe?
Copy e Drop sono tratti mutuamente esclusivi in Rust. Se un tipo implementa Drop per deallocare la memoria heap puntata dal puntatore raw, non può implementare Copy. Anche se questa restrizione fosse sollevata, le semantiche di Copy implicano che la duplicazione bit a bit crea due copie indipendenti e valide del valore. Per i puntatori raw che possiedono heap, questo porterebbe comunque a double-free perché entrambe le copie tenterebbero di liberare lo stesso indirizzo di memoria quando escono dallo scopo. Copy è riservato esclusivamente a tipi senza logica di distruzione personalizzata, come interi o riferimenti immutabili.
In che modo std::ptr::NonNull<T> migliora i puntatori raw quando implementiamo Clone e riduce la necessità di blocchi unsafe?
NonNull<T> fornisce un involucro non nullo e covariante attorno a *mut T, offrendo una migliore sicurezza di tipo e garantendo che il puntatore non sia mai nullo. Questo consente ottimizzazioni del compilatore come il riempimento dei valori niche e elimina i controlli sui puntatori nulli. Tuttavia, NonNull rimane un'astrazione del puntatore raw che non trasmette informazioni di proprietà o gestione automatica della memoria. Implementare Clone per una struct che contiene NonNull<T> richiede comunque blocchi unsafe per dereferenziare il puntatore e eseguire la copia profonda. Il vantaggio risiede nella chiarezza dell'API e nella correttezza delle varianze, ma il requisito fondamentale di gestire manualmente l'allocazione e prevenire i double-free rimane invariato.