Dua chapter terakhir menunjukkan betapa mahalnya tanggung jawab manual: setiap dereferensi raw pointer menuntut pembuktian lima invariant, dan setiap akses static mut menuntut keyakinan bahwa tidak ada thread lain. Bayangkan jika beban itu dibebankan ke setiap pemanggil — satu lupa pemeriksaan di satu titik, seluruh program rusak. Di titik inilah safe abstraction masuk: kode unsafe dikurung di lapisan dalam yang sempit, sementara API yang menghadap pemanggil tetap memakai tipe-tipe aman sehingga kompilator dapat membuktikan seluruh sisa program. Strategi ini adalah cara Vec, Mutex, dan String dibangun — standar library yang penuh unsafe di dalam, tetapi aman total di luar. Chapter ini membangun safe abstraction klasik dari buku Rust: split_at_mut, fungsi yang membelah satu slice menjadi dua potongan mutable sekaligus.
Masalah: Dua Potongan Mutable dari Satu Slice #
Mulailah dari kebutuhan yang terasa wajar: memproses paruh depan dan belakang sebuah array secara terpisah, misalnya mengurutkan keduanya secara paralel. Kedua potongan jelas tidak tumpang tindih — paruh depan menguasai elemen 0 sampai 2, paruh belakang elemen 3 sampai 5. Namun kode yang paling naif ditolak mentah-mentah:
fn main() {
let mut data = [100, 200, 300, 400, 500, 600];
let awal = &mut data[..3];
let akhir = &mut data[3..];
println!("Awal: {:?}, Akhir: {:?}", awal, akhir);
}Kompilasi gagal dengan pesan asli berikut:
error[E0499]: cannot borrow `data` as mutable more than once at a time
--> main.rs:5:22
|
4 | let awal = &mut data[..3];
| ---- first mutable borrow occurs here
5 | let akhir = &mut data[3..];
| ^^^^ second mutable borrow occurs here
6 |
7 | println!("Awal: {:?}, Akhir: {:?}", awal, akhir);
| ---- first borrow later used here
|
= help: use `.split_at_mut(position)` to obtain two mutable non-overlapping sub-slices
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0499`.Error E0499 ini sudah lama kita kenal, tetapi perhatikan bantuan di baris help: kompilator sendiri menunjuk solusi split_at_mut — method standar library yang mengembalikan dua &mut [T] dari satu slice. Pertanyaannya: bagaimana mungkin method itu lolos aturan yang sama yang baru saja menolak kita? Jawabannya: split_at_mut diimplementasikan dengan unsafe, lalu dibungkus API yang menanggung seluruh pembuktian sendiri. Bantuan compiler tersebut adalah pengakuan resmi bahwa beberapa pola aman tidak bisa diungkapkan dalam tata bahasa borrowing semata.
Strategi Safe Abstraction #
Bahan bakunya dapat dirumuskan tiga lapis. Pertama, signature harus aman: parameter dan nilai kembalian hanya memuat tipe yang sudah terjamin (&mut [i32], usize, tuple dua slice), tanpa raw pointer yang bocor keluar. Kedua, seluruh invariant diverifikasi sebelum menyentuh unsafe — pemeriksaan seperti batas indeks dilakukan dengan kode aman biasa sehingga kesalahannya berupa panic yang bersih, bukan undefined behavior. Ketiga, blok unsafe dibuat minimal: hanya baris-baris yang benar-benar membutuhkan kelima kemampuan khusus. Sebuah abstraction seperti ini disebut sound bila pemanggil yang patuh aturan safe Rust tidak mungkin memicu undefined behavior — kriteria yang sama dengan standar library.
Membangun split_at_mut Versi Kita #
Implementasinya memanfaatkan fakta bahwa potongan slice pada dasarnya adalah pasangan (raw pointer, panjang). Dari satu pointer dasar, kita bangun dua potongan dengan aritmetika .add():
use std::slice;
fn split_at_mut(nilai: &mut [i32], tengah: usize) -> (&mut [i32], &mut [i32]) {
let panjang = nilai.len();
let ptr = nilai.as_mut_ptr();
assert!(tengah <= panjang, "tengah tidak boleh melebihi panjang");
unsafe {
(
slice::from_raw_parts_mut(ptr, tengah),
slice::from_raw_parts_mut(ptr.add(tengah), panjang - tengah),
)
}
}
fn main() {
let mut data = [100, 200, 300, 400, 500, 600];
let (awal, akhir) = split_at_mut(&mut data, 3);
println!("Potongan awal: {:?}", awal);
println!("Potongan akhir: {:?}", akhir);
awal[1] = 250;
akhir[0] = 450;
println!("Data setelah ubah: {:?}", data);
}Output yang Dihasilkan:
Potongan awal: [100, 200, 300]
Potongan akhir: [400, 500, 600]
Data setelah ubah: [100, 250, 300, 450, 500, 600]Mari kita bedah kode di atas baris per baris:
- Baris 3 (
fn split_at_mut(nilai: &mut [i32], tengah: usize) -> (&mut [i32], &mut [i32]) {): signature memakai tipe aman penuh. Pemanggil tidak bisa salah pakai dari sisi tipe — inilah wajah publik abstraction, dan tidak ada satu pun raw pointer yang terlihat darinya. - Baris 4 (
let panjang = nilai.len();): panjang slice disimpan sebelum pointer diambil. Semua perhitungan ukuran berikutnya memakai angka ini, bukannilai.len()lagi — setelahas_mut_ptr(), lebih jelas meminimalisasi pemakaian borrownilai. - Baris 5 (
let ptr = nilai.as_mut_ptr();): pointer*mut i32penunjuk elemen pertama diperoleh dari method aman. Di titik ini kita tahu dua fakta teruji: alamat valid (borrow&mutsedang hidup) dan memori terinisialisasi (elemen slice selalu sah). - Baris 7 (
assert!(tengah <= panjang, "tengah tidak boleh melebihi panjang");): invariant pertama dibuktikan sebelum masukunsafe. Pemanggil yang mengirimtengahlebih besar dari panjang memperoleh panic dengan pesan jelas — bukan pointer yang menunjuk melewati alokasi. - Baris 9 (
unsafe {): pagar terbuka hanya untuk empat baris berikutnya. Seluruh risiko program ini terkumpul di sini — wilayah audit yang bisa dibaca dalam sekali pandang. - Baris 11 (
slice::from_raw_parts_mut(ptr, tengah),): fungsi pustaka membangun&mut [i32]berpanjangtengahdari pointer dasar. Kontraknya menuntut pointer valid, selaras, dan menunjuk minimaltengahelemen terinisialisasi — semua terpenuhi karenaptrlahir dari borrow hidup dantengah <= panjangsudah dibuktikan. - Baris 12 (
slice::from_raw_parts_mut(ptr.add(tengah), panjang - tengah),): potongan kedua mulai dari alamat dasar digesertengahelemen. Karenatengah <= panjang, hasil geseran paling jauh adalah posisi satu-elemen-di-belakang-ujung — masih dalam alokasi yang sama, dan potongan kedua berpanjangpanjang - tengahelemen sehingga tepat berakhir di ujung array. Kedua potongan berbagi alokasi tetapi tidak pernah berbagi elemen. - Baris 20 (
let (awal, akhir) = split_at_mut(&mut data, 3);): pemanggilan dimaintidak memakaiunsafesama sekali — pemanggil hanya melihat fungsi biasa yang menerima&mut [i32]danusize. - Baris 25–26 (
awal[1] = 250; akhir[0] = 450;): kedua potongan mutable dipakai serentak — skenario yang di dunia reference biasa mustahil. Dua tulisan mengenai elemen yang berbeda (data[1]dandata[3]), dan baris 28 membuktikannya: array berubah tepat di dua posisi itu.
Relasi kedua potongan terhadap alokasi aslinya dapat digambarkan sebagai berikut:
Kedua panah mengarah ke blok yang berbeda di dalam alokasi yang sama — itulah inti invariant yang tidak bisa dinyatakan borrow checker: “dua pinjaman mutable, wilayah elemen berbeda”. Fungsi std split_at_mut versi asli memakai teknik yang sama secara umum untuk tipe T berukuran berapa pun.
Menguji Soundness, Bukan Sekadar Jalan #
Program berjalan benar bukan bukti abstraction sound. Uji minimal yang layak: pastikan semua jalur batas berperilaku sehat — pemanggilan dengan tengah 0, tengah sama dengan panjang, dan tengah melebihi panjang. Dua kasus pertama harus menghasilkan potongan kosong yang sah di salah satu sisi; kasus ketiga harus panic di assert! sebelum pointer tersentuh. Test di Section Testing menjadi tempat alami untuk ketiganya, dan untuk pemeriksaan lebih dalam terdapat alat bantu cargo miri yang menafsirkan program dan melaporkan pelanggaran kontrak unsafe (akses di luar batas, aliasing ilegal) yang tidak terlihat mata. Prinsipnya satu: soundness adalah sifat yang dibuktikan lewat reasoning pada kode abstraction, lalu dikonfirmasi lewat uji — bukan sifat yang tercapai karena kebetulan berhasil dijalankan sekali.
Ringkasan #
Safe abstraction mengurung unsafe di lapisan sempit di balik signature aman: split_at_mut menerima &mut [i32] dan usize, memverifikasi tengah <= panjang dengan assert! di dunia aman, lalu membangun dua slice lewat as_mut_ptr(), .add(), dan from_raw_parts_mut di dalam blok unsafe bermata empat baris. Invariant kuncinya — pointer valid dan terinisialisasi, geseran tetap dalam alokasi, dan kedua potongan tidak berbagi elemen — dibuktikan satu per satu sebelum pagar dibuka. Error E0499 pada percobaan naif plus bantuan kompilator yang menunjuk split_at_mut menegaskan pola ini sebagai jalan resmi untuk aliasing terkontrol. Teknik yang sama juga menjadi tulang punggung integrasi lintas bahasa — pembahasan terakhir section ini: FFI dengan extern.