1 poin oleh GN⁺ 3 jam lalu | 1 komentar | Bagikan ke WhatsApp
  • std::pin::Pin adalah jaminan pada level tipe bahwa nilai yang ditunjuk pointer tidak akan dipindahkan melalui pointer tersebut, dan ini dibutuhkan untuk nilai yang alamatnya harus stabil seperti tipe yang mereferensikan dirinya sendiri
  • Dalam async/await, variabel lokal dan referensi yang tetap hidup melewati .await dapat menjadi field dalam state machine yang dibuat compiler, sehingga Future::poll membutuhkan Pin agar future tidak dipindahkan setelah dipoll
  • Pin mencegah pemindahan nilai dengan kode aman untuk nilai yang sudah dipin, tetapi tidak melarang perubahan biasa; jika T: Unpin tidak terpenuhi, &mut T tidak bisa diambil dengan aman dari Pin
  • Sebagian besar tipe Rust pada dasarnya adalah Unpin, jadi struct self-referential yang tidak boleh dipindahkan biasanya harus diberi field PhantomPinned agar menjadi !Unpin
  • Dalam praktiknya, saat memoll future secara langsung atau mengoperkannya ke API yang membutuhkan pinned future, digunakan Box::pin atau std::pin::pin!; jika mengimplementasikan Future sendiri atau primitif async tingkat rendah, Anda juga harus menangani invariant unsafe

Mengapa Pin dibutuhkan

  • std::pin::Pin adalah pembungkus pointer yang menyatakan jaminan bahwa nilai yang ditunjuk pointer tidak akan dipindahkan melalui pointer tersebut
  • Masalah intinya muncul pada tipe self-referential
    • Contoh struct SelfRef memiliki data: i32 dan ptr: *const i32, di mana ptr menunjuk ke self.data
    • Jika instance struct dipindahkan ke variabel lain atau dikembalikan dari fungsi, alamat memorinya bisa berubah
    • Pointer mentah ptr akan tetap menunjuk ke lokasi memori lama dan menjadi dangling pointer
  • Setelah referensi internal seperti ini disiapkan, dibutuhkan mekanisme untuk mencegah nilai tersebut dipindahkan lagi

Masalah yang muncul pada async/await dan Future

  • async/await dan Future adalah area paling umum tempat Pin muncul
  • Variabel lokal yang tetap hidup melewati titik .await akan menjadi field dari state machine yang dibuat compiler
  • Jika referensi ke suatu variabel lokal juga tetap hidup melewati .await yang sama, future yang dihasilkan bisa menjadi self-referential
  • Setelah polling dimulai, future bisa bergantung pada referensi yang menunjuk ke field lain di dalam dirinya sendiri
    • Jika future dipindahkan pada keadaan ini, referensi tersebut menjadi tidak valid
  • Untuk mencegah hal ini, Future::poll menerima Pin alih-alih &mut self
pub trait Future {
    type Output;
    fn poll(self: Pin, cx: &mut Context Pin {
      pub const fn get_mut(self) -> &'a mut T
      where
          T: Unpin
      { ... }
  }
  • Jika tipe tidak mengimplementasikan Unpin, yaitu !Unpin, maka &mut T tidak bisa diperoleh hanya dengan kode aman
  • Dalam kasus ini, Anda harus memakai metode unsafe seperti Pin::get_unchecked_mut, dan kode tersebut harus menjaga janji bahwa nilainya tidak akan dipindahkan keluar dari referensi itu

Unpin dan PhantomPinned

  • Tipe yang mengimplementasikan Unpin tidak bergantung pada pinning untuk keamanan memori
// std::marker
pub auto trait Unpin {}
  • Sebagian besar tipe di Rust aman untuk dipindahkan, sehingga secara default adalah Unpin
    • Contoh: i32, String, Vec
  • Unpin diimplementasikan otomatis untuk semua tipe kecuali jika secara eksplisit dibuat !Unpin
  • std::marker::PhantomPinned adalah marker struct yang secara eksplisit bersifat !Unpin
    • Karena auto trait menyebar otomatis, struct yang memiliki field PhantomPinned juga otomatis menjadi !Unpin
use std::marker::PhantomPinned;

struct SelfRef {
    data: i32,
    ptr: *const i32,
    _phantom: PhantomPinned, // makes the entire struct !Unpin
}
  • Ini adalah cara standar untuk menyatakan bahwa struct buatan pengguna tidak aman jika dipindahkan setelah dipin
  • Compiler biasanya tidak bisa mendeteksi otomatis self-reference yang dibuat melalui raw pointer unsafe
  • Karena itu, developer harus secara eksplisit melepaskan Unpin untuk struct self-referential
    • Biasanya dilakukan dengan menambahkan field PhantomPinned
  • Jika tipe self-referential dibiarkan tetap Unpin secara tidak sengaja, kode aman bisa mengambil referensi mutable dari Pin dan memindahkan nilainya
    • Hal itu akan merusak asumsi dari kode unsafe yang membuat self-reference tersebut

Cara membuat Pin

  • Pin sendiri tidak otomatis mem-pin suatu nilai

  • Membuat Pin berarti membuktikan bahwa pointee akan tetap berada di lokasi memori yang stabil selama masa hidup pin tersebut

  • Pin::new

    • Cara paling sederhana untuk membuatnya adalah Pin::new
    let mut value = 42;
    let pinned = Pin::new(&mut value);
    
    • Konstruktor ini hanya bisa digunakan saat T: Unpin
    • Tipe Unpin tidak bergantung pada pinning, jadi membungkusnya dengan Pin selalu aman
    • Dalam kasus ini, jaminan pinning pada dasarnya adalah no-op
  • std::pin::pin!

    • Saat perlu mem-pin nilai secara lokal tanpa alokasi heap, Anda bisa memakai macro pin!
    use std::pin::pin;
    
    let future = pin!(async {
        println!("Hello");
    });
    
    • Macro ini membuat variabel lokal lalu mengembalikan Pin yang menunjuk ke variabel tersebut
    • Compiler menjamin bahwa variabel lokal itu tidak akan dipindahkan selama sisa masa hidupnya, sehingga nilai !Unpin bisa dipin dengan aman di stack
    • Berbeda dari namanya, pin! tidak mem-pin memori stack itu sendiri
    • Ia hanya membuat referensi ter-pin yang terikat ke variabel lokal, dan saat variabel keluar dari scope, jaminan pinning juga berakhir
  • Box::pin

    • Untuk tipe !Unpin, konstruktor yang paling umum adalah Box::pin
    let pinned = Box::pin(SelfRef { ... });
    
    • pin! membuat Pin yang terikat ke variabel lokal, sedangkan Box::pin mengembalikan Pin yang dimiliki oleh Box
    • Alokasi heap itu sendiri tidak berpindah, sehingga pointee memiliki lokasi memori stabil selama masa hidup Box
    • Bahkan jika Box-nya dipindahkan, nilai yang dimilikinya tidak ikut berpindah; hanya pointer di dalam Box yang berpindah
    • Alokasi heap tetap berada di alamat yang sama
  • Pin::new_unchecked

    • Jika konstruktor aman tidak bisa membuktikan bahwa nilai akan tetap di tempatnya, Anda bisa membuat Pin secara langsung dengan kode unsafe
    let pinned = unsafe { Pin::new_unchecked(ptr) };
    
    • Pemanggil Pin::new_unchecked berjanji bahwa selama masa hidup Pin yang dikembalikan, pointee tidak akan dipindahkan lagi melalui pointer apa pun
    • Jika janji ini dilanggar, kode yang bergantung pada jaminan pinning dapat memicu undefined behavior
    • Karena itu, metode ini biasanya hanya dipakai saat mengimplementasikan abstraksi tingkat rendah yang memang bisa menjaga invariant tersebut

Kapan Anda benar-benar perlu memikirkannya

  • Bagi sebagian besar developer Rust, Pin dan Unpin bekerja diam-diam di belakang layar
  • Ada dua situasi utama ketika Anda perlu menanganinya secara langsung
    • Mengonsumsi kode async: jika harus memoll future secara langsung atau mengoperkannya ke API yang membutuhkan pinned future, pin future tersebut di heap dengan Box::pin(future) atau di stack lokal dengan std::pin::pin!(future)
    • Mengimplementasikan Future sendiri: saat menulis state machine kustom atau primitif async tingkat rendah, Anda harus menangani Pin, dan mungkin memerlukan PhantomPinned serta kode unsafe untuk menjaga invariant pinning
  • Pin adalah solusi Rust yang zero-cost untuk menangani masalah tipe yang sensitif terhadap alamat
  • Dengan ini, Rust bisa memakai async/await dan abstraksi self-referential lainnya sambil tetap menjaga jaminan keamanan memori tanpa garbage collector

1 komentar

 
GN⁺ 3 jam lalu
Komentar di Lobste.rs
  • std::pin::Pin terasa seperti Monad di dunia Rust. Begitu memahaminya, orang jadi tidak bisa tidak menulis posting blog

    • Tulisan-tulisan seperti itu biasanya mudah terjebak dalam monad tutorial fallacy
    • Apakah maksudnya, seperti halnya Monad, posting-posting blog semacam itu sebenarnya tidak berhasil menjelaskan apa pun dengan benar?
  • Sepertinya bagus kalau membahas beberapa hal yang dulu menjebak saya dan orang lain saat mencoba memahami Pin
    Nama Unpin kurang bagus. Nama yang lebih akurat, meski tetap kurang bagus, mungkin MovableWhenPinned atau PinIsNoOp
    Negasi ganda !Unpin di nightly tampak aneh, tetapi agar tipe yang sudah ada tetap menjadi kasus default 99%, perlu ditambahkan auto trait Unpin yang bisa di-opt-out oleh tipe, sehingga jadinya begitu. Kalau dibaca sebagai !MovableWhenPinned, itu lebih masuk akal
    Alternatif di versi stabil, PhantomPinned, juga namanya kurang bagus, karena keadaan pinned adalah keadaan sementara yang muncul karena ada referensi pinned, bukan sifat dari tipenya. Nama alternatifnya mungkin kira-kira PhantomNotMovableWhenPinned
    Setelah mulai menerjemahkan seperti ini di kepala, saya jauh lebih mudah memahaminya. Tentu saja masih membingungkan, mungkin saja saya hanya sedang beruntung

    • Sangat setuju. Dulu !Unpin membuat kepala saya pusing, tetapi setelah mulai membaca Unpin sebagai SafeToUnpin, rasanya sedikit lebih mudah
  • Saya pernah menanyakan ini sebelumnya dan rasanya seseorang memberi jawaban yang bijaksana, tapi saya tidak ingat. Pemahaman saya, Pin muncul dari async, dan masalahnya adalah referensi ke variabel lokal menjadi referensi-diri di dalam gumpalan data yang merepresentasikan state machine fungsi tertentu
    Jika state async itu dipindahkan, referensi-referensi ke variabel lokal tersebut akan menunjuk ke lokasi lama yang salah
    Namun bukankah itu hanya karena referensi adalah pointer nyata yang memiliki alamat absolut penuh? Saya penasaran mengapa solusinya bukan membuat referensi menjadi alamat relatif, melainkan menghilangkan kemampuan untuk dipindahkan
    Saya penasaran apakah jawabannya pada dasarnya adalah “karena jutaan engineer-year telah dicurahkan agar compiler, CPU, dan OS menangani pointer dengan sangat baik, pointer lebih unggul dalam banyak hal, jadi lebih baik memakai Pin di sana-sini”, atau apakah ada alasan keras mengapa referensi relatif benar-benar tidak layak sebagai alternatif

    • Ini bukan hanya masalah variabel lokal di dalam state async yang merujuk langsung ke variabel lokal lain di state yang sama. Kalau hanya itu, compiler mengetahui semua variabel lokal, jadi aksesnya bisa dibuat relatif. Namun jika referensi jauh di dalam suatu tipe menunjuk ke nilai yang jauh di dalam tipe lain, masalahnya jauh lebih rumit
      Jika referensi bersifat relatif, tipe-tipe tersebut harus memiliki representasi memori yang berbeda tergantung apakah dipakai di dalam state async atau tidak, dan juga diperlukan konsep pointer basis yang harus ikut dibawa untuk memulihkan pointer sebenarnya dari referensi relatif
      Objek-objek bersarang di dalam referensi pinned masih bisa dipindahkan dengan bebas meskipun objek root sudah pinned, jadi kita juga tidak bisa mengatakan bahwa semua referensi relatif hipotetis itu relatif terhadap pointer basis yang sama
      Pada akhirnya pointer absolut tetap diperlukan, dan referensi relatif tidak terlalu cocok. Kalau begitu, karena compiler Rust mengetahui tipe-tipe yang ada di sini, bagaimana kalau kita membuat objek tetap bisa dipindahkan dengan melacak seluruh graf objek dan memperbaiki referensi yang menunjuk ke objek yang dipindahkan agar menunjuk ke lokasi baru? Dengan begitu, pada dasarnya kita telah membuat tracing garbage collector
      Selain itu, compiler Rust tidak mengetahui semua tipe dalam graf objek. Referensi bisa diteruskan melalui FFI dan library eksternal bisa menyimpan referensi itu. Memperbaiki referensi yang dipindahkan melintasi batas FFI pada dasarnya adalah masalah yang sulit ditangani
      Jadi ini benar-benar rumit. Penting juga bahwa pemindahan objek itu sendiri adalah teknik yang relatif baru. Di kebanyakan program C/C++, semua objek bisa dianggap pinned secara implisit. Alasan pinning lebih jarang dibahas di sana adalah karena objek memang tidak dipindahkan, atau kalau dipindahkan, programmer bertanggung jawab memastikan tidak ada referensi menggantung yang tersisa
    • Pin juga diperlukan untuk interoperabilitas dengan bahasa lain yang tidak bisa memperlakukan memori sebagai gumpalan bit buram yang bisa dipindah seenaknya oleh Rust
      Sepemahaman saya, salah satu masalah interoperabilitas C++ adalah bahwa objek bukan sekadar gumpalan bit sederhana yang bisa dipindahkan dengan bebas, dan pada akhirnya cukup banyak tipe memerlukan pinning, yang membuat penggunaannya tidak nyaman
      Namun ini berdasarkan percakapan dengan orang-orang yang mengerjakan hal ini setidaknya sekitar 6 bulan lalu, jadi saya tidak tahu seberapa banyak situasinya sudah membaik sejak itu
  • Secara keseluruhan, saya melihat ini sebagai penjelasan yang enak dibaca sebagai tambahan untuk dokumentasi resmi Rust. Cara masuk ke masalahnya sedikit lebih halus
    Namun menurut saya memulai dengan struct referensi-diri justru lebih membingungkan daripada kalau dihilangkan. Khususnya kalimat di bagian pembuka, “karena itu, setelah referensi-diri semacam itu dibuat, diperlukan cara untuk mencegah SelfRef dipindahkan”, membuat saya terpikir pada “masalah mencegah pemindahan sepenuhnya” alih-alih inti persoalannya
    Inti sebenarnya ada jauh di belakang: “Pin tidak mencegah nilai dipindahkan secara fisik. Sebaliknya, ini adalah jaminan di tingkat tipe bahwa nilai tidak dipindahkan melalui pointer tersebut”
    Karena pemindahan itu sendiri tidak bisa dicegah, Pin dipakai agar data referensi-diri hanya diekspos di balik referensi eksklusif dalam API yang aman. Mungkin saya sudah terlalu banyak memahami Pin, tetapi kalau cara penjelasannya sedikit dipoles, pembaca sepertinya akan lebih sedikit tersesat

    • Saya akan mencoba mengubah tulisannya
      Artikel ini diambil dari catatan saya tentang pinning, dan awalnya saya juga memahaminya seperti itu. Saya merasa indah bahwa masalah seperti “mencegah pemindahan” bisa diselesaikan dengan jaminan di tingkat tipe
      Tentu saja itu bukan hal yang sebenarnya dilakukan Pin, jadi memang tepat untuk memperbaiki tulisan agar bagian itu terlihat
  • Layak dicatat di suatu tempat dalam artikel ini bahwa !UnPin hanya bisa diekspresikan di nightly Rust. Itulah alasan utama PhantomPinned ada

  • Disebut “wrapper pointer”, tetapi bahkan di Rust pun hampir tidak pernah perlu berurusan dengan pointer. Saya tidak tahu kenapa harus memakainya
    *const sulit ditemukan di dokumentasi Rust lewat Google; saya penasaran apakah itu terdokumentasi
    Apakah kita juga harus tahu bahwa “itu menjadi field dari state machine yang dibuat compiler”? Atau apakah error compiler yang absurd sebenarnya sedang mencoba mengatakan bahwa hal seperti itu terjadi?
    “Future yang dihasilkan menjadi referensi-diri” itu juga terjadi secara implisit kalau memakai future?
    Rasanya saya belum pernah memakai Future::poll secara langsung
    Katanya “kode aman tidak bisa memulihkan &mut T biasa”, tetapi juga “perubahan biasa tetap diizinkan”; jadi bagaimana caranya?
    Hal-hal seperti ini membuat saya berhenti menggali Rust lebih dalam

    • Pointer mentah adalah salah satu tipe primitif Rust. Dokumentasinya ada di sini dan di sini
      Meski begitu, benar juga bahwa Anda hampir tidak perlu memakainya kecuali turun ke level rendah. Saya sendiri baru mengetahuinya saat perlu memanggil library C
      Future::poll adalah dasar dari kode asinkron Rust. Anda tidak memanggilnya langsung; executor yang memanggilnya. Rust tidak memiliki executor bawaan, jadi Anda perlu menambahkan sesuatu seperti Tokio, smol, atau pollster, dan mereka memakai metode seperti poll yang didefinisikan pada trait Future untuk menjalankan pekerjaan
    • Saya bukan penulis artikel aslinya dan ini juga bukan satu-satunya alasan, tetapi alasan saya harus berurusan dengan pointer di Rust adalah FFI dan struktur data referensi-diri seperti graf
      Dokumentasinya ada di beberapa tempat, termasuk di sini
      Agak berlebihan kalau mengharapkan orang lain hanya menjelaskan persis hal yang pernah mereka butuhkan sendiri
      Saya kurang paham apa yang Anda tanyakan pada bagian “jadi bagaimana?”