Apa itu std::pin::Pin di Rust?
(vrong.me)std::pin::Pinadalah 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.awaitdapat menjadi field dalam state machine yang dibuat compiler, sehinggaFuture::pollmembutuhkanPinagar future tidak dipindahkan setelah dipoll Pinmencegah pemindahan nilai dengan kode aman untuk nilai yang sudah dipin, tetapi tidak melarang perubahan biasa; jikaT: Unpintidak terpenuhi,&mut Ttidak bisa diambil dengan aman dariPin- Sebagian besar tipe Rust pada dasarnya adalah Unpin, jadi struct self-referential yang tidak boleh dipindahkan biasanya harus diberi field
PhantomPinnedagar menjadi!Unpin - Dalam praktiknya, saat memoll future secara langsung atau mengoperkannya ke API yang membutuhkan pinned future, digunakan
Box::pinataustd::pin::pin!; jika mengimplementasikanFuturesendiri atau primitif async tingkat rendah, Anda juga harus menangani invariantunsafe
Mengapa Pin dibutuhkan
std::pin::Pinadalah 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
SelfRefmemilikidata: i32danptr: *const i32, di manaptrmenunjuk keself.data - Jika instance struct dipindahkan ke variabel lain atau dikembalikan dari fungsi, alamat memorinya bisa berubah
- Pointer mentah
ptrakan tetap menunjuk ke lokasi memori lama dan menjadi dangling pointer
- Contoh struct
- Setelah referensi internal seperti ini disiapkan, dibutuhkan mekanisme untuk mencegah nilai tersebut dipindahkan lagi
Masalah yang muncul pada async/await dan Future
async/awaitdan Future adalah area paling umum tempatPinmuncul- Variabel lokal yang tetap hidup melewati titik
.awaitakan menjadi field dari state machine yang dibuat compiler - Jika referensi ke suatu variabel lokal juga tetap hidup melewati
.awaityang 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::pollmenerimaPinalih-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 Ttidak 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
Unpintidak 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
- Contoh:
Unpindiimplementasikan otomatis untuk semua tipe kecuali jika secara eksplisit dibuat!Unpinstd::marker::PhantomPinnedadalah marker struct yang secara eksplisit bersifat!Unpin- Karena auto trait menyebar otomatis, struct yang memiliki field
PhantomPinnedjuga otomatis menjadi!Unpin
- Karena auto trait menyebar otomatis, struct yang memiliki field
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
Unpinuntuk struct self-referential- Biasanya dilakukan dengan menambahkan field
PhantomPinned
- Biasanya dilakukan dengan menambahkan field
- Jika tipe self-referential dibiarkan tetap
Unpinsecara tidak sengaja, kode aman bisa mengambil referensi mutable dariPindan memindahkan nilainya- Hal itu akan merusak asumsi dari kode
unsafeyang membuat self-reference tersebut
- Hal itu akan merusak asumsi dari kode
Cara membuat Pin
-
Pinsendiri tidak otomatis mem-pin suatu nilai -
Membuat
Pinberarti 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
Unpintidak bergantung pada pinning, jadi membungkusnya denganPinselalu aman - Dalam kasus ini, jaminan pinning pada dasarnya adalah no-op
- Cara paling sederhana untuk membuatnya adalah
-
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
Pinyang menunjuk ke variabel tersebut - Compiler menjamin bahwa variabel lokal itu tidak akan dipindahkan selama sisa masa hidupnya, sehingga nilai
!Unpinbisa 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
- Saat perlu mem-pin nilai secara lokal tanpa alokasi heap, Anda bisa memakai macro
-
Box::pin- Untuk tipe
!Unpin, konstruktor yang paling umum adalahBox::pin
let pinned = Box::pin(SelfRef { ... });pin!membuatPinyang terikat ke variabel lokal, sedangkanBox::pinmengembalikanPinyang dimiliki olehBox- 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 dalamBoxyang berpindah - Alokasi heap tetap berada di alamat yang sama
- Untuk tipe
-
Pin::new_unchecked- Jika konstruktor aman tidak bisa membuktikan bahwa nilai akan tetap di tempatnya, Anda bisa membuat
Pinsecara langsung dengan kode unsafe
let pinned = unsafe { Pin::new_unchecked(ptr) };- Pemanggil
Pin::new_uncheckedberjanji bahwa selama masa hidupPinyang 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
- Jika konstruktor aman tidak bisa membuktikan bahwa nilai akan tetap di tempatnya, Anda bisa membuat
Kapan Anda benar-benar perlu memikirkannya
- Bagi sebagian besar developer Rust,
PindanUnpinbekerja 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 denganstd::pin::pin!(future) - Mengimplementasikan
Futuresendiri: saat menulis state machine kustom atau primitif async tingkat rendah, Anda harus menanganiPin, dan mungkin memerlukanPhantomPinnedserta kode unsafe untuk menjaga invariant pinning
- Mengonsumsi kode async: jika harus memoll future secara langsung atau mengoperkannya ke API yang membutuhkan pinned future, pin future tersebut di heap dengan
Pinadalah solusi Rust yang zero-cost untuk menangani masalah tipe yang sensitif terhadap alamat- Dengan ini, Rust bisa memakai
async/awaitdan abstraksi self-referential lainnya sambil tetap menjaga jaminan keamanan memori tanpa garbage collector
1 komentar
Komentar di Lobste.rs
std::pin::Pinterasa seperti Monad di dunia Rust. Begitu memahaminya, orang jadi tidak bisa tidak menulis posting blogSepertinya bagus kalau membahas beberapa hal yang dulu menjebak saya dan orang lain saat mencoba memahami
PinNama
Unpinkurang bagus. Nama yang lebih akurat, meski tetap kurang bagus, mungkinMovableWhenPinnedatauPinIsNoOpNegasi ganda
!Unpindi nightly tampak aneh, tetapi agar tipe yang sudah ada tetap menjadi kasus default 99%, perlu ditambahkan auto traitUnpinyang bisa di-opt-out oleh tipe, sehingga jadinya begitu. Kalau dibaca sebagai!MovableWhenPinned, itu lebih masuk akalAlternatif 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-kiraPhantomNotMovableWhenPinnedSetelah mulai menerjemahkan seperti ini di kepala, saya jauh lebih mudah memahaminya. Tentu saja masih membingungkan, mungkin saja saya hanya sedang beruntung
!Unpinmembuat kepala saya pusing, tetapi setelah mulai membacaUnpinsebagaiSafeToUnpin, rasanya sedikit lebih mudahSaya pernah menanyakan ini sebelumnya dan rasanya seseorang memberi jawaban yang bijaksana, tapi saya tidak ingat. Pemahaman saya,
Pinmuncul dari async, dan masalahnya adalah referensi ke variabel lokal menjadi referensi-diri di dalam gumpalan data yang merepresentasikan state machine fungsi tertentuJika 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
Pindi sana-sini”, atau apakah ada alasan keras mengapa referensi relatif benar-benar tidak layak sebagai alternatifJika 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
Pinjuga diperlukan untuk interoperabilitas dengan bahasa lain yang tidak bisa memperlakukan memori sebagai gumpalan bit buram yang bisa dipindah seenaknya oleh RustSepemahaman 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
SelfRefdipindahkan”, membuat saya terpikir pada “masalah mencegah pemindahan sepenuhnya” alih-alih inti persoalannyaInti sebenarnya ada jauh di belakang: “
Pintidak 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,
Pindipakai agar data referensi-diri hanya diekspos di balik referensi eksklusif dalam API yang aman. Mungkin saya sudah terlalu banyak memahamiPin, tetapi kalau cara penjelasannya sedikit dipoles, pembaca sepertinya akan lebih sedikit tersesatArtikel 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 terlihatLayak dicatat di suatu tempat dalam artikel ini bahwa
!UnPinhanya bisa diekspresikan di nightly Rust. Itulah alasan utamaPhantomPinnedadaDisebut “wrapper pointer”, tetapi bahkan di Rust pun hampir tidak pernah perlu berurusan dengan pointer. Saya tidak tahu kenapa harus memakainya
*constsulit ditemukan di dokumentasi Rust lewat Google; saya penasaran apakah itu terdokumentasiApakah 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::pollsecara langsungKatanya “kode aman tidak bisa memulihkan
&mut Tbiasa”, tetapi juga “perubahan biasa tetap diizinkan”; jadi bagaimana caranya?Hal-hal seperti ini membuat saya berhenti menggali Rust lebih dalam
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::polladalah 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 sepertipollyang didefinisikan pada traitFutureuntuk menjalankan pekerjaanDokumentasinya 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?”