1 puan yazan GN⁺ 4 시간 전 | 1 yorum | WhatsApp'ta paylaş
  • std::pin::Pin, bir işaretçinin gösterdiği değerin o işaretçi üzerinden taşınmayacağını ifade eden tip düzeyinde bir garantidir; kendi içini referanslayan tipler gibi adresinin kararlı olması gereken değerler nedeniyle gereklidir
  • async/await içinde, .await sonrasında da yaşamaya devam eden yerel değişkenler ve referanslar derleyicinin ürettiği durum makinesinin alanları olabilir; bu yüzden future'ın poll edildikten sonra taşınmasını engellemek için Future::poll Pin ister
  • Pin, sabitlenmiş bir değerin güvenli kodla taşınmasını engeller ama genel değişiklikleri tamamen yasaklamaz; T: Unpin değilse Pin içinden güvenli şekilde &mut T çıkarılamaz
  • Rust'taki çoğu tip varsayılan olarak Unpin olduğu için, taşınmaması gereken kendi kendine referanslı struct'lar genelde bir PhantomPinned alanı eklenerek !Unpin yapılmalıdır
  • Pratikte, bir future'ı doğrudan poll ederken veya pinned future isteyen bir API'ye geçirirken Box::pin ya da std::pin::pin! kullanılır; Future ya da düşük seviyeli async ilkel tiplerini doğrudan uygularken ise unsafe değişmezleriyle de ilgilenmek gerekir

Pin neden gerekli?

  • std::pin::Pin, bir işaretçi sarmalayıcısıdır ve işaretçinin gösterdiği değerin o işaretçi üzerinden taşınmayacağı garantisini temsil eder
  • Temel sorun kendi kendine referanslı tiplerde ortaya çıkar
    • Örnek SelfRef struct'ı data: i32 ve ptr: *const i32 alanlarına sahiptir; ptr, self.datayı gösterir
    • Struct örneği başka bir değişkene taşınırsa veya bir fonksiyondan döndürülürse bellek adresi değişebilir
    • Ham işaretçi ptr, eski bellek konumunu göstermeye devam eder ve dangling pointer haline gelir
  • Kendi kendine referans kurulduktan sonra, ilgili değerin tekrar taşınmasını engelleyen bir mekanizmaya ihtiyaç vardır

async/await ve Future içinde ortaya çıkan sorun

  • async/await ve Future, Pinin sık görüldüğü başlıca alanlardır
  • .await noktasını aşarak yaşamaya devam eden yerel değişkenler, derleyicinin ürettiği durum makinesinin alanları haline gelir
  • Bir yerel değişkene ait referans da aynı .await sonrasında yaşamaya devam ederse, oluşan future kendi kendine referanslı olabilir
  • Polling başladıktan sonra future, kendi içindeki başka alanları gösteren referanslara bağlı olabilir
    • Bu durumda future taşınırsa ilgili referanslar geçersiz hale gelir
  • Bunu önlemek için Future::poll, &mut self yerine Pin alır
pub trait Future {
    type Output;
    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}
  • Polling başlamadan önce future serbestçe taşınabilir
  • Ancak bir kez Pin<&mut Self> ile poll edildikten sonra, kalan yaşam süresi boyunca yerinde kalmalıdır

Pin tam olarak neyi garanti eder?

  • Pin<Ptr> tek başına sihirli bir şey yapmaz; garanti, Pin API'sinin güvenli kısmının verdiği kısıtlarla sağlanır
  • Önemli nokta, Pinin işaretçinin gösterdiği değer için geçerli olmasıdır; işaretçinin kendisi için değil
    • Pin<Box<T>> taşınabilir
    • Ama içindeki T, Box içinde sabit bir adreste kalır
  • Pin, değeri tamamen değiştirilemez yapmaz; asıl engellediği şey değerin taşınmasıdır
    • Pin<&mut T> üzerinden, taşınma gerektirmeyen değişiklikler hâlâ yapılabilir
    • Örneğin alanların yerinde güncellenmesi mümkündür
  • Fakat güvenli kodun Tyi dışarı çıkarıp taşımasını önlemek gerekir
    • Bu yüzden Pin, rastgele şekilde &mut Tye erişim vermez

Neden &mut T verilmiyor?

  • Normal bir &mut T, sahibine mem::replace gibi işlemlerle değeri taşıma veya değiştirme yetkisi verir
let mut x = 5;
let y = std::mem::replace(&mut x, 6); // moves old value out
  • Eğer Pin<&mut T> her zaman güvenli biçimde &mut Tye dönüştürülebilseydi, sabitlenmiş bir !Unpin değer pin'in arkasından taşınabilirdi
  • Bu yüzden Pin yalnızca bazı durumlarda &mut Tye erişime izin verir
impl<'a, T: ?Sized> Pin<&'a mut T> {
    pub const fn get_mut(self) -> &'a mut T
    where
        T: Unpin
    { ... }
}
  • Tip Unpin uyguluyorsa, bu tip Pin olmadan da güvenle taşınabilir
  • Bu yüzden Pinden &mut T çıkarmak güvenlidir
  • Tip Unpin uygulamayan bir !Unpin ise, güvenli kodla &mut T elde edilemez
  • Bu durumda Pin::get_unchecked_mut gibi unsafe yöntemler kullanılmalıdır ve kod, değerin o referansın dışından taşınmayacağı sözünü tutmalıdır

Unpin ve PhantomPinned

  • Unpin uygulayan tipler bellek güvenliği için pinning'e bağımlı değildir
// std::marker
pub auto trait Unpin {}
  • Rust'taki çoğu tip, taşınsa da sorun çıkarmadığı için varsayılan olarak Unpindir
    • Örnek: i32, String, Vec
  • Unpin, açıkça !Unpin yapılmadıkça tüm tiplere otomatik olarak uygulanır
  • std::marker::PhantomPinned, açıkça !Unpin olan bir işaretleyici struct'tır
    • Auto trait'ler otomatik yayıldığı için, PhantomPinned alanı içeren bir struct da otomatik olarak !Unpin olur
use std::marker::PhantomPinned;

struct SelfRef {
    data: i32,
    ptr: *const i32,
    _phantom: PhantomPinned, // makes the entire struct !Unpin
}
  • Bu, kullanıcı tanımlı bir struct'ın sabitlendikten sonra taşınmasının güvensiz olduğunu beyan etmenin standart yoludur
  • Derleyici, genelde unsafe ham işaretçilerle kurulan kendi kendine referansları otomatik olarak algılayamaz
  • Bu yüzden geliştirici, kendi kendine referanslı struct için Unpin özelliğinden açıkça vazgeçmelidir
    • Genellikle bir PhantomPinned alanı eklenerek yapılır
  • Kendi kendine referanslı bir tip yanlışlıkla Unpin olarak kalırsa, güvenli kod Pin içinden değiştirilebilir referans çıkarıp değeri taşıyabilir
    • Bu da unsafe kodun kendi kendine referans kurarken yaptığı varsayımları bozar

Pin nasıl oluşturulur?

  • Pin kendi başına bir değeri sabitlemez

  • Pin oluşturmak, ilgili pointee'nin pin'in yaşam süresi boyunca kararlı bir bellek konumunda kalacağını kanıtlamak anlamına gelir

  • Pin::new

    • En basit oluşturma yöntemi Pin::newdür
    let mut value = 42;
    let pinned = Pin::new(&mut value);
    
    • Bu oluşturucu yalnızca T: Unpin olduğunda kullanılabilir
    • Unpin tipler pinning'e bağımlı olmadığından, Pin ile sarmalamak her zaman güvenlidir
    • Bu durumda pinning garantisi pratikte no-op olur
  • std::pin::pin!

    • Heap allocation olmadan yerel olarak bir değeri pinlemek gerektiğinde pin! makrosu kullanılabilir
    use std::pin::pin;
    
    let future = pin!(async {
        println!("Hello");
    });
    
    • Bu makro yerel bir değişken oluşturur ve o değişkeni gösteren bir Pin döndürür
    • Derleyici ilgili yerel değişkenin kalan yaşam süresi boyunca taşınmayacağını garanti ettiği için, stack üzerinde !Unpin değerler güvenle pinlenebilir
    • Adına rağmen pin!, stack belleğinin kendisini pinlemez
    • Yalnızca yerel değişkene bağlı sabit bir referans oluşturur; değişken scope dışına çıktığında pinning garantisi de biter
  • Box::pin

    • !Unpin tiplerde en yaygın oluşturucu Box::pindir
    let pinned = Box::pin(SelfRef { ... });
    
    • pin! yerel değişkene bağlı bir Pin üretirken, Box::pin Boxın sahip olduğu bir Pin döndürür
    • Heap allocation'ın kendisi taşınmadığı için pointee, Boxın yaşamı boyunca kararlı bir bellek konumuna sahip olur
    • Boxın kendisi taşınsa bile sahip olduğu değer taşınmaz; yalnızca Box içindeki işaretçi taşınır
    • Heap allocation aynı adreste kalır
  • Pin::new_unchecked

    • Güvenli oluşturucular değerin yerinde kalacağını kanıtlayamadığında, Pin doğrudan unsafe kodla oluşturulabilir
    let pinned = unsafe { Pin::new_unchecked(ptr) };
    
    • Pin::new_unchecked çağıran taraf, döndürülen Pinin yaşamı boyunca pointee'nin hiçbir işaretçi üzerinden tekrar taşınmayacağını taahhüt eder
    • Bu söz bozulursa, pinning garantisine dayanan kodda tanımsız davranış ortaya çıkabilir
    • Bu yüzden genellikle ancak bu değişmezi koruyabilen düşük seviyeli soyutlamalar yazılırken kullanılır

Pratikte ne zaman dikkat etmek gerekir?

  • Çoğu Rust geliştiricisi için Pin ve Unpin arka planda sessizce çalışır
  • Doğrudan ilgilenmeniz gereken durumlar genelde iki tanedir
    • async kod tüketimi: bir future'ı doğrudan poll etmeniz veya pinned future bekleyen bir API'ye vermeniz gerekiyorsa, Box::pin(future) ile heap'te ya da std::pin::pin!(future) ile yerel stack'te pinlersiniz
    • Future'ı doğrudan uygulama: özel durum makineleri veya düşük seviyeli async ilkel tipleri yazarken Pin ile uğraşmanız gerekir; pinning değişmezlerini korumak için PhantomPinned ve unsafe kod gerekebilir
  • Pin, adres duyarlı tipler sorununu çözmek için Rust'ın zero-cost yaklaşımıdır
  • Böylece Rust, garbage collector olmadan bellek güvenliği garantilerini korurken async/await ve diğer kendi kendine referanslı soyutlamaları kullanabilir

1 yorum

 
GN⁺ 4 시간 전
Lobste.rs yorumları
  • std::pin::Pin, Rust dünyasının Monad'ı gibi. Bir kez anladığınızda blog yazısı yazmadan duramıyorsunuz

    • Böyle yazılar genelde monad tutorial fallacy tuzağına düşmeye yatkın olur
    • Monad zamanındaki gibi, bu blog yazılarının aslında hiçbir şeyi doğru düzgün açıklayamadığı anlamına mı geliyor?
  • Pini anlamaya çalışırken benim ve başkalarının takıldığı birkaç noktayı ele almak iyi olabilir
    Unpin adı pek iyi değil. Daha doğru ama yine pek iyi olmayan adlar MovableWhenPinned ya da PinIsNoOp olabilirdi
    nightly'deki !Unpin çift olumsuzlaması garip görünüyor; ama mevcut tipleri %99'luk varsayılan durum olarak bırakmak için tipin dışına çıkabileceği otomatik trait Unpini eklemek gerektiğinden böyle oldu. Bunu !MovableWhenPinned diye düşününce daha mantıklı geliyor
    Kararlı sürümdeki alternatif olan PhantomPinnedın adı da çok iyi değil; çünkü pinned olma durumu, pinned bir referans bulunduğu için oluşan geçici bir durumdur, tipin bir özelliği değildir. Alternatif ad PhantomNotMovableWhenPinned gibi bir şey olabilirdi
    Kafamda bu şekilde çevirmeye başlayınca çok daha iyi anladım. Elbette hâlâ kafa karıştırıcı; belki de şanslıydım

    • Tamamen katılıyorum. Eskiden !Unpin kafamı ağrıtıyordu; Unpini SafeToUnpin olarak okumaya başlayınca biraz rahatladı
  • Eskiden bu soruyu sormuştum ve biri düşünceli bir yanıt vermişti sanırım, ama hatırlamıyorum. Benim anladığım kadarıyla Pin async'ten çıktı; sorun, yerel değişken referanslarının belirli bir fonksiyonun durum makinesini temsil eden veri yığını içinde kendine referanslı hâle gelmesiydi
    Async durum taşınırsa bu yerel değişken referansları eski, yanlış konumları göstermeye başlar
    Ama bu, yalnızca referansların tam mutlak adrese sahip gerçek pointer'lar olması yüzünden değil mi? Çözümün referansları göreli adres yapmak yerine taşıma yeteneğini kaldırmak olmasının nedenini merak ediyorum
    Yanıt büyük ölçüde “derleyici, CPU ve OS pointer'ları çok iyi işleyebilsin diye milyonlarca mühendis-yılı harcandığından pointer'lar birçok açıdan daha iyidir; bu yüzden sağda solda Pin kullanmak daha iyidir” mi, yoksa göreli referansların alternatif olarak gerçekten mümkün olmamasına dair katı bir neden mi var, merak ediyorum

    • Sorun yalnızca async durum içindeki bir yerel değişkenin aynı durum içindeki başka bir yerel değişkeni doğrudan referanslaması değil. O durumda derleyici tüm yerel değişkenleri bildiği için erişimi göreli hâle getirebilir. Ama bir tipin derinlerindeki bir referans başka bir tipin derinlerindeki bir değeri gösteriyorsa iş çok daha zorlaşır
      Referanslar göreli olsaydı, bu tiplerin async durum içinde kullanılıp kullanılmadığına bağlı olarak bellek temsilleri değişmek zorunda kalırdı; ayrıca göreli referanstan gerçek pointer'ı geri oluşturmak için birlikte taşınması gereken bir temel pointer kavramı da gerekirdi
      Pinned bir referans içindeki iç içe nesneler, kök nesne pinned olsa bile hâlâ serbestçe taşınabildiğinden, varsayımsal göreli referansların hepsinin aynı temel pointer'a göreli olduğunu da söyleyemeyiz
      Sonuçta mutlak pointer gerekiyor ve göreli referanslar pek uymuyor. Peki Rust derleyicisi buradaki tipleri bildiğine göre, tüm nesne grafiğini izleyip taşınan nesneyi gösteren referansları yeni konuma düzelterek nesneyi taşınabilir yapsa nasıl olur? O zaman fiilen izlemeli çöp toplayıcı yapmış olursunuz
      Üstelik Rust derleyicisi nesne grafiğindeki tüm tipleri bilmez. Referanslar FFI üzerinden aktarılabilir ve dış kütüphane o referansı saklayabilir. FFI sınırını aşan taşınmış referansları düzeltmek pratikte başa çıkılması zor bir problemdir
      Bu yüzden gerçekten zor. Nesne taşımanın kendisinin de görece yeni bir teknik olması önemli. Çoğu C/C++ programında tüm nesnelerin örtük olarak pinned olduğu söylenebilir. O tarafta pinning'in daha az tartışılmasının nedeni, nesnelerin ya hiç taşınmaması ya da taşınsalar bile dangling referans kalmamasını sağlamanın programcının sorumluluğunda olmasıdır
    • Pin, Rust'ın belleği opak bir bit yığını gibi keyfince taşıyamayan diğer dillerle birlikte çalışabilirliği için de gerekli
      Benim anladığım kadarıyla C++ birlikte çalışabilirliğindeki sorunlardan biri, nesnelerin serbestçe taşınabilen basit bit yığınları olmaması; sonuçta epey çok tip için pinning gerekiyor ve kullanım ergonomisi kötüleşiyor
      Ancak bu, en az 6 ay kadar önce bu iş üzerinde çalışan kişilerle yaptığım konuşmaya dayanıyor; o zamandan beri durum ne kadar iyileşti bilmiyorum
  • Genel olarak resmi Rust belgelerine ek olarak okunması iyi bir açıklama bence. Probleme giriş biçimi biraz daha yumuşak
    Yine de kendine referanslı struct ile başlamanın, bunu tamamen çıkarmaktan daha kafa karıştırıcı olduğunu düşünüyorum. Özellikle girişteki “Bu nedenle böyle bir kendine referans oluşturulduktan sonra SelfRefin taşınmasını engellemenin bir yoluna ihtiyaç var” cümlesi, asıl noktadan çok “taşımayı tamamen engelleme sorunu”nu düşündürdü
    Asıl nokta, çok daha sonra gelen “Pin, değerin fiziksel olarak taşınmasını engellemez. Bunun yerine, değerin o pointer üzerinden taşınmayacağına dair tip düzeyinde bir garantidir” kısmında
    Taşımayı bizzat engellemek mümkün olmadığından, güvenli API'de kendine referanslı veriyi yalnızca özel referansın arkasında göstermek için Pin kullanılır. Belki ben Pini zaten fazlasıyla anlamış durumdayım; ama anlatım biraz düzeltilirse okur daha az bocalar

    • Yazıyı değiştirip böyle ifade etmeye çalışacağım
      Bu yazı pinning hakkındaki notlarımdan alındı ve başta ben de böyle anlamıştım. “Taşımayı engelleme” gibi bir sorunun tip düzeyinde garanti ile çözülebilmesi güzel gelmişti
      Elbette Pinin gerçekte yaptığı şey bu değil; dolayısıyla yazıyı bunu gösterecek şekilde düzeltmek doğru olur
  • Bu yazının bir yerinde !UnPinin yalnızca nightly Rust'ta ifade edilebildiğini belirtmek iyi olabilir. PhantomPinnedın var olmasının ana nedeni bu

  • “Pointer wrapper” deniyor, ama Rust'ta bile pointer'larla uğraşmanız neredeyse hiç gerekmiyor. Neden kullanmak gerektiğini bilmiyorum
    *const için Google'da Rust belgelerini bulmak zor; belgelendirilmiş mi merak ediyorum
    “Derleyicinin ürettiği durum makinesinin alanı olur” kısmını da bilmem mi gerekiyor? Yoksa saçma bir derleyici hatası aslında böyle bir şey olduğunu mu söylemeye çalışıyor?
    “Üretilen future kendine referanslı olur” da future kullandığınızda örtük olarak olan bir şey mi?
    Future::pollü doğrudan hiç kullanmadım sanırım
    “Güvenli kod normal &mut Tyi geri elde edemez” deyip “normal değişikliklere izin verir” deniyor; o zaman bu nasıl oluyor?
    Bu tür şeyler yüzünden Rust'ı daha derinlemesine kurcalamayı bıraktım

    • Ham pointer, Rust'ın primitive tiplerinden biridir. Belgeler burada ve burada
      Yine de düşük seviyeye inmediğiniz sürece kullanmanızın neredeyse hiç gerekmediği doğru. Ben de ancak bir C kütüphanesini çağırmaya çalışırken öğrendim
      Future::poll, Rust async kodunun temelidir. Doğrudan siz çağırmazsınız; executor çağırır. Rust'ta varsayılan executor yoktur, bu yüzden Tokio, smol, pollster gibi bir şey eklemek gerekir; bunlar da işi yapmak için Future trait'inde tanımlı poll gibi metotları kullanır
    • Orijinal yazının yazarı değilim ve bunlar tek nedenler de değil; ama Rust'ta pointer'larla uğraşmak zorunda kalmamın nedenleri FFI ve grafikler gibi kendine referanslı veri yapılarıydı
      Belgeler burası dahil olmak üzere birkaç yerde var
      Başkalarının yalnızca bizzat ihtiyaç duydukları şeyleri açıklamasını beklemek biraz fazla
      “Peki nasıl?” derken neyi sorduğunuzu tam anlayamadım