Rust'ın ham işaretçileri (*const T ve *mut T) sahiplik anlamı taşımayan yalnızca bir bellek adresini kodlayan ilkel türlerdir. Box veya Rc gibi, ayrıştırma boyutu veya düşürme yükümlülükleriyle ilgili hiçbir meta veri taşımazlar. Ham işaretçi içeren bir yapı için #[derive(Clone)] uygulandığında, derleyici adresin bit düzeyinde bir kopyasını oluşturur, bu da aynı yığın tahsisatını alan iki yapı örneği oluşturur. Bu yüzeysel kopyalama, her iki örnek de düşürüldüğünde kaçınılmaz olarak bir çift serbest bırakmaya yol açar, çünkü her bir yıkıcı aynı bellek bölgesini serbest bırakmaya çalışır.
Temel problem, tür sistemi ile manuel bellek yönetimi arasındaki anlamsal boşluktan kaynaklanmaktadır. Rust derleyicisi, yığın belleğe sahip olan bir işaretçi ile sadece dış verileri borç alan bir işaretçiyi ayırt edemez. Sonuç olarak, derin bir kopya yapmak için Clone'u manuel olarak uygulamak zorunlu hale gelir: yeni bir bellek tahsis etmek, kaynak işaretçiden yeni tamponun içine içeriği kopyalamak ve yeni adresi ayrı bir yapı örneğine sarmak. Bu işlem doğal olarak, ham işaretçileri dereferanslama, borç kontrolörünün güvenlik garantilerinin dışında kaldığı için güvenli olmayan bloklar gerektirir.
Çözüm, orijinal tahsisi yansıtmak için GlobalAlloc API'sinin kullanılmasını içerir. Uygulama, başlangıçta yapılan tahsiste kullanılan Layout'u saklamalı, yeni bir tampon oluşturmak için std::alloc::alloc'u çağırmalı, aynı boyut ve hizalamaya sahip olmalı ve baytları duplike etmek için ptr::copy_nonoverlapping kullanmalıdır. Ele alınması gereken önemli bir nokta, tahsisat hatalarını handle_alloc_error aracılığıyla yönetmek, yeni işaretçinin klonlanmış örneğe özgü olduğunu sağlamak ve orijinal ile klonun altta yatan kaynağın sahipliğini paylaşmadığından emin olmaktır.
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 } } } }
Vulkan ile entegre bir yüksek performanslı grafik motorunda, 256 bayt hizalaması gerektiren cihaz görünür belleği yönetmek için bir AlignedBuffer yapısı uyguladık. Uygulama, aynı başlangıç tepe verileri gerektiren arka plan asenkron hesaplama görevleri açıldığında bu tamponların klonlanmasını gerektiriyordu ve bu, ana renderleme iş parçacığını bloke etmeden yapılmalıydı. Kritik kısıtlama, Vec<u8>'in grafik sürücüsü tarafından zorunlu kılınan spesifik hizalamayı garanti edememesi olduğundan, doğrudan std::alloc::alloc ve ham işaretçilerin kullanılmasını zorunlu kılmıştır.
Çözüm A: Clone Türetilmesi. Bu yaklaşım, AlignedBuffer yapısına #[derive(Clone)] uygulamaktadır. Artılar: Sıfır geliştirme süresi ve hiçbir güvenli olmayan kod bloğu. Eksiler: Ham işaretçinin yüzeysel bir kopyasını alır, bu da hem orijinalin hem de klonun özdeş belleğe işaret etmesine neden olur; her ikisi de serbest bırakıldığında, uygulama çift serbest bırakma hatası ile çökmesine ya da GPU sürücüsü yığınını bozmasına neden olacaktır.
Çözüm B: Klonlama sırasında Vec'e dönüştürme. Bu, verilerle birlikte bir Vec<u8> tahsis eder, güvenli yöntemler kullanarak klonlar ve sonra doğru hizalayarak ham işaretçiye geri dönüştürür. Artılar: Standart kütüphane soyutlamaları kullanarak tamamen güvenli Rust kodu. Eksiler: Klon başına iki tahsisat ve iki kopya gerektirir, Vec'nin 256 bayt hizalama gereksinimini ihlal eder ve render sıcak yolunda kabul edilemez bir gecikme getirir.
Çözüm C: Güvenli olmayan bir derin kopya ile manuel uygulama. Clone'u uygulayarak saklanan Layout'ı çıkarır, std::alloc::alloc'ı çağırır, ptr::copy_nonoverlapping kullanarak baytları kopyalar ve ManuallyDrop korumaları ile yeni bir AlignedBuffer oluştururuz. Artılar: Gerekli hizalamayı korur, klon başına tek bir tahsisat yapar ve veri aktarımı için sıfır kopyalama anlamlarını karşılar. Eksiler: güvenli olmayan kod gerektirir, bellek yetersizliği koşullarını manuel olarak yönetmeli ve tahsisten sonra ama işaretçiyi saklamadan önce yapıcı hata verirse bellek sızıntıları riski taşır.
Çözüm C'yi seçtik çünkü Vulkan sürücüsü ile olan hizalama sözleşmesi müzakere edilemezdi ve performans bütçesi Vec dönüştürme aşamasında bir gecikmeye yer bırakmıyordu. Manuel uygulama, panic sırasında temizliği sağlamak için yapım sırasında ManuallyDrop korumalarını dikkatlice kullandı. Sonuç, 48 saatlik stres testi sonrasında hiçbir bellek sızıntısı tespit edilmeden, kararlı bir 60fps render döngüsü oldu ve başarılı bir şekilde Miri'nin istif borçları doğrulamasından geçildi.
Ham işaretçileri içeren yapılara neden #[derive(Clone)] uygulamasına izin veriyor?
Rust derleyicisi ham işaretçileri Copy türleri olarak ele alır, yani bit düzeyinde duplike edilmesi klonlama işlemi olarak tanımlanır. Clone'un herhangi bir Copy türü için bit düzeyinde kopya yoluyla otomatik olarak uygulanması nedeniyle, #[derive(Clone)] bu yüzeysel kopyayı işaretçi alanı için basitçe çağrılır. Derleyici, işaretçinin sahipliği altında yığın belleği temsil ettiğine dair anlamsal bilgiye sahip değildir; işaretçiye opak bir tamsayı adresi olarak davranır. "işaretçiyi kopyalamak" ile "tahsisi klonlamak" arasındaki bu ayrım tamamen geliştiricinin sorumluluğundadır ve bunu özel bir uygulama ile kodlamalıdır.
Güvenli olmayan kod yazmaktan kaçınmak için Copy özelliğini uygulamamızı engelleyen nedir?
Copy ve Drop Rust'da karşılıklı olarak dışlayıcı özelliklerdir. Eğer bir tür, ham işaretçiye gösterilen yığın belleği boşaltmak için Drop'u uygularsa, Copy'yi uygulayamaz. Bu kısıtlama kaldırılabilse bile, Copy anlamları bit düzeyinde duplike etmenin iki bağımsız, geçerli kopya oluşturduğunu belirtir. Yığın sahipliğine sahip ham işaretçiler için bu durumda da çift serbest bırakmalar meydana gelecektir, çünkü her iki kopya da kapsam dışına çıktıklarında aynı bellek adresini serbest bırakmaya çalışacaktır. Copy, sadece özel yok etme mantığına sahip olmayan türler için ayrılmıştır; bu türler arasında tam sayılar veya değişmez referanslar bulunur.
std::ptr::NonNull<T>, Clone'ı uygularken ham işaretçileri nasıl iyileştirir ve güvenli olmayan blokların gereksinimini ortadan kaldırır mı?**
NonNull<T>, *mut T etrafında a null olmayan, kovaryant bir sargı sağlar; bu, daha iyi tür güvenliği sunar ve işaretçinin hiçbir zaman null olmadığını garanti eder. Bu, derleyici optimizasyonlarına olanak tanır, ancak NonNull, süregelen herhangi bir bellek yönetim bilgisi veya otomatik yönetime dair bilgi vermez. NonNull<T> içeren bir yapı için Clone'u uygularken, işaretçiyi dereferanslamak ve derin bir kopya yapmak için hala güvenli olmayan bloklar gereklidir. Avantaj, API netliği ve varyans doğruluğundadır, ancak çift serbest bırakmaları önleme ve tahsisat yönetimi gibi temel gereklilikler değişmeden kalır.