Rust's ruwe pointers (*const T en *mut T) zijn primitieve types die alleen een geheugenadres coderen zonder eigendomssemantiek. In tegenstelling tot Box of Rc, bevatten ze geen metadata over de allocatiegrootte of opruimverplichtingen. Wanneer #[derive(Clone)] wordt toegepast op een struct die een ruwe pointer bevat, genereert de compiler een bitwise kopie van het adres, waardoor twee struct-instanties ontstaan die naar dezelfde heap-allocatie verwijzen. Deze oppervlakkige kopie leidt onvermijdelijk tot een dubbele vrijgave wanneer beide instanties worden afgebroken, omdat elke destructor probeert hetzelfde geheugenadres vrij te geven.
Het kernprobleem voortkomt uit de semantische kloof tussen het typesysteem en handmatig geheugenbeheer. De Rust-compiler kan geen onderscheid maken tussen een pointer die heap-geheugen bezit (dat een diepe kopie vereist) en een die slechts extern gegevens leent. Daarom wordt het handmatig implementeren van Clone verplicht om een diepe kopie uit te voeren: het alloceren van nieuw geheugen, het kopiëren van de inhoud van de bronpointer naar de nieuwe buffer, en het omhulden van het nieuwe adres in een aparte struct-instantie. Deze bewerking vereist inherent unsafe blokken omdat derefereren van ruwe pointers om hun gegevens te benaderen buiten de veiligheidswaarborgen van de borrow checker valt.
De oplossing houdt in dat de GlobalAlloc API wordt gebruikt om de oorspronkelijke allocatie te weerspiegelen. De implementatie moet de Layout opslaan die tijdens de initiële allocatie is gebruikt, std::alloc::alloc oproepen om een nieuwe buffer met identieke grootte en uitlijning te maken, en ptr::copy_nonoverlapping gebruiken om de bytes te dupliceren. Kritisch is dat de code allocatiefouten moet afhandelen via handle_alloc_error, ervoor moet zorgen dat de nieuwe pointer uniek is voor de gekloonde instantie, en moet garanderen dat de originele en de kloon geen eigendom delen van de onderliggende bron.
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 } } } }
In een high-performance graphics-engine die integreert met Vulkan, hebben we een AlignedBuffer struct geïmplementeerd om apparaatzichtbaar geheugen te beheren dat 256-byte uitlijning vereist voor uniforme buffers. De toepassing vereiste het klonen van deze buffers bij het starten van achtergrond-asynchrone computetaken die identieke initiële vertexgegevens vereisten zonder de hoofdrenderthread te blokkeren. De kritische beperking was dat Vec<u8> de specifieke uitlijning die door de graphics-driver werd vereist niet kon garanderen, waardoor directe gebruik van std::alloc::alloc en ruwe pointers noodzakelijk was.
Oplossing A: Derive Clone. Deze aanpak past #[derive(Clone)] toe op de AlignedBuffer struct. Voordelen: Geen ontwikkeltijd en geen unsafe codeblokken. Nadelen: Voert een oppervlakkige kopie van de ruwe pointer uit, waardoor zowel de originele als de kloon naar identiek geheugen wijzen; wanneer beide worden afgebroken, crasht de toepassing met een dubbele vrijgave of verstoort het de GPU-driver heap.
Oplossing B: Converteer naar Vec tijdens kloon. Dit aloceert een Vec<u8> met de gegevens, kloont deze met veilige methoden, en converteert vervolgens terug naar een ruwe pointer met de juiste uitlijning. Voordelen: Volledig veilige Rust-code met behulp van standaardbibliotheekabstracties. Nadelen: Vereist twee allocaties en twee kopieën per kloon, schendt de 256-byte uitlijningsvereiste van Vec, en introduceert onaanvaardbare latentie in het renderhulpad.
Oplossing C: Handmatige diepe kopie met unsafe. We implementeren Clone door de opgeslagen Layout te extraheren, std::alloc::alloc aan te roepen, ptr::copy_nonoverlapping te gebruiken om de bytes te dupliceren en een nieuwe AlignedBuffer te construeren met ManuallyDrop-beveiligingen om lekken tijdens paniek te voorkomen. Voordelen: Behoudt de vereiste uitlijning, voert een enkele allocatie per kloon uit, en voldoet aan de zero-copy semantiek voor de gegevensoverdracht. Nadelen: Vereist unsafe code, moet handmatig met out-of-memory-voorwaarden omgaan, en loopt het risico op geheugenlekken als de constructor panikeert na allocatie maar vóór het opslaan van de pointer.
We hebben Oplossing C gekozen omdat het contract voor uitlijning met de Vulkan-driver niet onderhandelbaar was, en het prestatielimiet geen ruimte bood voor overhead van Vec-conversie. De handmatige implementatie gebruikte zorgvuldig ManuallyDrop-beveiligingen tijdens de constructie om opruiming bij paniek te garanderen. Het resultaat was een stabiele 60fps renderloop zonder geheugenlekken die werden gedetecteerd tijdens 48 uur stress testen, en succesvol goedgekeurd door Miri's gestapelde uitleenvalidatie.
Waarom staat de compiler #[derive(Clone)] toe op structs die ruwe pointers bevatten als dit een dubbele vrijgavegevaar creëert?
De Rust-compiler behandelt ruwe pointers als Copy-types, wat betekent dat bitwise duplicatie is gedefinieerd als de kloonbewerking. Aangezien Clone automatisch wordt geïmplementeerd voor elk Copy-type via bitwise kopie, roept #[derive(Clone)] eenvoudig deze oppervlakkige kopie op voor het pointerveld aan. De compiler mist de semantische kennis dat de pointer eigen heap-geheugen vertegenwoordigt; hij behandelt de pointer als een ondoorzichtige gehele adres. Dit onderscheid tussen "het kopiëren van de pointer" en "het klonen van de allocatie" is volledig de verantwoordelijkheid van de ontwikkelaar om handmatig via een aangepaste implementatie vast te leggen.
Wat voorkomt dat we in plaats van Clone de Copy-trait implementeren om onveilige code te vermijden?
Copy en Drop zijn onderling exclusieve traits in Rust. Als een type Drop implementeert om het heap-geheugen vrij te geven dat door de ruwe pointer wordt pointed, kan het geen Copy implementeren. Zelfs als deze beperking werd opgeheven, impliceert de Copy semantiek dat bitwise duplicatie twee onafhankelijke, geldige kopieën van de waarde creëert. Voor heap-bezitende ruwe pointers zou dit nog steeds leiden tot dubbele vrijgave omdat beide kopieën dezelfde geheugenadres zouden proberen vrij te geven wanneer ze buiten bereik raken. Copy is alleen gereserveerd voor types zonder aangepaste vernietigingslogica, zoals gehele nummers of onveranderlijke verwijzingen.
Hoe verbetert std::ptr::NonNull<T> de ruwe pointers bij het implementeren van Clone, en elimineert het de behoefte aan onveilige blokken?
NonNull<T> biedt een niet-nul, covariante wrapper rond *mut T, die betere typeveiligheid biedt en garandeert dat de pointer nooit nul is. Dit stelt compileroptimalisaties zoals nichewaardevulling mogelijk en elimineert nulpointercontroles. Echter, NonNull blijft een abstractie van ruwe pointers die geen eigendomsinformatie of automatisch geheugenbeheer communiceert. Het implementeren van Clone voor een struct die NonNull<T> bevat, vereist nog steeds unsafe blokken om de pointer te derefereren en de diepe kopie uit te voeren. Het voordeel ligt in API-duidelijkheid en variantiecorrectheid, maar de fundamentele vereiste om allocatie handmatig te beheren en dubbele vrijgaven te voorkomen blijft ongewijzigd.