Varyans, tür sistemlerinde, genel parametreler arasındaki alt türleme ilişkilerinin genel tür üzerindeki etkisini belirler. Rust'ın yaklaşımı, bölge tabanlı bellek yönetimi araştırmalarından ve kullanımdan sonra serbest bırakma (use-after-free) açıklarına karşı korunma ihtiyacından büyük ölçüde etkilenmiştir. Rust, değişken referansları (&mut T) tanıttığında, tasarımcılar bunların kovaryant (tıpkı &T gibi), karşıt varyant veya invariat olup olmaması gerektiğine karar vermek zorundaydılar. &mut T için invariat seçimi, çalışma zamanı kontrolleri gerektirmeden bellek güvenliğini korumak için kritik öneme sahipti.
Eğer &mut T T üzerinde kovaryant olsaydı, U türü V türünün bir alt türü olduğunda &mut U ifadesi &mut V yerine ikame edilebilirdi. Yaşam süresi açısından, 'long bir alt tür 'short olduğundan (çünkü 'long 'short'dan daha uzun yaşıyor), bu durumda &mut &'long str değerini &mut &'short str'ye atayabilirdiniz. Bu masum görünse de, bir sağlamlık açığı yaratır.
&mut T T üzerinde invariant'dir. Bu, &mut &'a str ve &mut &'b str türlerinin, yaşam süreleri arasındaki alt türleme ilişkisine bakılmaksızın, 'a tam olarak 'b'ye eşit olmadığı sürece alakalı olmadığını ifade eder. Derleyici, aralarında zorlamaya çalışan kodları reddeder ve değişken dolayım aracılığıyla kısa süreli verilerin daha uzun süreli referansları bekleyen konumlara atanmasını engeller.
Kod Örneği:
fn demonstrate_invariance() { let mut long_lived: &'static str = "statik dize"; // &mut T kovaryant olsaydı, bu derlenebilirdi: // let short_ref: &mut &'short str = &mut long_lived; // Ancak &mut T invariant olduğundan, bu başarısız olur: // hata: yaşam süresi uyuşmazlığı // let short_ref: &mut &'_ str = &mut long_lived; let local = String::from("geçici"); // Eğer yukarıdakine izin verilseydi, şöyle yapabilirdik: // *short_ref = &local; // Artık long_lived, bırakılan verilere (UAF!) işaret ediyor. } // local burada bırakıldı
Bir ekip, yüksek performanslı bir ağ yığını için bir konfigürasyon yöneticisi inşa ediyordu. Temel yapı, sahiplik almadan çalışma zamanı boyunca değiştirilebilen bir protokol konfigürasyonuna yönelik bir değişken referansı tutmak zorundaydı.
Problem: İlk API tasarımı, &mut &'a Config kullandı ve 'a, ağ oturumunun yaşam süresiydi. Geliştiriciler, bunu &mut &'static Config (küresel varsayılan konfigürasyonlar için) ile başlatmaya çalıştılar ve sonra bunu &mut &'session Config bekleyen fonksiyonlara geçirmeye çalıştılar. Derleyici bunu reddetti, çünkü değişmez referanslar (& &'static Config) iyi çalışıyordu.
Düşünülen Çözümler:
1. Dönüştürmeyi Zorlamak İçin Güvensiz Transmute Kullanma Ekip, &mut &'static Config türünü &mut &'session Config türüne dönüştürmek için std::mem::transmute kullanmayı düşündü. Bu, derleyicinin varyans kontrollerini atlamak anlamına gelecekti. Ancak, bu, kısa ömürlü bir konfigürasyon referansını mevcut kapsamı aşacak bir konuma yazmaya izin verir, bu da konfigürasyon bırakıldıktan sonra erişimle hemen tanımsız davranışa yol açar. Üretim kodundaki kullanımdan sonra serbest bırakma riskinin kabul edilemez olduğu belirlendi.
2. Değişmez Referanslara Geçiş API'yi & &'a Config yerine &mut &'a Config kullanacak şekilde değiştirmeyi düşündüler. Paylaşılan referanslar kovaryant olduğundan, & &'static Config & &'session Config'ye dönüştürülebilirdi. Ancak, bu çalışma zamanı güncellemeleri sırasında konfigürasyonları atomik olarak değiştirme yeteneğini ortadan kaldırdı, bu da bağlantıları yeniden başlatmadan sıcak değişim gereksinimiydi.
3. İçsel Değişkenlik için Cell<&'a Config> Kullanma Bu seçenek, paylaşılan bir referans aracılığıyla değişiklik yapmayı sağlayacaktı. Ancak, Cell<T>, aynı güvenlik nedenleriyle T üzerinde de invarant olduğu için varyans sorununu çözmüyordu. Ayrıca, Cell, çoklu iş parçacıklı erişim için eşzamanlama sağlamıyor ve RefCell ile çalışma zamanı borç kontrolü aşırı maliyetli olduğu düşünüldü.
4. Sahip Türler ve Dolayım ile Yeniden Tasarım Seçilen çözüm, referans-to-referans desenini tamamen ortadan kaldırdı. &mut &'a Config yerine, yapı &'a mut ConfigHolder sakladı; burada ConfigHolder sahiplik sarmalayıcıydı. Bu, değişkenliği referans seviyesinden tutucu seviyesine taşıdı, varyans tuzağını önleyerek konfigürasyonları değiştirme yeteneğini korudu. API, kullanıcıların çift referansları yönetmesi gerekmeyecek şekilde daha ergonomik hale geldi.
Sonuç: Yeniden tasarım, güvensiz kod olmadan derlenebilen daha güvenli bir API üretti. &mut T'nin invarant doğası, ekibi yaşam süresi varsayımlarının ihlal edilebileceği bir potansiyel mimari hatayı tanımaya zorladı. Nihai sistem, geçersizliğine son verebilecek bozuk konfigürasyon göstericilerinin varlığını engelledi.
Cell<T> neden T üzerinde invarant ve bu &mut T'nin varyansı ile nasıl ilişkilidir? Cell<T>, paylaşılan referanslar aracılığıyla değişim sağlamayı mümkün kılar. Eğer Cell<T> T üzerinde kovaryant olsaydı, Cell<&'short str>'yi Cell<&'static str>'ye yukarı dönüştürebilirdiniz. Daha sonra, kısa ömürlü bir dize referansını içeride saklayabilir ve daha sonra Cell<&'static str> türü üzerinden okuyabilirdiniz. Bu, geçersiz bir kullanıma neden olacaktı. Bu sebeple, &mut T gibi, Cell<T> (ve UnsafeCell<T>) kısa ömürlü verileri daha uzun ömürlü verileri saklaması gereken bir alana yazmamayı sağlamak için T üzerinde invarant olmalıdır. Bu invarans, RefCell, Mutex ve diğer içsel değişkenlik türlerine yayılır.
PhantomData<T> bir yapının içerdiği gerçek T yoksa varyansı nasıl etkiler ve neden PhantomData<fn(T)> kullanarak karşıt varyans sağlarsınız?
PhantomData<T>, derleyiciye yapının bellek temizleme ve varyans açısından bir T'ye sahipmiş gibi davranmasını söyler. Varsayılan olarak, PhantomData<T> yapıya T ile aynı varyansı verir. Ancak, fonksiyon işaretçileri özel bir varyansa sahiptir: fn(A) -> B A (argüman) açısından karşıt varyant ve B (dönüş) açısından kovaryanttır. Eğer bir yapının yaşam süresi konusunda karşıt varyant olmasını istiyorsanız (yani Struct<'long> Struct<'short> türünün bir alt türü olsun), PhantomData<fn(T)> kullanırsınız. Bu, yaşam süreleri arasındaki ilişki tersine dönerken, tip güvenli geri çağırmalar veya karşılaştırıcılar oluşturmak için kritik öneme sahiptir.
Güvensiz kodda, ham işaretçiler kullanarak kendi kendine referans veren bir yapı uygularsanız, yapı neden yaşam süresi parametreleri üzerinde invarant olarak işaretlenmelidir?
Bir yapı, aynı yapının içinde başka verilere işaret eden bir ham işaretçi içeriyorsa (kendi kendine referans), o yapının yaşam süresi, işaretçinin geçerliliğini belirler. Eğer yapı yaşam süresi 'a üzerinde kovaryant olsaydı, 'a'yı daha kısa bir yaşam süresine, 'b'ye küçültebilirsiniz, bu da yapının yalnızca 'b süresince yaşadığı iddiasında bulunmak anlamına gelir. Ancak, içindeki ham işaretçi, yapının daha uzun süre yaşarken oluşturulmuş olduğundan, artık geçerli olmayacak verilere işaret edebilir. Invarans, yapının daha kısa bir yaşam süresine zorlanamayacağını sağlar ve kendi referansının tür sisteminde kodlanan yaşam süresi boyunca geçerli kalmasını korur. Bu nedenle, Pin, genellikle güvenli olmayan kendi kendine referans veren uygulamalarda açık varyans belirteçleriyle birleştirilir.