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
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
Lobste.rs yorumları
std::pin::Pin, Rust dünyasının Monad'ı gibi. Bir kez anladığınızda blog yazısı yazmadan duramıyorsunuzPini anlamaya çalışırken benim ve başkalarının takıldığı birkaç noktayı ele almak iyi olabilirUnpinadı pek iyi değil. Daha doğru ama yine pek iyi olmayan adlarMovableWhenPinnedya daPinIsNoOpolabilirdinightly'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 traitUnpini eklemek gerektiğinden böyle oldu. Bunu!MovableWhenPinneddiye düşününce daha mantıklı geliyorKararlı 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 adPhantomNotMovableWhenPinnedgibi bir şey olabilirdiKafamda bu şekilde çevirmeye başlayınca çok daha iyi anladım. Elbette hâlâ kafa karıştırıcı; belki de şanslıydım
!Unpinkafamı ağrıtıyordu;UnpiniSafeToUnpinolarak 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
Pinasync'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 gelmesiydiAsync 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
Pinkullanmak daha iyidir” mi, yoksa göreli referansların alternatif olarak gerçekten mümkün olmamasına dair katı bir neden mi var, merak ediyorumReferanslar 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 gerekliBenim 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ındaTaşı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
Pinkullanılır. Belki benPini zaten fazlasıyla anlamış durumdayım; ama anlatım biraz düzeltilirse okur daha az bocalarBu 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 olurBu 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
*constiç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
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çinFuturetrait'inde tanımlıpollgibi metotları kullanırBelgeler 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