Rust bir Drop uygulaması derlediğinde, yöneticinin, struct'ın başlatılmamış veriler içermesi durumunda bile güvenli bir şekilde çalışabilmesini sağlar. Drop::drop metodu &mut self alır, bu da özel erişim sağlar ama mülkiyet vermez. self'ten bir alanı taşımaya çalışmak, struct'ın o kısmını taşınmış bir durumda bırakır ve mantıksal bir çelişki oluşturur: yöneticinin tamamen başlatılmış kaynakları yönetmesi beklenirken, struct'ın bir kısmı tüketilmiş olur.
Bu kısıtlama, kullanımdan sonra taşınma (use-after-move) güvenlik açıklarının önlenmesine yardımcı olur. Eğer Rust parçalı taşınmalara izin verseydi, aynı Drop uygulaması içindeki sonraki kod veya kalan alanların örtük taşınması, başlatılmamış belleğe erişebilirdi. Derleyici, struct alanlarının başlatılma durumunu takip ederek bunu zorlar; Drop içinde bir alanı taşımaya yönelik herhangi bir girişim E0509 ('taşınma çıkışı yapılamaz... tanımlayıcı Drop özelliği ile') hatasını tetikler.
Yıkım sırasında değerleri güvenle çıkarmak için, Rust std::mem::ManuallyDrop sağlar; bu, bir değeri sarmalar ve otomatik yöneticisini devre dışı bırakır. Bu, yıkımın ne zaman ve eğer olacağını açıkça kontrol etmemizi sağlar ve mülkiyet sorumluluğunu programcıya kaydırarak parçalı taşınma kısıtlamasını aşmamıza olanak tanır. ManuallyDrop kullanmak unsafe kod gerektirir ama bir dosya tanıtıcısını çıkarırken otomatik temizliği önlemeyi sağlayarak desenler elde etmemizi sağlar.
Rust'ta yüksek performanslı bir ağ sürücüsü inşa ediyorduk, DMA tamponlarını sıfır kopyalı paket işleme için yönetiyordu. Her Packet struct'ı, kernel belleğine bir ham işaretçi, bir meta veri başlığı ve bir tamamlama geri çağrısı tutuyordu. Standart Drop uygulaması, tamponları kernel havuzuna döndürüyor ve telemetriyi kaydediyordu.
Zorluk, zaman zaman ham tamponun mülkiyetini almak için bir miras C kütüphanesi ile entegre olurken ortaya çıktı, ve böylece çift kopyalama önlenmiş oluyordu. Packet içinden ham işaretçiyi çıkarmamız gerekiyordu ancak kernel geri dönüş mantığını tetiklememek için. Bu gereklilik, Rust'ın Drop'dan çıkış yaparak alanları taşımayı yasaklama kuralı ile doğrudan çelişiyordu.
Ham işaretçiyi *Option<mut u8> içine sarmayı ve Drop içinde take() kullanmayı düşündük. Bu yaklaşım tamamen güvenli ve idiyomatik. Artıları arasında sıfır unsafe kodu ile açıklık var: None, tamponun aktarıldığını gösteriyor. Ancak dezavantajları arasında her erişimde ayrım kontrolü nedeniyle çalışma zamanı üzerindeki ek yük ve işaretçi kavramsal olarak her zaman mevcut olmasına rağmen kod tabanında Option'ı açmanın zorluğu var.
Diğer bir yaklaşım, alanı dışarı taşımak ve ana struct üzerinde std::mem::forget çağrısı yapmak oldu, böylece yöneticisini bastırmış olurduk. Bu, parçalı taşınma hatasını önler ancak dezavantajlar ciddi: forget, tüm diğer alanları (meta veri başlığı ve geri çağırma) sızdırıyor, bu kaynakların ayrı bir şekilde manuel olarak temizlenmesini gerektiriyor. Bu yaklaşım hata yapmaya açık ve RAII ilkelerine aykırıdır.
Ham işaretçiyi *ManuallyDrop<mut u8> içine sarmayı seçtik. Standart Drop uygulamasında, işaretçinin hala geçerli olup olmadığını kontrol etmek için bir atomik bayrak kullandık ve ardından kernel'e geri döndürüldü veya C kütüphanesi için ManuallyDrop::take kullanarak çıkardık. Artıları, sıcak yolda çalışma süresi kontrolü olmaksızın sıfır maliyetli soyutlama ve yıkım zaman çizelgesi üzerinde açık kontrol sağlamak. Dezavantajlar, unsafe blokları ve işaretçiyi iki katına çıkarmadan veya sızdırmadan emin olma sorumluluğunu içeriyor.
Bu çözümü seçtik çünkü performans gereksinimleri Option üzerindeki yükü yasaklıyordu ve kaynak mülkiyetinin aktarılması nadir ama kritik bir yoldu. Sonuç, Rust tarafının güvenlik garantilerini koruduğu ve C entegrasyonunun kaynak sızıntısı olmadan sıfır kopyalı aktarım gerçekleştirdiği temiz bir arayüz oldu.
Neden mem::replace veya mem::swap kullanmanın Drop içinde bazen işe yaradığını, oysa doğrudan taşımaların başarısız olduğunu belirtir misiniz?
Birçok aday, Drop'un tamamen tüm değişiklikleri yasakladığını varsayıyor. Gerçekte, mem::replace işe yarar çünkü taşınan alanın yerine geçerli bir değer bırakmaktadır; bu, yöneticinin uygulanması sırasında tüm alanların her zaman başlatılmış kalmasını sağlar. Derleyici yalnızca alanları başlatılmamış bırakacak taşımaları reddeder (parçalı taşımalar). mem::replace kullanarak, ileride güvenli bir şekilde yok edilebilecek bir "sahte" değer sağlarsınız ve bu, başlatılmamış verilerle ilişkili belirsiz davranıştan kaçınmanızı sağlar. Bu ayrım, Vec gibi toplayıcıların öğeleri temizleme sırasında yeniden düzenlemeleri için kritiktir; başka bir kısmen ayırma tetiklemeksizin, başlatılmamış slotlar üzerinde Drop kodu çalıştırma veriyor.
ManuallyDrop kullanarak alanlar taşındığında bir Drop uygulaması içinde panik yaşanmasının sonuçları nelerdir?
Adaylar genellikle Drop uygulamalarının panik güvenli olması gerektiğini göz ardı ediyorlar. ManuallyDrop::take kullanarak bir değeri çıkardığınızda ve ardından onu yeniden başlatmadan veya güvenli bir şekilde elden çıkarmadan önce panik yaşarsanız, bir sızdırma yaratmış olursunuz. Ancak, ManuallyDrop'ın kendisi içerisindeki içerikler için Drop uygulamaz, bu nedenle çift bir yıkım gerçekleşmez. Önemli olan nokta, eğer panik, diğer yöneticiler aracılığıyla açılırsa, daha önce alınan ManuallyDrop alanları ortadan kalkar, ancak struct'in kendisi (unutulmadıysa) bellek açılma sırasında tekrar yok edilebilir. Bu, sonraki Drop çağrısı sırasında alınan alanlara erişim yapıldığında kullanımdan sonra serbest bırakılma durumuna yol açabilir. Doğru bir panik güvenliği sağlamak dikkatli bir sıralama gerektirir veya tüm struct üzerinde ptr::read ile mem::forget kullanmayı gerektirir.
Drop uygulamasının varlığı, bir yapının yapı eşlemeyi (destructuring) nasıl etkiler?
Geliştiriciler sıklıkla Drop uygulamasının yapı eşlemesini kullanma yeteneğini ortadan kaldırdığını unutur (örneğin, let MyStruct { field } = value), çünkü bu, alanı yöneticiyi çalıştırmadan dışarı taşır. Rust, yöneticilerin yalnızca bir kez çalıştırılmasını gerektirir ve desen eşleşmesi mülkiyetin parçalı olarak taşınmasını sağlar, bu da Drop'u tetiklemez. Bu kısıtlama, RAII kaynaklarının her zaman düzgün bir şekilde serbest bırakılmasını sağlar, hatta programcı değerleri çıkarmaya çalıştığında bile. Yıkım yeteneğini geri kazanmak için, std::mem::ManuallyDrop kullanmalı veya self'i tüketen ve sonunda mem::forget(self) çağrısı yapan özel bir into_inner metodu uygulamalısınız. Bu, otomatik Drop çağrısını önlerken alan çıkışına izin verir. Bu, RAII garantileri ile yapı eşleme esnekliği arasında bir takas yapmak, Rust'ın mülkiyet sisteminin temelini oluşturur.