Lewati ke konten

Unsafe Rust

Semua jaminan yang dibahas di kursus ini, seperti tidak ada referensi yang menggantung, tidak ada data race, dan tidak ada akses di luar batas, dipastikan oleh compiler. unsafe adalah caramu berkata ke compiler: “biarkan aku melakukan sesuatu yang tidak bisa kamu pastikan aman; aku yang bertanggung jawab.”

Apa yang dilakukan unsafe, dan apa yang tidak

Section titled “Apa yang dilakukan unsafe, dan apa yang tidak”

Banyak orang mengira unsafe mematikan seluruh pemeriksaan keamanan Rust. Tidak begitu. Di dalam blok unsafe, kamu bisa melakukan tepat lima hal tambahan yang biasanya dilarang:

  1. Melakukan dereference pada raw pointer
  2. Memanggil fungsi unsafe
  3. Membaca atau mengubah variabel static yang mutable
  4. Mengimplementasikan trait unsafe
  5. Mengakses field dari sebuah union

Semua hal lain, seperti aturan borrowing, semantik move, dan pemeriksaan tipe, tetap berlaku penuh di dalam kode unsafe. unsafe itu pisau bedah untuk lima operasi tertentu, bukan saklar untuk mematikan semuanya.

Referensi biasa di Rust (& dan &mut) selalu menunjuk ke data yang valid dan tersusun dengan benar di memori. Itu bagian dari jaminan compiler. Raw pointer (pointer mentah, *const T atau *mut T) melepaskan jaminan itu, dan karena itulah membacanya butuh unsafe:

fn main() {
let value = 5;
let pointer = &value as *const i32;
unsafe {
println!("{}", *pointer);
}
}

Output:

5
▶ Coba sendiri

Kamu boleh membuat raw pointer di kode yang aman. Yang butuh unsafe hanyalah dereference-nya (membaca nilai yang ditunjuknya), karena compiler tidak bisa lagi memastikan pointer itu valid.

Beberapa fungsi ditandai unsafe karena punya syarat yang tidak bisa diperiksa compiler untukmu. Memanggilnya berarti kamu berjanji sudah memeriksa syarat-syarat itu sendiri:

unsafe fn dangerous() {
println!("fungsi ini percaya kamu sudah memeriksa syarat-syaratnya");
}
fn main() {
unsafe {
dangerous();
}
}

Output:

fungsi ini percaya kamu sudah memeriksa syarat-syaratnya
▶ Coba sendiri

Membungkus unsafe di dalam fungsi yang aman

Section titled “Membungkus unsafe di dalam fungsi yang aman”

Standard library punya fungsi unsafe sungguhan yang bisa kamu panggil. Salah satunya adalah get_unchecked pada slice. Cara kerjanya seperti numbers[i], tapi tanpa pemeriksaan batas. Kalau i di luar jangkauan, ia tidak panic. Ia membaca apa pun yang kebetulan ada di memori itu, dan ini disebut undefined behavior (perilaku yang tidak terdefinisi): programmu bisa crash, mencetak sampah, atau tampak baik-baik saja sampai suatu saat tidak lagi.

Jadi aturan untuk memanggilnya adalah “hanya dengan indeks yang kamu tahu valid”. Pola yang biasa dipakai adalah memeriksa aturan itu sendiri, di dalam fungsi biasa yang aman:

fn first_or_zero(numbers: &[i32]) -> i32 {
if numbers.is_empty() {
return 0;
}
// SAFETY: slice-nya tidak kosong, jadi indeks 0 pasti ada.
unsafe { *numbers.get_unchecked(0) }
}
fn main() {
println!("{}", first_or_zero(&[7, 8, 9]));
println!("{}", first_or_zero(&[]));
}

Output:

7
0
▶ Coba sendiri

Lihat apa yang dilihat pemanggilnya: fn first_or_zero(numbers: &[i32]) -> i32. Tidak ada unsafe di signature-nya, dan tidak ada cara untuk memanggilnya dengan salah, karena fungsinya memeriksa aturan sebelum sampai ke blok unsafe. Bagian unsafe-nya tetap kecil dan tersembunyi di dalam.

Ada dua kebiasaan yang diikuti developer Rust berpengalaman:

  • Tulis komentar // SAFETY: di atas setiap blok unsafe, yang menjelaskan kenapa aturannya terpenuhi. Komentar itu untuk orang berikutnya yang membaca kode, dan untuk dirimu sendiri di masa depan.
  • Buat blok unsafe sekecil mungkin, supaya kode yang perlu diperiksa ulang lebih sedikit.

Jujur saja, kamu tidak akan menulis fungsi persis seperti ini di kode sungguhan: numbers.first() melakukan hal yang sama dengan aman, dan compiler sangat pandai membuang pemeriksaan batas yang terbukti tidak diperlukan. Tapi polanya (periksa aturannya, lalu panggil unsafe di dalam fungsi yang aman) persis seperti cara Vec, String dan kawan-kawannya dibangun. Halaman FFI memakai trik yang sama untuk membungkus fungsi C.

Kalau Rust begitu mementingkan keamanan, kenapa hal ini diizinkan? Karena ada hal-hal yang memang mustahil dibuktikan aman saat compile, padahal dalam praktiknya benar-benar aman: berkomunikasi langsung dengan hardware, memanggil library C (lihat FFI), atau membangun struktur data tertentu yang butuh performa tinggi. unsafe memungkinkan standard library (dan, sesekali, kodemu sendiri) mengimplementasikan bagian-bagian tingkat rendah ini, sementara semua yang dibangun di atasnya tetap berupa Rust biasa yang aman dan diperiksa penuh.

Menulis unsafe bukan berarti kodemu sekarang pasti penuh bug. Artinya, compiler tidak bisa lagi menangkap jenis-jenis kesalahan tertentu untukmu di blok itu, dan kamulah yang harus menjaga jaminan yang sama secara manual. Kebanyakan developer Rust berpengalaman sangat jarang menulis kode unsafe secara langsung. Biasanya kode itu berada di bagian-bagian kecil yang ditinjau dengan cermat di lapisan paling bawah sebuah library, dibungkus dengan API yang aman untuk dipakai semua orang.

Buat let mut score = 10;, buat raw pointer *mut i32 ke variabel itu, lalu pakai pointer-nya untuk menambah 5. Setelah itu cetak score. Petunjuk: buat pointer-nya dengan &mut score as *mut i32, dan tulis *pointer += 5 di dalam blok unsafe.

Lihat solusi
fn main() {
let mut score = 10;
let pointer = &mut score as *mut i32; // membuatnya aman
unsafe {
*pointer += 5; // menulis lewat pointer itu tidak aman
}
println!("score = {score}");
}

Output:

score = 15
▶ Coba sendiri

Dalam praktiknya ini aman, karena score masih hidup dan tidak ada yang lain memakainya selagi kita menulis. Menepati janji-janji itu adalah tugasmu begitu kamu memakai unsafe.

Tulis fn get_or(numbers: &[i32], index: usize, default: i32) -> i32. Fungsi ini mengembalikan elemen di posisi index, atau default kalau indeksnya di luar jangkauan. Pakai get_unchecked untuk membacanya, dan pastikan fungsinya tidak pernah bisa dipanggil dengan cara yang melanggar aturannya. Petunjuk: bandingkan index < numbers.len() dulu, dan hanya panggil get_unchecked kalau hasilnya true.

Lihat solusi
fn get_or(numbers: &[i32], index: usize, default: i32) -> i32 {
if index < numbers.len() {
// SAFETY: kita baru saja memeriksa bahwa index ada di dalam batas.
unsafe { *numbers.get_unchecked(index) }
} else {
default
}
}
fn main() {
let numbers = [10, 20, 30];
println!("{}", get_or(&numbers, 1, -1));
println!("{}", get_or(&numbers, 5, -1));
}

Output:

20
-1
▶ Coba sendiri

Indeks 5 tidak pernah sampai ke blok unsafe, jadi ditangani dengan aman. Sebagai perbandingan, versi yang sepenuhnya aman adalah numbers.get(index).copied().unwrap_or(default), dan biasanya itulah yang akan kamu tulis.

  • unsafe membuka tepat lima operasi (terutama raw pointer dan memanggil fungsi unsafe); semua hal lain tetap diperiksa penuh.
  • Raw pointer (*const T / *mut T) boleh dibuat di kode yang aman; dereference-nya butuh unsafe.
  • Bungkus unsafe di dalam fungsi kecil yang aman, yang memeriksa aturannya dulu, dan jelaskan kenapa ia benar di komentar // SAFETY:.
  • unsafe ada supaya kode tingkat rendah (akses hardware, interop dengan C) bisa dibangun, sementara kode yang memakainya tetap aman.
  • Sebagai pemula, kamu jarang perlu menulisnya sendiri. Cari abstraksi yang aman lebih dulu.

Kuis singkat

1. Apa yang sebenarnya dibuka oleh blok unsafe?

2. Bolehkah kamu membuat raw pointer di kode yang aman?

3. Sebagai pemula, seberapa sering kamu akan menulis unsafe sendiri?

4. Apakah menulis kode unsafe berarti programmu sekarang punya bug?

Skor: 0 / 4