Sebagian besar nilai Rust dapat berpindah dengan bebas. Menetapkan nilai ke variabel lain, meneruskannya by value, mengembalikannya dari function, atau menggantinya di dalam container dapat membuat nilai tersebut berakhir di alamat memori yang berbeda.
Biasanya, memang itulah perilaku yang diinginkan. Model ownership Rust melacak siapa yang memiliki sebuah nilai, bukan di alamat mana nilai tersebut harus tetap berada.
Namun, ada kelompok type yang lebih kecil dengan kebutuhan berbeda. Beberapa nilai menjadi sensitif terhadap alamat: kode bergantung pada nilai tersebut tetap berada di lokasi memori yang sama. Future yang dihasilkan compiler dan struktur self-referential yang dirancang dengan hati-hati adalah contoh umum.
Pin ada agar invariant yang sensitif terhadap alamat ini dapat dinyatakan melalui API yang aman.
Mental model yang paling berguna adalah:
ownership biasa -> siapa yang boleh menggunakan atau menghancurkan nilai?
pinning -> bolehkah nilai berpindah ke alamat lain?Keduanya adalah pertanyaan yang berbeda. Pinning tidak mengubah ownership, dan ownership biasa tidak menjamin alamat yang stabil.
Mulai dari arti sebuah move
Di Rust, semantic move memindahkan ownership:
let first = String::from("report");
let second = first;Setelah assignment tersebut, second memiliki string dan first tidak dapat digunakan lagi.
Untuk nilai String biasa, tidak masalah apakah struct String itu sendiri berada di alamat yang sama sebelum dan sesudah move. Kontrak publiknya tidak bergantung pada alamat tersebut.
Hal yang sama berlaku untuk sebagian besar type Rust. Perilakunya tidak bergantung pada lokasi tempat object value kebetulan berada.
Pinning baru relevan ketika sebuah type memiliki invariant yang bergantung pada lokasinya.
Pin membungkus pointer, bukan nilai secara langsung
Type standard library-nya adalah:
Pin<Ptr>Ptr adalah type mirip pointer seperti &mut T atau Box<T>.
Detail pentingnya adalah Pin<Ptr> mem-pin pointee, bukan nilai pointer itu sendiri.
Contohnya:
use std::pin::Pin;
fn inspect(value: Pin<&mut String>) {
println!("{}", value.len());
}Pin<&mut String> menyatakan bahwa String yang direferensikan sedang diakses melalui pinning pointer.
Wrapper Pin sendiri tetap dapat berpindah seperti nilai Rust biasa. Memindahkan Pin<Box<T>>, misalnya, memindahkan handle Box, bukan alokasi heap yang berisi T.
Perbedaan ini sangat penting untuk memahami pinning.
Unpin berarti sebuah type tidak bergantung pada pinning
Sebagian besar type Rust mengimplementasikan auto trait Unpin.
Type yang mengimplementasikan Unpin menjanjikan bahwa kebenarannya tidak bergantung pada tetap berada di satu alamat.
Untuk T: Unpin, pinning tidak secara berarti membatasi mutable access biasa:
use std::pin::Pin;
let mut name = String::from("draft");
let mut pinned = Pin::new(&mut name);
pinned.as_mut().get_mut().push_str(".md");
assert_eq!(name, "draft.md");String mengimplementasikan Unpin, sehingga get_mut() dapat dengan aman mengembalikan &mut String biasa.
Setelah Anda memiliki &mut String, operasi seperti mem::replace diperbolehkan karena memindahkan nilai String tidak melanggar invariant apa pun yang bergantung pada pinning.
Dari sini muncul aturan penting:
Pinhanya benar-benar penting ketika pointee tidak mengimplementasikanUnpin, atau ketika API generic harus bekerja benar untuk type yang mungkin tidak mengimplementasikannya.
PhantomPinned membuat type tidak mendapat Unpin otomatis
Sebagian besar struct otomatis mengimplementasikan Unpin ketika field-field-nya juga mengimplementasikannya.
Type yang membutuhkan jaminan pinning dapat keluar dari perilaku otomatis tersebut dengan menyertakan PhantomPinned:
use std::marker::PhantomPinned;
struct AddressSensitive {
label: String,
_pin: PhantomPinned,
}PhantomPinned sendiri tidak mengimplementasikan Unpin, sehingga struct yang memuatnya tidak akan otomatis mengimplementasikan Unpin.
Hal ini mengubah apa yang boleh dilakukan safe code melalui Pin.
Misalnya sebuah function menerima:
use std::pin::Pin;
fn update(value: Pin<&mut AddressSensitive>) {
// ...
}Safe code tidak dapat begitu saja mengubahnya menjadi &mut AddressSensitive lalu memindahkan seluruh nilai keluar menggunakan mem::replace.
Pembatasan inilah yang memungkinkan unsafe internals mengandalkan nilai yang dipin tetap berada di alamat yang sama.
Pin tidak membuat sebuah nilai tidak bisa berpindah di mana pun
Kesalahpahaman umum adalah menganggap begitu sebuah type menjadi !Unpin, nilainya tidak pernah boleh berpindah.
Nilainya tetap bisa berpindah.
Sebuah nilai dapat berpindah dengan bebas sebelum dipin:
use std::marker::PhantomPinned;
struct AddressSensitive {
id: u64,
_pin: PhantomPinned,
}
let first = AddressSensitive {
id: 7,
_pin: PhantomPinned,
};
let second = first;Move ini valid karena belum ada jaminan pinning yang dibentuk.
Siklus yang relevan adalah:
buat nilai
-> move masih diperbolehkan
bentuk pinning
-> safe code harus mempertahankan lokasi yang dipin
drop nilaiPinning adalah state yang dibentuk melalui pinning pointer. Ia bukan properti permanen yang berlaku sejak konstruksi.
Pin nilai di stack dengan pin!
Untuk nilai lokal sementara, macro pin! sering menjadi pilihan aman paling sederhana:
use std::marker::PhantomPinned;
use std::pin::pin;
struct TaskState {
progress: u8,
_pin: PhantomPinned,
}
let state = TaskState {
progress: 0,
_pin: PhantomPinned,
};
let mut state = pin!(state);Hasilnya berperilaku seperti pinned mutable reference dengan lifetime yang terikat pada scope lokal.
Sebuah function dapat menerimanya tanpa membutuhkan alokasi heap:
use std::pin::Pin;
fn report(state: Pin<&TaskState>) {
println!("progress: {}", state.progress);
}
report(state.as_ref());Gunakan stack pinning ketika nilai yang dipin hanya perlu hidup di dalam scope saat ini.
Gunakan Pin<Box<T>> untuk penyimpanan pinned yang dimiliki
Ketika nilai yang dipin perlu dimiliki dan dikembalikan atau disimpan melampaui borrow lokal, alokasi heap umum digunakan:
use std::marker::PhantomPinned;
use std::pin::Pin;
struct TaskState {
progress: u8,
_pin: PhantomPinned,
}
fn new_state() -> Pin<Box<TaskState>> {
Box::pin(TaskState {
progress: 0,
_pin: PhantomPinned,
})
}Box::pin mengalokasikan nilai dan mengembalikan Pin<Box<T>>.
Anda tetap boleh memindahkan variabel Pin<Box<TaskState>>:
let first = new_state();
let second = first;Alokasi heap yang berisi TaskState tidak ikut berpindah hanya karena ownership handle box berpindah.
Inilah mengapa kalimat pointer boleh berpindah sementara pointee tetap dipin sangat berguna.
Pinned method membuat pembatasan menjadi eksplisit
Sebuah type dapat mewajibkan pinning untuk operasi tertentu dengan memakai pinned receiver:
use std::pin::Pin;
impl TaskState {
fn progress(self: Pin<&Self>) -> u8 {
self.progress
}
}Untuk operasi mutable, receiver dapat berupa:
impl TaskState {
fn set_progress(self: Pin<&mut Self>, progress: u8) {
// Access ke field harus mempertahankan invariant pinning.
// Contoh ini hanya memperlihatkan type receiver.
let _ = progress;
}
}Signature method memberi tahu caller bahwa operasi tersebut mengharapkan nilai sudah memasuki pinned state.
Signature itu juga memberi tahu implementor bahwa memindahkan bagian self yang sensitif terhadap alamat mungkin dilarang.
Field access adalah bagian pinning yang rumit
Sebuah struct yang dipin dapat memiliki field dengan kebutuhan pinning yang berbeda.
Misalnya sebuah type berisi:
struct Worker {
name: String,
// bayangkan ada field lain yang alamatnya harus tetap stabil
}Jika Worker dipin, tidak otomatis berarti setiap field harus diperlakukan sebagai pinned secara individual.
Menentukan apakah sebuah field mewarisi jaminan pinning dari parent disebut pin projection atau structural pinning.
Bagian ini rumit karena mengembalikan Pin<&mut Field> merupakan janji bahwa field tersebut sendiri tidak akan dipindahkan selama dipin. Sebuah type harus memastikan tidak ada safe method lain yang kemudian dapat melanggar janji itu.
Untuk application code biasa, hindari menulis unsafe pin projection sendiri kecuali Anda benar-benar memahami invariant yang terlibat. Library yang mendefinisikan type sensitif alamat biasanya menyediakan projection API yang aman.
Future menjelaskan mengapa Pin muncul di async Rust
Method poll pada trait Future menerima pinned mutable reference ke future:
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
fn poll_once<F>(
future: Pin<&mut F>,
cx: &mut Context<'_>,
) -> Poll<F::Output>
where
F: Future,
{
Future::poll(future, cx)
}Mengapa polling membutuhkan pinning?
Sebuah async block atau async fn dapat dikompilasi menjadi state machine yang pada state berikutnya mereferensikan data yang disimpan pada bagian lebih awal dari future yang sama. Setelah future seperti itu mulai bergantung pada lokasi internal, memindahkannya dapat merusak hubungan tersebut.
Karena itu trait Future menggunakan Pin<&mut Self> sehingga API yang sama dapat bekerja untuk:
- future yang aman dipindahkan dan mengimplementasikan
Unpin; - future yang membutuhkan lokasi stabil setelah polling dimulai.
Biasanya Anda tidak mengelola internals ini sendiri. Executor dan async combinator menyediakan boundary pinning yang dibutuhkan.
Konsekuensi praktis utamanya adalah API yang mensyaratkan F: Future + Unpin lebih restriktif daripada API yang menerima pinned future secara langsung.
Unpin berkaitan dengan type pointee, bukan type pointer
Pertimbangkan:
Pin<Box<T>>Apakah pinning membatasi access bergantung pada T.
Pertanyaan yang relevan adalah:
Apakah T mengimplementasikan Unpin?Bukan:
Apakah Box<T> mengimplementasikan Unpin?Perbedaan ini mencegah salah mental model yang umum.
Box<T> hanyalah owning pointer. Janji pinning berkaitan dengan nilai yang ditunjuknya.
Demikian juga, memindahkan variabel Pin<Box<T>> tidak memindahkan T.
Pin tidak mencegah interior mutation
Pinning berkaitan dengan lokasi, bukan immutability.
Sebuah nilai yang dipin tetap dapat mengubah state jika API-nya mengizinkan perubahan tersebut.
Contohnya, object sensitif alamat dapat memperbarui counter, flag, atau buffer yang dialokasikan di heap sambil tetap berada di alamat memori yang sama.
Jangan membaca:
pinnedsebagai:
frozenAturan sebenarnya lebih dekat ke:
nilai yang dipin tidak boleh dipindahkan dengan cara yang melanggar kontrak pinningMutation dan movement adalah operasi yang berbeda.
Pin tidak otomatis membuat unsafe code menjadi benar
Pin adalah kontrak library yang memungkinkan safe API mempertahankan invariant yang dibentuk oleh unsafe code.
Ia tidak memeriksa raw pointer atau membuktikan bahwa custom self-referential structure diinisialisasi dengan benar.
Jika unsafe code membuat internal pointer yang invalid sebelum pinning, Pin tidak memperbaikinya.
Demikian pula, unsafe method seperti Pin::new_unchecked membebankan tanggung jawab pada caller untuk mempertahankan kontrak pinning. Menggunakannya hanya untuk menghilangkan type error justru merusak safety model.
Utamakan safe constructor seperti Box::pin dan pin! kecuali Anda sedang mengimplementasikan abstraction yang memang membutuhkan unsafe pinning internals.
Kesalahan umum
Menambahkan Pin ke API biasa tanpa invariant yang sensitif terhadap alamat
Jika sebuah type aman dipindahkan, &mut T biasa lebih jelas.
Pin<&mut T> menambah kompleksitas konsep dan API. Gunakan hanya ketika type atau generic contract benar-benar membutuhkan stabilitas lokasi.
Menganggap !Unpin berarti sebuah nilai tidak pernah boleh berpindah
Nilai !Unpin boleh berpindah sebelum dipin. Pembatasan dimulai ketika jaminan pinning dibentuk.
Mengira Pin<Box<T>> mem-pin variabel Box
Handle box boleh berpindah. Pointee-nya yang tetap berada di lokasi yang dijanjikan.
Menganggap pinning sebagai immutability
Data yang dipin sering tetap dapat dimutasi. Pinning membatasi move yang dapat merusak invariant sensitif lokasi.
Terlalu cepat memakai new_unchecked
Unchecked pinning constructor adalah janji unsafe. Jika lifecycle dapat dinyatakan dengan safe constructor, gunakan yang aman.
Kapan menggunakan Pin secara langsung
Sebagian besar application code Rust tidak perlu mendefinisikan custom pinned type.
Anda biasanya akan berhadapan langsung dengan Pin ketika:
- mengimplementasikan low-level asynchronous abstraction;
- bekerja manual dengan
Future::poll; - merancang self-referential atau intrusive data structure;
- mengekspos safe API di atas unsafe internals yang sensitif alamat;
- mengintegrasikan library yang API-nya sudah mewajibkan pinned value.
Untuk struct biasa, collection, request object, configuration, dan business-domain model, ownership dan borrowing normal biasanya sudah cukup.
Pisahkan pertanyaan ownership dan lokasi
Ownership Rust menjawab siapa yang mengendalikan lifetime sebuah nilai. Pinning menjawab apakah nilai sensitif alamat boleh berpindah setelah jaminan pinning dibentuk.
Unpin berarti sebuah type tidak bergantung pada jaminan tersebut. PhantomPinned dapat membuat custom type keluar dari Unpin otomatis. pin! menyediakan local stack pinning, sedangkan Box::pin menyediakan owned heap-backed pinning.
Pelajaran praktisnya bukan mem-pin lebih banyak nilai. Yang penting adalah mengenali situasi sempit ketika lokasi memori menjadi bagian dari kontrak kebenaran sebuah type, lalu membuat kontrak itu eksplisit alih-alih mengandalkan kebiasaan.