Reference yang kita pakai sepanjang Course adalah alamat memori dengan jaminan: tidak null, tidak menggantung, aliasing diatur borrow checker, dan lifetime tercatat di tipe. Jaminan-jaminan itulah yang membuat kompilator berani meloloskan dereferensi tanpa pemeriksaan runtime. Raw pointer adalah alamat memori tanpa jaminan tersebut: *const T untuk penunjuk baca-saja dan *mut T untuk penunjuk baca-tulis. Dengan melepas jaminan, raw pointer membuka kemampuan yang mustahil dilakukan reference — aliasing bebas, umur tak terbatas, penunjukan alamat arbitrer — sekaligus menyerahkan seluruh pembuktian keamanan kepada kita. Chapter ini membedah sifat-sifat *const T dan *mut T, cara membuatnya dengan aman, serta batas-batas yang tetap diberlakukan kompilator meski jaminan sudah hilang.

Dua Bentuk Raw Pointer #

Rust menyediakan dua tipe raw pointer yang menjadi pasangan reference: *const T menunjuk nilai yang hanya boleh dibaca, sedangkan *mut T menunjuk nilai yang boleh dibaca dan ditulis. Keduanya berukuran satu alamat — sama seperti &T dan &mut T — tetapi tidak membawa informasi apa pun tentang umur data yang ditunjuk. Perbedaan mendasar dengan reference dirangkum pada tabel berikut:

AspekReference &T / &mut TRaw pointer *const T / *mut T
Bisa nullTidakYa, lewat std::ptr::null()
Bisa menggantungTidak, lifetime dijaga kompilatorYa, tidak ada lifetime pada tipe
Aliasing dua penunjuk satu dataDitolak borrow checkerDiperbolehkan
Bisa dilewat antar threadYa, bila T: Sync/T: SendTidak, !Send dan !Sync
DereferensiAman di mana punHanya di dalam blok unsafe

Baris terakhir tabel adalah kunci: memegang raw pointer tidak berbahaya dan tidak butuh unsafe — bahaya baru muncul saat nilai di baliknya diakses, dan itulah pembahasan Chapter Dereferensi Raw Pointer.

Membuat Raw Pointer dengan Aman #

Cara paling sehat memperoleh raw pointer adalah mengonversinya dari reference yang sudah terbukti valid. Konversi ini legal di kode aman karena tidak ada yang diakses — kita hanya mencatat alamatnya:

rust
fn main() {
    let mut harga = 15000;

    // Membuat raw pointer: aman, tanpa blok unsafe:
    let ptr_baca = &harga as *const i32;
    let ptr_tulis = &mut harga as *mut i32;

    // Aliasing dua pointer ke satu data diperbolehkan:
    println!("ptr_baca == ptr_tulis: {}", ptr_baca == ptr_tulis);

    // Pointer bisa disalin seperti nilai biasa (Copy):
    let salinan_ptr = ptr_baca;
    println!("salinan_ptr == ptr_baca: {}", salinan_ptr == ptr_baca);

    let ptr_kosong: *const i32 = std::ptr::null();
    println!("ptr_kosong.is_null(): {}", ptr_kosong.is_null());
}

Output yang Dihasilkan:

text
ptr_baca == ptr_tulis: true
salinan_ptr == ptr_baca: true
ptr_kosong.is_null(): true

Mari kita bedah kode di atas baris per baris:

  1. Baris 2 (let mut harga = 15000;): satu variabel i32 di stack menjadi sasaran ketiga pointer sekaligus — skenario yang akan ditolak mentah-mentah bila kita memakai dua &mut.
  2. Baris 5 (let ptr_baca = &harga as *const i32;): cast &harga as *const i32 membaca alamat harga lalu menyimpannya sebagai raw pointer baca-saja. Borrow baca berakhir di baris yang sama; setelahnya harga bebas dipinjam ulang.
  3. Baris 6 (let ptr_tulis = &mut harga as *mut i32;): raw pointer baca-tulis dibuat dari pinjaman mutable. Alih-alih error “cannot borrow as mutable more than once”, kompilator meloloskan kedua pointer — raw pointer tidak ikut aturan aliasing reference.
  4. Baris 9 (println!("ptr_baca == ptr_tulis: {}", ptr_baca == ptr_tulis);): operator == pada raw pointer membandingkan alamat, dan hasilnya true karena keduanya menunjuk lokasi yang sama. Perbandingan alamat adalah salah satu dari sedikit operasi aman pada raw pointer.
  5. Baris 12 (let salinan_ptr = ptr_baca;): raw pointer mengimplementasikan Copy, sehingga menyalinnya tidak memindahkan kepemilikan apa pun. Kini ada tiga penunjuk menuju satu i32.
  6. Baris 15 (let ptr_kosong: *const i32 = std::ptr::null();): fungsi std::ptr::null() menghasilkan pointer alamat nol. Pointer null bisa dibuat dan diperiksa dengan aman lewat is_null() — yang berbahaya hanyalah mencoba dereferensinya.

Sumber raw pointer lain yang sama amannya adalah method as_ptr() dan as_mut_ptr() pada slice, array, dan Vec — keduanya akan menjadi aktor utama di Chapter Safe Abstraction.

Batas yang Tetap Diberlakukan Kompilator #

Kehilangan jaminan tidak berarti raw pointer menjadi liar sepenuhnya. Dua penolakan berikut membuktikan bahwa kompilator tetap menjaga pagar di sekitar operasi yang tidak bisa dibuktikan aman. Pertama, aritmetika pointer dengan operator + tidak diizinkan:

rust
fn main() {
    let data = [10, 20, 30];
    let ptr = data.as_ptr();
    let ptr_berikutnya = ptr + 1;
    println!("{}", ptr == ptr_berikutnya);
}

Kompilasi gagal dengan pesan asli berikut:

text
error[E0369]: cannot add `{integer}` to `*const {integer}`
 --> main.rs:4:30
  |
4 |     let ptr_berikutnya = ptr + 1;
  |                          --- ^ - {integer}
  |                          |
  |                          *const {integer}
  |
help: consider using `wrapping_add` or `add` for pointer + {integer}
  |
4 -     let ptr_berikutnya = ptr + 1;
4 +     let ptr_berikutnya = ptr.wrapping_add(1);
  |

error: aborting due to 1 previous error

For more information about this error, try `rustc --explain E0369`.

Error E0369 menegaskan bahwa penggeseran alamat hanya boleh lewat method khusus seperti .add() dan .wrapping_add() — method yang kontraknya kita bedah di Chapter Dereferensi Raw Pointer. Alasannya bukan sekadar gaya penulisan: operator + membawa janji overflow checking biasa, sementara aritmetika pointer punya aturan in-bounds tersendiri yang lebih ketat.

Kedua, raw pointer tidak boleh menyeberang batas thread. Program berikut mencoba mengirim pointer hasil cast ke dalam thread::spawn:

rust
use std::thread;

fn main() {
    let nilai: i32 = 40;
    let ptr = &nilai as *const i32;

    let pekerja = thread::spawn(move || {
        println!("Pointer tiba di thread: {}", !ptr.is_null());
    });

    pekerja.join().unwrap();
}

Kompilasi gagal dengan pesan asli berikut:

text
error[E0277]: `*const i32` cannot be sent between threads safely
 --> main.rs:7:33
  |
7 |       let pekerja = thread::spawn(move || {
  |                     ------------- ^------
  |                     |             |
  |  ___________________|_____________within this `{[email protected]:7:33: 7:40}`
  | |                   |
  | |                   required by a bound introduced by this call
8 | |         println!("Pointer tiba di thread: {}", !ptr.is_null());
9 | |     });
  | |_____^ `*const i32` cannot be sent between threads safely
  |
  = help: within `{[email protected]:7:33: 7:40}`, the trait `Send` is not implemented for `*const i32`
note: required because it's used within this closure
 --> main.rs:7:33
  |
7 |     let pekerja = thread::spawn(move || {
  |                                 ^^^^^^^
note: required by a bound in `spawn`
 --> /rustc/b940084d7eb6a299eb4bfeb8e34901bc051e7ac4/library/std/src/thread/functions.rs:125:0

error: aborting due to 1 previous error

For more information about this error, try `rustc --explain E0277`.

Error E0277 menunjuk pelakunya dengan gamblang: *const i32 tidak mengimplementasikan Send. Kompilator sengaja menandai semua raw pointer sebagai !Send dan !Sync karena tidak ada informasi kepemilikan pada tipe — siapa pun bisa memegang pointer ke data yang dikerjakan thread lain. Penandaan ini tidak bisa diakali lewat konversi tipe; satu-satunya pintu keluar resmi adalah unsafe impl Send pada tipe pembungkus, yang kita bahas tuntas di Chapter Static Mutable.

Sifat Lain yang Membedakan dari Reference #

Tiga sifat tersisa layak dicatat sebelum kita melangkah ke dereferensi. Pertama, raw pointer tidak mengimplementasikan Display maupun Debug, sehingga println!("{}", ptr) ditolak dengan E0277 yang menyebut “*const i32 doesn’t implement std::fmt::Display”; untuk melihat alamatnya kita memakai format {:p} yang mencetak alamat memori aktual (nilainya berbeda setiap eksekusi, jadi tidak cocok menjadi contoh output tetap). Kedua, raw pointer tidak menjalankan kode apa pun saat keluar scope — tidak ada drop, tidak ada dealokasi; pointer hanya alamat yang dicatat di kertas. Ketiga, dan paling berbahaya: tidak adanya lifetime membuat pointer bisa hidup lebih lama daripada data yang ditunjuknya.

Ringkasan #

*const T dan *mut T adalah alamat tanpa jaminan: nullable, bisa menggantung, bebas di-alias, Copy, serta !Send/!Sync. Membuat raw pointer lewat cast dari reference atau as_ptr()/as_mut_ptr() adalah operasi aman, begitu pula membandingkan alamat dan memeriksa is_null(). Kompilator tetap menjaga pagar tertentu — operator + pada pointer ditolak E0369 demi method .add()/.wrapping_add() yang kontraknya eksplisit, dan penyeberangan thread ditolak E0277 karena !Send. Yang belum kita sentuh adalah alasan seluruh fasilitas ini ada: membaca dan menulis nilai di balik pointer. Operasi itulah satu-satunya gerbang menuju blok unsafe, dan menjadi fokus Chapter Dereferensi Raw Pointer.