Rust 1.39'da async/await'in stabilize edilmesi ve 1.33 sürümünde tanıtılan Pin türü, asenkron durum makineleri için kritik olan güvenli kendine referans veren yapıların oluşturulmasını sağladı. Bu yapılar genellikle kendi sahip olduğu verilere referans veren iç işaretçiler içerir, örneğin, tamponlar ve bu tamponlardaki aktif görüntüler. Manuel gelecekler veya karmaşık içe giren veri yapıları uygularken, geliştiricilerin bireysel alanlara Pin<&mut Self> üzerinden erişim sağlaması gerekir; bu da bellek konumu garantilerini koruyan güvenli projeksiyon mekanizmalarını gerektirir.
Bir yapı Pin aracılığıyla sabitlendiğinde, derleyici, tütün türü Unpin'i uygulamadığı sürece, bellek adresinin sabit kalacağına dair garanti verir. Eğer yapı kendine referans veren işaretçiler içeriyorsa, örneğin, bir iç vektöre işaret eden ham işaretçi, yapının hareket etmesi bu işaretçileri geçersiz kılarak dangling referanslar oluşturur. Basit bir projeksiyon yaklaşımı, Pin<&mut Self>'i &mut Self'e doğrudan deref ederek, alanları güvenli Rust koduna açar; bu da bu alanlar üzerinde mem::swap veya mem::replace gibi çağrılar yapılmasına olanak tanıyabilir, böylece bunları sabitlenmiş bellek konumlarından hareket ettirir ve temel pinning sözleşmesini ihlal eder.
Güvenli projeksiyon, pinning varsayımını koruyan bir güvensiz dönüşüm gerektirir: Eğer üst yapı !Unpin ise, alan projeksiyonu &mut Field yerine Pin<&mut Field> döndürmelidir, böylece hareket etmeyi önler. Uygulama, alanın yapısal olarak sabitlenmesini garanti etmelidir; yani, sabitleme durumu, üst yapının sabitleme durumu ile bağlanmalıdır; bu genellikle işaretçi aritmetiği veya Pin::map_unchecked_mut yoluyla sağlanır. Unpin'i uygulayan alanlar için, projeksiyon &mut Field döndürebilir; çünkü bu türler, sabitlenmiş veriler içinde yerleşik olmalarına rağmen hareket etmelerine izin verilir, fakat dikkat edilmelidir ki bu tür hareketler diğer kendine referans veren alanları geçersiz kılmasın.
use std::pin::Pin; use std::marker::PhantomPinned; struct Buffer { data: [u8; 1024], cursor: *const u8, _pin: PhantomPinned, } impl Buffer { // Veri alanına güvenli projeksiyon (Unpin) fn data_mut(self: Pin<&mut Self>) -> &mut [u8; 1024] { unsafe { &mut self.get_unchecked_mut().data } } // İmleç alanına projeksiyon fn cursor(self: Pin<&mut Self>) -> *const u8 { unsafe { self.get_unchecked_mut().cursor } } }
Bağlam
Yüksek performanslı, sıfır kopyalı bir finansal protokol için ayrıştırıcı oluşturuyorduk; burada mesajlar tekrar kullanılabilir bir iç tamponun alt aralıklarına referans verebilir. Ayrıştırıcı durumunun asenkron I/O işlemleri arasında korunması gerekiyordu; bu da kendine referans veren işaretçilerin tamponun içinde sabitlenmelerini gerektiriyordu.
Problemin açıklaması
Parser yapısı bir Vec<u8> tamponu ve o tampondan mevcut mesajı temsil eden &[u8] dilimini içermekteydi. Bu ayrıştırıcı için Stream uygularken, poll_next metodu Pin<&mut Self>'i alıyordu. Tamponu (daha fazla veri okumak için) değiştirirken dilim referansının geçerliliğini korumamız gerekiyordu; bu dikkatli alan projeksiyonunu gerektiriyordu.
Göz önünde bulundurulan çözümler
Çözüm A: İndeks tabanlı adresleme
Bir dilim &[u8] saklamak yerine, vektör içindeki (usize, usize) indekslerini sakladık. Artıları: Tamamen güvenli, hiçbir Pin karmaşıklığı yok, kolay uygulanabilir. Eksileri: Çalışma zamanı sınır kontrolü yükü, her erişim için manuel dilimleme gerektiren daha az ergonomik API, indeks senkronizasyon hatası potansiyeli.
Çözüm B: Ham işaretçiler ile güvenli Pin projeksiyonu
Mesajı ham işaretçi *const u8 olarak ve uzunluğunu sakladık, tamponu erişmek için Pin::map_unchecked_mut kullanarak manuel projeksiyon yöntemleri uyguladık ve işaretçi alanını sabitli tutarak. Artıları: Sıfır maliyetli soyutlama, kendine referanslılığı korur, doğrudan işaretçi aritmetiğine izin verir. Eksileri: unsafe kod blokları gerektirir, Pin varsayımları ihlal edildiğinde belirsiz davranış riski (örneğin, Unpin'i yanlış uygulamak).
Çözüm C: pin-project kütüphanesi kullanımı
Güvenli projeksiyon kodunu otomatik olarak üretmek için prosedürel makrolardan faydalanma. Artıları: Ergonomik, iyi test edilmiş güvenlik varsayımları, gereksiz kodu azaltır. Eksileri: Ek bağımlılık, makro ile üretilen kodun hata ayıklaması daha zor olabilir, hafif bir derleme süresi maliyeti.
Seçilen çözüm ve sonuçlar
Dış bağımlılıkları ortadan kaldırmak ve bellek düzeni üzerinde açık kontrol sağlamak amacıyla Çözüm B'yi seçtik. Yapının Unpin uygulamadığını dikkatlice garanti ettik; bunun için PhantomPinned ekledik ve pinning varsayımlarını doğrulamak için kapsamlı Miri testleri yazdık. Sonuç, mesaj başına hiç tahsisat gerektirmeyen sıfır kopya semantiği ile 10 Gbps throughput’a ulaşan bir ayrıştırıcıydı; CPU doygunluğuna neden olmadı.
Kendine referans veren işaretçiler içeren bir yapı için Unpin uygulamanın neden güvenilir olmadığı?
Unpin, bir türün Pin içinde sarılıyken bile güvenle taşınabilir olduğunu belirtir; bu, güvenli kodun Pin<&mut T>'den &mut T almasına olanak tanır. Kendine referans veren bir yapı olduğunda, yapının hareket etmesi, içindeki içeriklerin bellek adresini değiştirir ve bu içerikleri referans alan iç işaretçileri geçersiz kılar. Unpin uygulamak, güvenli kodun yapı taşınırken hareket etmesine izin verir; bu da asenkron çalışma sürelerine sağlanan güvenlik garantisini ihlal eder ve kullanımdan sonra serbest bırakma (use-after-free) güvenlik açıklarına neden olur. Bu nedenle, bu tür yapılar, PhantomPinned kullanarak Unpin'den açıkça çıkmalı ve kazara otomatik uygulamayı önlemelidir.
Enum varyantları için projeksiyon, yapı alanlarından nasıl farklıdır?
Birçok aday, projeksiyon mekanizmalarının enum ve yapılar için özdeş olduğunu varsayıyor; ancak enumlar, ayrımcı belirleyenin hangi varyantın aktif olduğunu belirlediği için benzersiz zorluklar sunar. Pin<&mut Enum>'i belirli bir varyanta projekte ederken, varyantın sabit kalmasını sağlamak ve ayrımcının değişmesini engellemek gereklidir; çünkü varyantların değiştirilmesi altındaki veriyi hareket ettirir. Rust, varyant projeksiyonu için kararlı yerleşik destek sunmaz; çünkü ayrımcı ve varyant verileri, bellek düzeni açısından birbiriyle paylaşılmaktadır; güvenli projeksiyon, aktif varyantı belirten ve enum sabitken varyant değişimi olmadığını garanti eden güvensiz kod gerektirir.
PhantomPinned'in otomatik özellik uygulamalarını önlemede rolü nedir?
Yeni başlayanların genellikle gözden kaçırdığı durum, Rust'ın çoğu tür için otomatik olarak Unpin uygulaması yapmasıdır; bu, açıkça !Unpin alanları içermezlerse, kapsayıcı türü varsayılan olarak !Unpin hale getirir. PhantomPinned, yapının !Unpin durumunu açıkça tanımlayan, sıfır boyutlu bir işaretçi türüdür; yapının içinde yer aldığında, negatif bir uygulama sınırı olarak hizmet eder. Bu işaretçi olmadan, geliştiriciler yapıların hareket etmeyeceğini varsaydıkları halde güvensiz projeksiyon kodu yazsalar bile, derleyici Unpin'i otomatik olarak uygulayabilir; bu da güvenli kodun Pin::into_inner_unchecked aracılığıyla yapıyı çıkarmasına ve hareket ettirmesine neden olarak güvensiz varsayımları ihlal eder ve tanımsız davranışları tetikler.