Chapter sebelumnya memetakan lima kemampuan yang dibuka unsafe, mulai dari dereferensi raw pointer hingga pemanggilan fungsi extern. Sekarang kita masuk ke pertanyaan praktis: di mana dan dengan bentuk apa kelima operasi itu boleh dieksekusi? Rust menyediakan dua kendaraan — blok unsafe yang menandai wilayah kecil di dalam fungsi biasa, dan fungsi unsafe yang menandai kontrak berisiko pada signature. Pilihan di antara keduanya menentukan siapa yang menanggung risiko: blok memusatkan tanggung jawab pada pemanggil di titik operasi, sedangkan fungsi unsafe mengumumkan risiko ke seluruh pengguna API. Chapter ini membedah kedua bentuk tersebut beserta aturan pembedahan kode yang menyertainya.
Blok unsafe: Pagar Terkecil #
Blok unsafe adalah pasangan kurung kurawal yang dibuka kata kunci unsafe. Di dalamnya, lima operasi berisiko menjadi legal; di luarnya, semuanya ditolak kompilator. Bentuk paling umum ditemui pada fungsi biasa yang bekerja dengan raw pointer:
fn reset_saldo(saldo: *mut i64) {
unsafe {
*saldo = 0;
}
}
fn main() {
let mut saldo_koperasi: i64 = 450000;
println!("Saldo sebelum reset: {}", saldo_koperasi);
reset_saldo(&mut saldo_koperasi);
println!("Saldo setelah reset: {}", saldo_koperasi);
}Output yang Dihasilkan:
Saldo sebelum reset: 450000
Saldo setelah reset: 0Mari kita bedah kode di atas baris per baris:
- Baris 1 (
fn reset_saldo(saldo: *mut i64) {): signature fungsi sepenuhnya aman — parameter bertipe raw pointer tidak menuntutunsafekarena sekadar memegang pointer bukanlah operasi berisiko. Konsekuensinya, siapa pun boleh memanggil fungsi ini tanpa pagar tambahan. - Baris 2 (
unsafe {): pagar dibuka hanya satu operasi sebelum penutup. Wilayah sekecil ini mudah diaudit manusia: satu baris, satu risiko, satu tanggung jawab. - Baris 3 (
*saldo = 0;): dereferensi*saldomenulis angka 0 ke alamat yang ditunjuk pointer. Kompilator tidak memeriksa apakahsaldovalid, selaras, atau terinisialisasi — fungsi ini percaya pemanggilnya menjaga ketiganya. - Baris 11 (
reset_saldo(&mut saldo_koperasi);): pemanggil membuat raw pointer*mut i64dari&mut saldo_koperasilewat konversi otomatis pada argumen. Karena pointer lahir dari variabel yang masih hidup, kontrak fungsi terpenuhi dan hasilnya terprediksi. - Baris 12 (
println!("Saldo setelah reset: {}", saldo_koperasi);): nilai asli terbaca 0 — bukti penulisan lewat pointer mengenai memori miliksaldo_koperasi.
Blok unsafe juga adalah ekspresi biasa yang mengembalikan nilai, sehingga pola let x = unsafe { ... }; sah dipakai untuk menangkap hasil operasi berisiko dalam satu baris. Kita akan sering memakai bentuk ringkas ini di Chapter Static Mutable.
Fungsi unsafe: Kontrak pada Signature #
Kadang risiko melekat pada fungsi itu sendiri, bukan pada satu titik di tubuhnya. Kata kunci unsafe di depan fn mengubah fungsi menjadi unsafe function: tubuhnya boleh berisi operasi unsafe, dan — ini yang terpenting — setiap pemanggil wajib membungkus pemanggilannya dengan blok unsafe. Mari kita lihat apa yang terjadi bila pemanggil lupa:
unsafe fn baca_sensor(alamat: *const u8) -> u8 {
*alamat
}
fn main() {
let alamat_sensor: *const u8 = std::ptr::null();
let nilai = baca_sensor(alamat_sensor);
println!("Nilai sensor: {}", nilai);
}Kompilasi gagal dengan pesan asli berikut:
error[E0133]: call to unsafe function `baca_sensor` is unsafe and requires unsafe function or block
--> main.rs:7:17
|
7 | let nilai = baca_sensor(alamat_sensor);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ call to unsafe function
|
= note: consult the function's documentation for information on how to avoid undefined behavior
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0133`.Error E0133 menyebutkan nama fungsinya secara eksplisit dan menuntut blok unsafe di sisi pemanggil. Pesan ini muncul justru karena tubuh baca_sensor belum dijalankan sama sekali — penolakan terjadi di gate kompilasi, bukan saat runtime. Catatan “consult the function’s documentation” juga bukan basa-basi: fungsi unsafe yang baik selalu mendokumentasikan kontraknya, seperti yang akan kita lakukan sebentar lagi.
Perhatikan satu ketidakadilan yang disengaja di contoh di atas: alamat_sensor bernilai null. Bila pemanggilan ini lolos (misalnya kita membungkusnya dengan unsafe begitu saja), dereferensi alamat null memicu undefined behavior. Keyword unsafe tidak menyelamatkan kita dari pointer buruk — keyword hanya memindahkan kewajiban memeriksanya.
Tubuh Fungsi unsafe adalah Konteks unsafe #
Pada edisi 2021 yang dipakai Course ini, tubuh sebuah fungsi unsafe otomatis menjadi konteks unsafe. Operasi berisiko di dalamnya boleh ditulis langsung tanpa blok bersarang:
unsafe fn tulis_byte(buffer: *mut u8, nilai: u8) {
*buffer = nilai;
}
fn main() {
let mut sel: u8 = 0;
unsafe {
tulis_byte(&mut sel, 0x41);
}
println!("Sel berisi: 0x{:02X}", sel);
}Output yang Dihasilkan:
Sel berisi: 0x41Mari kita bedah kode di atas baris per baris:
- Baris 1 (
unsafe fn tulis_byte(buffer: *mut u8, nilai: u8) {): penandaunsafepada fungsi mengumumkan kontrak berisiko ke seluruh calon pemanggil. - Baris 2 (
*buffer = nilai;): dereferensi berjalan tanpa blok tambahan karena tubuh fungsiunsafesudah menjadi konteksunsafedi edisi 2021. - Baris 7 (
unsafe {): pemanggil tetap wajib membuka blokunsafe— penanda pada definisi tidak meloloskan pemanggilan. - Baris 8 (
tulis_byte(&mut sel, 0x41);): raw pointer*mut u8lahir dari&mut sel, buffer valid dan terinisialisasi, sehingga kontrak fungsi terpenuhi danselberisi0x41.
Dokumentasikan Kontrak dengan # Safety #
Fungsi unsafe tanpa dokumentasi adalah bom waktu: pemanggil tidak tahu invariant apa yang harus dijaga. Konvensi komunitas Rust menuntut bagian # Safety pada doc comment yang menjelaskan syarat pemanggilan aman. Bentuk lengkapnya terlihat seperti ini:
/// Membaca byte pada `indeks` dari buffer mentah.
///
/// # Safety
///
/// Pemanggil harus memastikan `buffer` menunjuk ke minimal `indeks + 1`
/// byte yang valid dan sudah terinisialisasi.
unsafe fn baca_byte(buffer: *const u8, indeks: usize) -> u8 {
*buffer.add(indeks)
}
fn baca_byte_aman(buffer: &[u8], indeks: usize) -> Option<u8> {
if indeks < buffer.len() {
Some(unsafe { baca_byte(buffer.as_ptr(), indeks) })
} else {
None
}
}
fn main() {
let data_paket = [0x11, 0x52, 0x7A, 0x90];
match baca_byte_aman(&data_paket, 2) {
Some(nilai) => println!("Byte ke-2 paket: 0x{:02X}", nilai),
None => println!("Indeks di luar jangkauan"),
}
match baca_byte_aman(&data_paket, 9) {
Some(nilai) => println!("Byte ke-9 paket: 0x{:02X}", nilai),
None => println!("Indeks 9 di luar jangkauan"),
}
}Output yang Dihasilkan:
Byte ke-2 paket: 0x7A
Indeks 9 di luar jangkauanMari kita bedah kode di atas baris per baris:
- Baris 7 (
unsafe fn baca_byte(buffer: *const u8, indeks: usize) -> u8 {): fungsi berisiko diberi nama, parameter, dan kontrak yang jelas — bukan blok gelap yang tersebar di kode. - Baris 8 (
*buffer.add(indeks)):buffer.add(indeks)menggeser pointer sejauhindekselemen, lalu dereferensi membaca byte pada alamat hasil. Kompilator tidak membuktikan bahwa offset masih berada di dalam buffer; kontrak# Safetydi atas fungsi membebankannya ke pemanggil. - Baris 11 (
fn baca_byte_aman(buffer: &[u8], indeks: usize) -> Option<u8> {): fungsi pembungkus bertanda aman total — signature-nya hanya memuat tipe aman (&[u8],usize,Option<u8>), sehingga pemanggil tidak butuhunsafesama sekali. - Baris 12 (
if indeks < buffer.len() {): invariant “offset berada dalam jangkauan” dibuktikan secara aman lewat pemeriksaan panjang slice — inilah pemindahan tanggung jawab dari konvensi ke kode. - Baris 13 (
Some(unsafe { baca_byte(buffer.as_ptr(), indeks) })): blokunsafeterkecil mungkin — satu pemanggilan, dibuka dan ditutup dalam satu ekspresi. Setelah baris ini, seluruh sisa program kembali aman penuh. - Baris 22–25 (
match baca_byte_aman(&data_paket, 2) { ... }): indeks 2 sah sehinggaSome(0x7A)dicetak; hasil pembacaan sesuai elemen ketiga array. - Baris 27–30 (
match baca_byte_aman(&data_paket, 9) { ... }): indeks 9 ditolak sebelum pointer digeser sedikit pun — jalurNonemencegah pemanggilan fungsiunsafealih-alih membiarkannya gagal saat berjalan.
Pola baca_byte_aman adalah embrio dari safe abstraction: risiko dikurung di satu titik, bukti keamanan dinyatakan dalam kode aman, dan pemanggil tinggal menikmati API yang mustahil disalahgunakan. Chapter Safe Abstraction akan membangun versi jauh lebih serius dari pola ini.
Jebakan Blok yang Tidak Berisi Apa-apa #
Kompilator juga mengawasi arah sebaliknya: blok unsafe yang tidak memuat satu pun operasi berisiko memicu warning. Program berikut menunjukkan kasusnya:
fn main() {
unsafe {
println!("Halo dari blok unsafe");
}
}Kompilasi berhasil, tetapi warning berikut muncul:
warning: unnecessary `unsafe` block
--> main.rs:2:5
|
2 | unsafe {
| ^^^^^^ unnecessary `unsafe` block
|
= note: `#[warn(unused_unsafe)]` (part of `#[warn(unused)]`) on by default
warning: 1 warning emittedWarning unnecessary unsafe block berguna sebagai alarm kebersihan: pagar yang melindungi apa-apa hanya menambah kebisingan dan menormalisasi rasa takut pada kompilator. Blok unsafe yang benar adalah yang hilang dengan sendirinya saat operasi berisikonya dihapus.
Ringkasan #
Blok unsafe memberi pagar lokal untuk lima operasi berisiko dan sebaiknya dibuat sekecil mungkin; fungsi unsafe menandai kontrak pada signature sehingga pemanggil wajib membungkus pemanggilan dengan blok unsafe di bawah ancaman E0133. Tubuh fungsi unsafe menjadi konteks unsafe otomatis pada edisi 2021, sementara edisi 2024 menuntut blok eksplisit melalui lint unsafe_op_in_unsafe_fn. Doc comment dengan bagian # Safety adalah tempat invariant dituliskan agar pemanggil tahu syarat aman, dan warning unnecessary unsafe block menjaga pagar tetap bermakna. Dengan dua kendaraan ini di tangan, kita siap mengenal alat paling klasik di dunia unsafe: raw pointer.