Geçmiş: Rust 1.0'da PhantomData'nın stabilizasyonundan önce geliştiriciler, yalnızca ham işaretçiler depolayan yapılar (örneğin C kütüphaneleriyle sarmalanan nesneler) için tür ilişkilerini ifade etmede zorluk çekiyorlardı. Derleyici, varyansı ve sahipliği çıkarım yapmak için yalnızca somut alanlara dayanıyordu ve bu da ya aşırı kısıtlayıcı yaşam döngüsü hatalarına ya da türün içeriğiyle ilgisiz olduğu varsayıldığında sessiz bellek güvenliği ihlallerine yol açıyordu. PhantomData, çalışma zamanı maliyeti olmaksızın varyansı, sahipliği ve özellik sonuçlarını açık bir şekilde iletmek için sıfır boyutlu bir işaretçi olarak tanıtıldı.
Sorun: Özel bir akıllı işaretçi olarak struct RawBox<T> { ptr: *const T } düşünün. *const T T üzerinde kovaryandır, ancak derleyici RawBox'un mantıksal olarak T değerini sahiplenmesine dair açık bir onay almaz, özellikle Drop Check (dropck) ile ilgili olarak. PhantomData olmadan, derleyici T'yi yapının yalnızca bahsettiği ancak sahiplenmediği tamamen sentetik bir tür parametresi olarak değerlendirir, bu da T'nin düşmesine izin verebilirken yapı hala ham işaretçiyi belleğini tutabilir. Bu ihmal ayrıca yapının T'nin özelliklerine dayanarak otomatik özellikler gibi Send ve Sync'i doğru bir şekilde uygulamasını engeller.
Çözüm: PhantomData<T> alanı ekleyerek, RawBox'u T üzerinde kovaryant olarak açıkça işaretlersiniz ve mantıksal sahipliği belirtirsiniz. Bu, derleyicinin T'nin yapıyı aşacağını zorunlu kılmasını ve alt türler için doğru varyans kurallarını uygulamasını sağlar. Farklı varyans gereksinimleri için, PhantomData farklı tür yapıcıları kabul eder: PhantomData<fn(T)> kontravaryans oluştururken, PhantomData<*mut T> veya PhantomData<Cell<T>> invariansı zorunlu kılar. Bu mekanizma, Rust'ın sıfır maliyet garantilerini korurken ham işaretçiler üzerinde güvenli soyutlama sağlar.
Yüksek performanslı bir ses işleme kütüphanesi geliştirirken, aslında bir Rust yapısı olan AudioBuffer<T>'ye yazılmış bir C API işleyicisi *mut AudioContext'i sarmalamak zorundaydım ve T f32 veya i16 olabilirdi. Sarmalayıcı AudioHandle<T>, yalnızca ham işaretçiyi ve bir vtable işaretçisini depoluyordu, ancak yaşam döngüleri ve iş parçacığı güvenliği açısından Box<AudioBuffer<T>> gibi davranmasını istedim. Özellikle, T Send olduğunda handle'ın Send olmasına ve ses örnek türlerinin sorunsuz bir şekilde yer değiştirmesine izin vermek için T üzerinde kovaryant olmasına ihtiyaç vardı.
İlk yaklaşım, herhangi bir işaretçi atlamaya ve yalnızca *mut c_void alanına güvenmeye dayanıyordu. Bu strateji, minimum yapı boyutu korumuş ve herhangi bir boilerplate'i önlemişti; bu da başlıca avantajlarıydı. Ancak, derleyici AudioHandle<T>'nin T üzerinde invariant olduğunu varsaydığı için ve sahipliği doğrulayamıyordu, Send uygulamasını reddetti, bu da çapraz iş parçacığı handle hareketini gerektiren API sözleşmesini bozmuştu.
İkinci yaklaşım, yalnızca tür sistemini yönlendirmek için bir Option<Box<T> depolamayı düşündü. Bu yöntem, doğru bir biçimde varyansı ve Send/Sync türetimini sağladı, özellik uygulama sorunlarını çözdü. Ne yazık ki, yapı boyutunu iki katına çıkardı ve C işaretçisi ile sahte alanın uygun bir şekilde senkronize edilmediği takdirde panik yapma riski taşıyan karmaşık bir bırakma mantığı getirdi, bu da sıfır maliyet soyutlama amacını boşa çıkardı.
Seçilen çözüm, yapıya marker: PhantomData<AudioBuffer<T>> eklemek oldu. Bu sıfır boyutlu işaretçi, hızla T üzerinde kovaryant anlamlar kazandırdı, T'ye dayanarak otomatik özelliklerin doğru bir şekilde türetilmesini sağladı ve Drop Check'in AudioBuffer<T>'nin handle'dan önce düşmediğini doğrulamasını sağladı. Sonuç olarak, FFI sarmalayıcı hatasız derlendi, herhangi bir çalışma zamanı maliyeti getirmedi ve T Send olduğunda ses işleyicilerinin çapraz iş parçacığı hareketine güvenli bir şekilde izin verdi, böylece kütüphanenin gereksinimlerini mükemmel bir şekilde karşıladı.
Neden PhantomData<T> özellikle bir değerin, referanslı verilerin hala canlı olduğu sırada düşmesini önleyen Drop Check (dropck) kuralını tetikler ve bunun olmaması durumunda ne tür bir güvensizlik meydana gelebilir?
PhantomData<T> olmadan, derleyici yapının T'yi sahiplenmediğini varsayar, bu da kullanıcı kodunun T'yi düşürmesine izin verirken yapının Drop uygulaması hala T'nin belleğini tutabilir. Bu, yıkıcı çalıştığında bir kullanımdan sonra serbest bırakmaya yol açar, çünkü bellek yeniden tahsis edilmiş veya zehirlenmiş olabilir. PhantomData, yapının mantıken T'yi içerdiğini düşürmekte ve derleyiciye T'nin yapıyı aşacağına dair doğrulamayı sağlamakta, böylece bu güvensizlik önlenmektedir; bunu yaparken T hiç bayt almamakta.
PhantomData nasıl bir tür parametresi üzerinde kontravaryansı zorlamak için kullanılabilir ve bu tür bir API tasarımında bu neden gereklidir?**
Kontravaryans, PhantomData<fn(T)> kullanarak elde edilir. Bu, struct Comparator<T> { compare: fn(T, T) -> Ordering, _marker: PhantomData<fn(T)> } gibi geri çağırma depolama türleri için gereklidir. Çünkü fn(T) T üzerinde kontravaryandır, yapı, &'static str kabul eden bir karşılaştırıcının, &'short str karşılaştırıcısı bekleyen her yerde kullanılabileceğini doğru bir şekilde modellemekte, bu da kovaryansın zıttı bir ilişkidir ve işlev işaretçisi alt türlendirmesi için kritiktir.
PhantomData<Cell<T>> ile PhantomData<T>'nin varyans sonuçlarını ne ayırır ve güvenli iç değişkenlik ilkesini sarmalayan bir yapının neden birincisini gerektirebileceği?**
PhantomData<T> kovaryansı ifade ederken, PhantomData<Cell<T>> içindeki içeriği üzerindeki invariansı zorlar. MyRefCell<T> gibi hem UnsafeCell destekli bir konteyner inşa ederken invarians, MyRefCell<&'long str>'nin MyRefCell<&'short str>'ye dönüştürülmesini engellemek için gereklidir. Böyle bir zorlamanın, kısa ömürlü bir referansın, uzun ömürlü bir referans beklenirken depolanmasını sağlaması, aliasing kurallarını ihlal eder ve yazma işlemleri sırasında sarkık işaretçilere neden olur, ki bu da invariant işaretçinin engellediği bir durumdur.