Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Unsafe Rust

Semua kode yang udah kita bahas sejauh ini punya jaminan keamanan memori (memory safety guarantees) yang ditegakkan oleh Rust saat compile time. Namun, Rust punya sebuah bahasa kedua yang tersembunyi di dalamnya yang tidak menegakkan jaminan keamanan memori ini: namanya unsafe Rust dan dia bekerja persis kayak Rust biasa, tapi ngasih kita kekuatan super (superpowers) tambahan.

Unsafe Rust eksis karena, secara natur, analisis statis itu sifatnya konservatif. Saat compiler mencoba buat menentukan apakah sebuah kode mematuhi jaminan keamanannya atau tidak, jauh lebih baik baginya buat menolak beberapa program yang sebenarnya valid ketimbang menerima beberapa program yang ternyata tidak valid. Walaupun kodenya mungkin baik-baik aja, kalau compiler Rust tidak punya informasi yang cukup buat merasa yakin, dia bakal menolak kode tersebut. Di kasus-kasus seperti ini, kita bisa memakai kode unsafe buat ngasih tahu compiler, “Percaya deh, aku tahu apa yang lagi aku lakuin.” Namun, peringatan: kita memakai unsafe Rust dengan risiko kita sendiri: kalau kita memakai kode unsafe dengan tidak benar, masalah-masalah bisa bermunculan akibat tidak amannya memori, seperti proses dereferencing pada null pointer.

Alasan lain kenapa Rust punya sebuah alter ego yang unsafe adalah karena hardware komputer yang mendasarinya (underlying computer hardware) itu sifatnya memang tidak aman (inherently unsafe). Kalau Rust tidak membiarkan kita melakukan operasi-operasi yang tidak aman, kita tidak bakal bisa mengerjakan tugas-tugas tertentu. Rust perlu mengizinkan kita melakukan pemrograman sistem tingkat rendah (low-level systems programming), kayak berinteraksi secara langsung sama sistem operasi atau bahkan menulis sistem operasi kita sendiri. Bekerja dengan pemrograman sistem tingkat rendah adalah salah satu tujuan dari bahasa ini. Mari kita eksplorasi apa aja yang bisa kita lakukan dengan unsafe Rust dan gimana cara melakukannya.

Unsafe Superpowers

Buat beralih ke unsafe Rust, gunakan keyword unsafe lalu mulai sebuah blok baru buat menampung kode unsafe tersebut. Ada lima hal yang bisa kita lakukan di unsafe Rust yang tidak bisa kita lakukan di Rust biasa (safe Rust), yang mana kita sebut sebagai unsafe superpowers. Kekuatan super tersebut meliputi kemampuan buat:

  1. Men-dereferensi sebuah raw pointer (pointer mentah)
  2. Memanggil fungsi atau method unsafe
  3. Mengakses atau memodifikasi variabel static yang mutable
  4. Mengimplementasikan trait unsafe
  5. Mengakses field dari sebuah union

Penting sekali buat dipahami kalau unsafe tidak mematikan borrow checker atau menonaktifkan (disable) pengecekan keamanan Rust lainnya: kalau kita memakai sebuah referensi di dalam kode unsafe, dia bakal tetap dicek. Keyword unsafe cuma ngasih kita akses ke lima fitur ini yang kemudian tidak bakal dicek sama compiler buat keamanan memori. Kita bakal tetap dapat tingkat keamanan tertentu di dalam sebuah blok unsafe.

Selain itu, unsafe tidak berarti kalau kode di dalam blok tersebut pasti berbahaya atau bahwa dia pasti bakal punya masalah keamanan memori: maksud utamanya adalah sebagai programmer, Andalah yang bakal memastikan kalau kode di dalam blok unsafe tersebut bakal mengakses memori dengan cara yang valid.

Manusia itu bisa aja salah dan kesalahan (mistakes) emang bakal terjadi, tapi dengan mewajibkan kelima operasi unsafe ini buat berada di dalam blok-blok yang dianotasi dengan unsafe, kita bakal tahu kalau error apa pun yang berkaitan dengan keamanan memori itu pasti ada di dalam sebuah blok unsafe. Usahakan supaya blok unsafe itu kecil; kita bakal bersyukur nanti pas kita lagi nyelidikin bugs memori.

Buat mengisolasi kode unsafe sebanyak mungkin, praktik terbaiknya adalah membungkus (enclose) kode semacam itu di dalam sebuah abstraksi yang aman (safe abstraction) lalu menyediakan API yang aman, yang mana bakal kita bahas nanti di bab ini pas kita meneliti fungsi dan method unsafe. Beberapa bagian dari standard library diimplementasikan sebagai abstraksi yang aman di atas kode unsafe yang udah diaudit. Membungkus kode unsafe di dalam sebuah abstraksi yang aman bakal mencegah penggunaan unsafe agar tidak bocor (leaking out) ke semua tempat di mana kita atau user kita mungkin pengen memakai fungsionalitas yang diimplementasikan dengan kode unsafe tersebut, karena memakai sebuah abstraksi yang aman itu sifatnya aman.

Mari kita bahas masing-masing dari lima unsafe superpowers tersebut satu per satu. Kita juga bakal melihat beberapa abstraksi yang nyediain interface (antarmuka) yang aman buat mengakses kode unsafe.

Men-dereferensi sebuah Raw Pointer

Di Bab 4, di bagian “Dangling References”, kita menyebutkan kalau compiler selalu memastikan kalau referensi itu selalu valid. Unsafe Rust punya dua tipe baru bernama raw pointers yang mirip sama referensi. Sama kayak referensi, raw pointers bisa bersifat immutable atau mutable dan masing-masing ditulis sebagai *const T dan *mut T. Tanda bintang (*) di sini bukanlah operator dereferensi; dia adalah bagian dari nama tipenya. Di dalam konteks raw pointers, immutable berarti kalau pointer tersebut tidak bisa secara langsung di-assign (diisi nilai baru) setelah ia di-dereferensi.

Berbeda dari referensi dan smart pointers, raw pointers:

  • Diizinkan buat ngabaikan aturan borrowing dengan membiarkan kita punya baik pointer immutable maupun mutable, atau banyak pointer mutable yang menunjuk ke lokasi yang sama
  • Tidak dijamin bakal menunjuk ke memori yang valid
  • Diizinkan buat bernilai null
  • Tidak mengimplementasikan pembersihan otomatis (automatic cleanup) apa pun

Dengan memilih keluar (opting out) dari ditegakkannya jaminan-jaminan ini oleh Rust, kita bisa mengorbankan (give up) keamanan yang dijamin demi mendapatkan performa yang lebih besar atau kemampuan buat berinteraksi sama bahasa pemrograman lain atau hardware yang mana jaminan Rust tidak berlaku di situ.

Listing 20-1 menunjukkan gimana cara membikin sebuah raw pointer yang immutable dan yang mutable.

fn main() {
    let mut num = 5;

    let r1 = &raw const num;
    let r2 = &raw mut num;
}
Listing 20-1: Membikin raw pointers memakai operator raw borrow

Perhatikan bahwa kita tidak memasukkan keyword unsafe di kode ini. Kita bisa membikin raw pointers di dalam safe code (kode yang aman); kita cuma tidak bisa men-dereferensi raw pointers di luar sebuah blok unsafe, kayak yang bakal kita lihat sebentar lagi.

Kita udah membikin raw pointers dengan memakai operator raw borrow: &raw const num membikin sebuah raw pointer immutable *const i32, dan &raw mut num membikin sebuah raw pointer mutable *mut i32. Karena kita membikin mereka secara langsung dari sebuah variabel lokal, kita tahu pasti kalau raw pointers spesifik ini itu valid, tapi kita tidak bisa bikin asumsi kayak gitu buat sembarang raw pointer yang mana aja.

Buat mendemonstrasikan hal ini, selanjutnya kita bakal membikin sebuah raw pointer yang mana validitasnya tidak bisa kita pastikan, dengan memakai keyword as buat meng-cast (mengubah tipe) sebuah nilai ketimbang memakai operator raw borrow. Listing 20-2 menunjukkan gimana cara membikin sebuah raw pointer ke sebuah lokasi sembarang (arbitrary location) di memori. Mencoba buat memakai memori sembarang itu adalah perilaku yang tidak terdefinisi (undefined behavior): mungkin ada data di alamat tersebut atau mungkin tidak ada, compiler mungkin bakal mengoptimasi kodenya sehingga tidak ada akses memori yang terjadi, atau programnya mungkin bakal berhenti (terminate) karena segmentation fault. Biasanya, tidak ada alasan yang bagus buat menulis kode kayak gini, apalagi di kasus-kasus di mana kita bisa memakai operator raw borrow sebagai gantinya, tapi hal ini tetap memungkinkan buat dilakukan.

fn main() {
    let address = 0x012345usize;
    let r = address as *const i32;
}
Listing 20-2: Membikin sebuah raw pointer ke alamat memori sembarang

Ingat kembali kalau kita bisa membikin raw pointers di safe code, tapi kita tidak bisa men-dereferensi raw pointers dan membaca data yang ditunjuknya. Di Listing 20-3, kita memakai operator dereferensi * pada sebuah raw pointer yang mana mewajibkan sebuah blok unsafe.

fn main() {
    let mut num = 5;

    let r1 = &raw const num;
    let r2 = &raw mut num;

    unsafe {
        println!("r1 is: {}", *r1);
        println!("r2 is: {}", *r2);
    }
}
Listing 20-3: Men-dereferensi raw pointers di dalam sebuah blok unsafe

Membikin sebuah pointer itu tidak membahayakan apa-apa; cuma pas kita mencoba buat mengakses nilai yang ditunjuknya barulah kita berisiko berurusan dengan sebuah nilai yang tidak valid.

Perhatikan juga kalau di Listing 20-1 dan 20-3, kita membikin raw pointers *const i32 dan *mut i32 yang dua-duanya menunjuk ke lokasi memori yang sama, tempat di mana num disimpan. Kalau kita nyoba buat membikin referensi immutable dan mutable ke num sebagai gantinya, kodenya tidak bakal bisa di-compile karena aturan ownership Rust tidak mengizinkan adanya referensi mutable di saat yang bersamaan dengan referensi immutable apa pun. Dengan raw pointers, kita bisa membikin pointer mutable dan pointer immutable ke lokasi yang sama dan mengubah datanya lewat pointer mutable tersebut, yang secara potensial bisa ngebikin sebuah data race. Hati-hati ya!

Dengan semua bahaya ini, kenapa kita mau repot-repot memakai raw pointers? Salah satu skenario penggunaan (use case) utamanya adalah saat berinteraksi sama kode C, kayak yang bakal kita lihat di bagian selanjutnya. Skenario lainnya adalah pas kita lagi ngebangun abstraksi yang aman yang mana si borrow checker tidak bisa pahami. Kita bakal mengenalkan fungsi-fungsi unsafe lalu melihat sebuah contoh abstraksi aman yang memakai kode unsafe.

Memanggil Fungsi atau Method Unsafe

Jenis operasi kedua yang bisa kita lakukan di dalam blok unsafe adalah memanggil fungsi-fungsi unsafe. Fungsi dan method unsafe kelihatannya persis kayak fungsi dan method biasa, tapi mereka punya tambahan unsafe sebelum sisa definisinya. Keyword unsafe di konteks ini mengindikasikan bahwa fungsi tersebut punya persyaratan yang harus kita junjung tinggi pas kita manggil fungsi ini, karena Rust tidak bisa ngejamin kalau kita udah menuhi persyaratan tersebut. Dengan memanggil fungsi unsafe di dalam blok unsafe, kita menyatakan kalau kita udah ngebaca dokumentasi fungsi ini dan kita mengambil tanggung jawab buat mematuhi kontrak dari fungsi tersebut.

Berikut ini adalah fungsi unsafe bernama dangerous yang tidak melakukan apa-apa di dalam isinya (body):

fn main() {
    unsafe fn dangerous() {}

    unsafe {
        dangerous();
    }
}

Kita wajib memanggil fungsi dangerous di dalam sebuah blok unsafe yang terpisah. Kalau kita mencoba buat memanggil dangerous tanpa blok unsafe, kita bakal dapat error:

$ cargo run
   Compiling unsafe-example v0.1.0 (file:///projects/unsafe-example)
error[E0133]: call to unsafe function `dangerous` is unsafe and requires unsafe block
 --> src/main.rs:4:5
  |
4 |     dangerous();
  |     ^^^^^^^^^^^ call to unsafe function
  |
  = note: consult the function's documentation for information on how to avoid undefined behavior

For more information about this error, try `rustc --explain E0133`.
error: could not compile `unsafe-example` (bin "unsafe-example") due to 1 previous error

Dengan blok unsafe, kita lagi menegaskan ke Rust kalau kita udah membaca dokumentasi dari fungsinya, kita ngerti gimana cara memakainya dengan benar, dan kita udah memverifikasi kalau kita mematuhi kontrak dari fungsi tersebut.

Buat melakukan operasi unsafe di dalam isi dari sebuah fungsi unsafe, kita tetap butuh memakai sebuah blok unsafe, sama seperti di dalam fungsi biasa, dan compiler bakal ngingetin (warn) kita kalau kita lupa. Ini ngebantu kita ngejaga supaya blok unsafe itu sekecil mungkin, karena operasi unsafe mungkin tidak diperlukan di seluruh isi fungsi tersebut.

Membikin Abstraksi yang Aman di atas Kode Unsafe

Cuma karena sebuah fungsi mengandung kode unsafe tidak berarti kita harus menandai keseluruhan fungsinya sebagai unsafe. Faktanya, membungkus kode unsafe di dalam sebuah fungsi yang aman adalah sebuah abstraksi yang sangat umum. Sebagai contoh, mari kita pelajari fungsi split_at_mut dari standard library, yang mana mewajibkan beberapa kode unsafe. Kita bakal mengeksplorasi gimana kita mungkin mengimplementasikannya. Method aman ini didefinisikan pada slices mutable: dia mengambil satu slice lalu membikinnya jadi dua dengan membelah slice tersebut di indeks yang diberikan sebagai argumen. Listing 20-4 menunjukkan gimana cara memakai split_at_mut.

fn main() {
    let mut v = vec![1, 2, 3, 4, 5, 6];

    let r = &mut v[..];

    let (a, b) = r.split_at_mut(3);

    assert_eq!(a, &mut [1, 2, 3]);
    assert_eq!(b, &mut [4, 5, 6]);
}
Listing 20-4: Memakai fungsi aman split_at_mut

Kita tidak bisa mengimplementasikan fungsi ini kalau cuma memakai Rust yang aman saja. Percobaannya mungkin bakal kelihatan kayak yang ada di Listing 20-5, yang mana tidak bakal bisa di-compile. Demi kesederhanaan, kita bakal mengimplementasikan split_at_mut sebagai sebuah fungsi bukannya method dan cuma buat slices yang berisi nilai i32 bukannya buat sebuah tipe generik T.

fn split_at_mut(values: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
    let len = values.len();

    assert!(mid <= len);

    (&mut values[..mid], &mut values[mid..])
}

fn main() {
    let mut vector = vec![1, 2, 3, 4, 5, 6];
    let (left, right) = split_at_mut(&mut vector, 3);
}
Listing 20-5: Sebuah percobaan implementasi split_at_mut yang cuma memakai Rust yang aman

Fungsi ini pertama-tama mengambil panjang (length) total dari slice tersebut. Terus dia menegaskan (asserts) kalau indeks yang diberikan sebagai parameter itu ada di dalam batas-batas slice dengan mengecek apakah indeks tersebut kurang dari atau sama dengan panjangnya. Penegasan ini berarti kalau kita ngasih indeks yang lebih besar dari panjang slice tersebut buat ngebelah si slice, fungsinya bakal panic sebelum dia mencoba buat memakai indeks tersebut.

Lalu kita mengembalikan dua slices mutable di dalam sebuah tuple: yang satu dari awal (start) slice aslinya sampai ke indeks mid, dan satu lagi dari mid sampai ke akhir dari slice tersebut.

Pas kita mencoba buat men-compile kode di Listing 20-5, kita bakal dapat sebuah error:

$ cargo run
   Compiling unsafe-example v0.1.0 (file:///projects/unsafe-example)
error[E0499]: cannot borrow `*values` as mutable more than once at a time
 --> src/main.rs:6:31
  |
1 | fn split_at_mut(values: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
  |                         - let's call the lifetime of this reference `'1`
...
6 |     (&mut values[..mid], &mut values[mid..])
  |     --------------------------^^^^^^--------
  |     |     |                   |
  |     |     |                   second mutable borrow occurs here
  |     |     first mutable borrow occurs here
  |     returning this value requires that `*values` is borrowed for `'1`
  |
  = help: use `.split_at_mut(position)` to obtain two mutable non-overlapping sub-slices

For more information about this error, try `rustc --explain E0499`.
error: could not compile `unsafe-example` (bin "unsafe-example") due to 1 previous error

Borrow checker Rust tidak bisa ngerti kalau kita lagi meminjam (borrowing) bagian yang berbeda dari slice tersebut; dia cuma tahu kalau kita lagi meminjam dari slice yang sama sebanyak dua kali. Meminjam bagian yang berbeda dari sebuah slice itu secara fundamental baik-baik aja karena kedua slices tersebut tidak saling tumpang tindih (overlapping), tapi Rust tidak cukup pintar buat tahu hal ini. Pas kita tahu kalau kodenya itu aman, tapi Rust tidak tahu, inilah saatnya buat ngambil kode unsafe.

Listing 20-6 nunjukin gimana caranya memakai sebuah blok unsafe, sebuah raw pointer, dan beberapa pemanggilan ke fungsi unsafe buat ngebikin implementasi dari split_at_mut ini jalan.

use std::slice;

fn split_at_mut(values: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
    let len = values.len();
    let ptr = values.as_mut_ptr();

    assert!(mid <= len);

    unsafe {
        (
            slice::from_raw_parts_mut(ptr, mid),
            slice::from_raw_parts_mut(ptr.add(mid), len - mid),
        )
    }
}

fn main() {
    let mut vector = vec![1, 2, 3, 4, 5, 6];
    let (left, right) = split_at_mut(&mut vector, 3);
}
Listing 20-6: Memakai kode unsafe di dalam implementasi fungsi split_at_mut

Ingat kembali dari “Tipe Slice” di Bab 4 kalau sebuah slice itu adalah pointer ke suatu data beserta panjang dari slice tersebut. Kita memakai method len buat dapat panjang dari sebuah slice dan method as_mut_ptr buat ngakses raw pointer dari sebuah slice. Di kasus ini, karena kita punya slice mutable ke nilai i32, as_mut_ptr mengembalikan sebuah raw pointer dengan tipe *mut i32, yang udah kita simpan di dalam variabel ptr.

Kita tetap naruh penegasan kalau indeks mid itu ada di dalam slice. Terus kita masuk ke kode unsafe-nya: fungsi slice::from_raw_parts_mut menerima sebuah raw pointer dan sebuah panjang, terus dia ngebikin sebuah slice. Kita memakai fungsi ini buat ngebikin sebuah slice yang dimulai dari ptr dan panjangnya sebesar mid item. Terus kita panggil method add pada ptr dengan mid sebagai argumen buat dapetin raw pointer yang mulai di posisi mid, dan kita ngebikin sebuah slice memakai pointer itu dan sisa jumlah item setelah mid sebagai panjangnya.

Fungsi slice::from_raw_parts_mut itu sifatnya unsafe karena dia menerima sebuah raw pointer dan harus percaya kalau pointer ini benar-benar valid. Method add pada raw pointers juga sifatnya unsafe karena dia harus percaya kalau lokasi offset-nya itu juga merupakan pointer yang valid. Oleh karena itu, kita harus menaruh blok unsafe di sekeliling pemanggilan ke slice::from_raw_parts_mut dan add supaya kita bisa memanggil mereka. Dengan ngelihat kodenya dan dengan menambahkan penegasan kalau mid itu harus kurang dari atau sama dengan len, kita bisa tahu pasti kalau semua raw pointers yang dipakai di dalam blok unsafe tersebut bakal jadi pointers yang valid yang menunjuk ke data di dalam slice tersebut. Ini adalah penggunaan dari unsafe yang bisa diterima dan sangat tepat.

Perhatikan bahwa kita tidak perlu menandai fungsi split_at_mut hasilnya sebagai unsafe, dan kita bisa memanggil fungsi ini dari safe code. Kita udah ngebikin sebuah abstraksi yang aman ke dalam kode unsafe tersebut dengan sebuah implementasi fungsi yang memakai kode unsafe dengan cara yang aman, karena dia cuma membikin pointers yang valid dari data yang emang bisa diakses sama fungsi ini.

Sebaliknya, pemakaian slice::from_raw_parts_mut di Listing 20-7 kemungkinan besar bakal crash (rusak) pas slice-nya dipakai. Kode ini mengambil sebuah lokasi memori sembarang lalu membikin sebuah slice yang panjangnya 10.000 item.

fn main() {
    use std::slice;

    let address = 0x01234usize;
    let r = address as *mut i32;

    let values: &[i32] = unsafe { slice::from_raw_parts_mut(r, 10000) };
}
Listing 20-7: Membikin sebuah slice dari lokasi memori sembarang

Kita tidak memiliki memori di lokasi sembarang ini, dan tidak ada jaminan kalau slice yang dibikin sama kode ini benar-benar berisi nilai i32 yang valid. Mencoba buat memakai values seolah-olah dia adalah slice yang valid bakal menghasilkan perilaku yang tidak terdefinisi (undefined behavior).

Memakai Fungsi extern Buat Memanggil Kode Eksternal

Kadang-kadang kode Rust kita mungkin perlu berinteraksi sama kode yang ditulis di bahasa pemrograman lain. Buat keperluan ini, Rust punya keyword extern yang memfasilitasi pembuatan dan penggunaan Foreign Function Interface (FFI), yang mana adalah sebuah cara bagi sebuah bahasa pemrograman buat mendefinisikan fungsi lalu memungkinkan bahasa pemrograman (asing) lain buat memanggil fungsi-fungsi tersebut.

Listing 20-8 mendemonstrasikan gimana caranya nge-setup sebuah integrasi dengan fungsi abs dari standard library C. Fungsi-fungsi yang dideklarasikan di dalam blok extern itu umumnya tidak aman (unsafe) buat dipanggil dari kode Rust, jadi blok extern tersebut juga harus ditandai sebagai unsafe. Alasannya adalah karena bahasa- bahasa lain tidak menerapkan (enforce) aturan-aturan dan jaminan-jaminan yang dipunyai Rust, dan Rust tidak bisa ngecek mereka, jadi tanggung jawabnya jatuh ke tangan si programmer buat memastikan keamanan kodenya.

Filename: src/main.rs
unsafe extern "C" {
    fn abs(input: i32) -> i32;
}

fn main() {
    unsafe {
        println!("Absolute value of -3 according to C: {}", abs(-3));
    }
}
Listing 20-8: Mendeklarasikan dan memanggil sebuah fungsi extern yang didefinisikan di bahasa pemrograman lain

Di dalam blok unsafe extern "C", kita mendaftarkan (list) nama-nama dan signatures dari fungsi-fungsi eksternal dari bahasa lain yang mau kita panggil. Bagian "C" itu mendefinisikan application binary interface (ABI) mana yang dipakai sama fungsi eksternal tersebut: ABI ini mendefinisikan gimana caranya memanggil fungsinya pada tingkat bahasa rakitan (assembly level). ABI "C" adalah yang paling umum dipakai dan mengikuti standar ABI dari bahasa pemrograman C. Informasi tentang semua ABI yang didukung oleh Rust ada di Rust Reference.

Semua item yang dideklarasikan di dalam blok unsafe extern itu secara implisit sifatnya unsafe. Namun, beberapa fungsi FFI memang aman buat dipanggil. Contohnya, fungsi abs dari standard library C itu tidak punya pertimbangan apa pun soal keamanan memori dan kita tahu dia bisa dipanggil pakai sembarang i32. Di kasus-kasus kayak gini, kita bisa memakai keyword safe buat ngasih tahu kalau fungsi yang spesifik ini itu aman buat dipanggil biarpun dia ada di dalam sebuah blok unsafe extern. Begitu kita bikin perubahan tersebut, manggil fungsinya udah tidak perlu blok unsafe lagi, kayak yang ditunjukin di Listing 20-9.

Filename: src/main.rs
unsafe extern "C" {
    safe fn abs(input: i32) -> i32;
}

fn main() {
    println!("Absolute value of -3 according to C: {}", abs(-3));
}
Listing 20-9: Secara eksplisit menandai sebuah fungsi sebagai safe di dalam sebuah blok unsafe extern dan memanggilnya dengan aman

Menandai sebuah fungsi sebagai safe tidak secara otomatis (inherently) membikin fungsinya jadi aman ya! Sebaliknya, ini ibarat janji yang kita buat ke Rust kalau dia itu aman. Itu tetap jadi tanggung jawab kita buat mastiin kalau janji itu ditepati!

Memanggil Fungsi Rust dari Bahasa Lain

Kita juga bisa memakai extern buat membikin interface yang memungkinkan bahasa pemrograman lain buat memanggil fungsi Rust. Ketimbang membikin satu blok extern utuh, kita menambahkan keyword extern dan menentukan ABI apa yang mau dipakai tepat sebelum keyword fn buat fungsi yang relevan. Kita juga perlu nambahin anotasi #[unsafe(no_mangle)] buat ngasih tahu compiler Rust supaya tidak nge-mangle nama dari fungsi ini. Mangling adalah pas compiler ngubah nama yang udah kita kasih ke sebuah fungsi jadi nama lain yang mengandung lebih banyak informasi biar bisa dikonsumsi sama bagian-bagian lain dari proses kompilasi tapi jadinya kurang enak dibaca sama manusia. Setiap compiler bahasa pemrograman nge-mangle nama dengan cara yang agak berbeda-beda, jadi supaya sebuah fungsi Rust bisa dipanggil namanya sama bahasa lain, kita harus mematikan fitur name mangling dari compiler Rust. Hal ini sifatnya unsafe karena bisa aja terjadi bentrok nama (name collisions) antar libraries kalau mangling bawaan ini dimatiin, jadi ini adalah tanggung jawab kita buat mastiin kalau nama yang kita pilih itu aman buat diekspor tanpa di-mangle.

Di contoh berikut ini, kita membikin fungsi call_from_c supaya bisa diakses dari kode C, setelah dia di-compile jadi shared library dan di-link dari C:

#[unsafe(no_mangle)]
pub extern "C" fn call_from_c() {
    println!("Just called a Rust function from C!");
}

Penggunaan extern ini cuma mewajibkan adanya unsafe di dalam atributnya aja, bukan di blok extern-nya.

Mengakses atau Memodifikasi Variabel Static yang Mutable

Di buku ini, kita belum pernah ngebahas soal variabel global (global variables), yang mana memang didukung oleh Rust tapi bisa jadi bermasalah kalau digabungkan sama aturan ownership Rust. Kalau ada dua threads yang lagi mengakses variabel global yang mutable yang sama, hal itu bisa nyebabin sebuah data race.

Di Rust, variabel global itu disebut sebagai variabel static (statis). Listing 20-10 menunjukkan contoh deklarasi dan penggunaan dari sebuah variabel statis dengan nilai berupa sebuah string slice.

Filename: src/main.rs
static HELLO_WORLD: &str = "Hello, world!";

fn main() {
    println!("value is: {HELLO_WORLD}");
}
Listing 20-10: Mendefinisikan dan memakai sebuah variabel statis yang immutable

Variabel static itu mirip sama konstanta (constants), yang mana udah kita bahas di “Konstanta” di Bab 3. Nama-nama buat variabel statis itu ditulis pakai gaya SCREAMING_SNAKE_CASE secara konvensi. Variabel statis cuma bisa menyimpan referensi dengan lifetime 'static, yang berarti compiler Rust udah tahu soal lifetime-nya dan kita tidak diwajibkan buat menganotasinya secara eksplisit. Mengakses sebuah variabel statis yang immutable itu aman.

Satu perbedaan yang cukup halus (subtle) antara konstanta dan variabel statis immutable adalah nilai-nilai yang ada di dalam sebuah variabel statis punya alamat yang tetap di memori. Memakai nilai tersebut bakal selalu mengakses data yang sama. Konstanta, di sisi lain, diizinkan buat menduplikasi data mereka kapan pun mereka dipakai. Perbedaan lainnya adalah variabel statis itu bisa bersifat mutable (bisa diubah). Mengakses dan memodifikasi variabel statis mutable itu sifatnya unsafe. Listing 20-11 menunjukkan gimana caranya mendeklarasikan, mengakses, dan memodifikasi sebuah variabel statis mutable bernama COUNTER.

Filename: src/main.rs
static mut COUNTER: u32 = 0;

/// SAFETY: Calling this from more than a single thread at a time is undefined
/// behavior, so you *must* guarantee you only call it from a single thread at
/// a time.
unsafe fn add_to_count(inc: u32) {
    unsafe {
        COUNTER += inc;
    }
}

fn main() {
    unsafe {
        // SAFETY: This is only called from a single thread in `main`.
        add_to_count(3);
        println!("COUNTER: {}", *(&raw const COUNTER));
    }
}
Listing 20-11: Ngebaca atau nulis ke sebuah variabel statis mutable itu unsafe.

Sama kayak variabel biasa, kita menentukan mutabilitasnya dengan memakai keyword mut. Kode apa pun yang membaca atau menulis dari COUNTER wajib berada di dalam sebuah blok unsafe. Kode di Listing 20-11 berhasil di-compile dan mencetak COUNTER: 3 kayak yang kita harapkan karena ini adalah program single-threaded (utas tunggal). Kalau sampai ada banyak threads yang mengakses COUNTER, itu kemungkinan besar bakal nyebabin data races, jadi itu dianggap sebagai perilaku yang tidak terdefinisi. Oleh karena itu, kita perlu menandai keseluruhan fungsinya sebagai unsafe dan mendokumentasikan batasan keamanannya, supaya siapa pun yang memanggil fungsi ini tahu apa aja yang boleh dan tidak boleh mereka lakuin dengan aman.

Kapan pun kita nulis sebuah fungsi yang unsafe, sangatlah idiomatik buat nulis sebuah komentar yang diawali dengan kata SAFETY dan ngejelasin apa aja yang perlu dilakuin sama si pemanggil fungsi buat memanggil fungsi tersebut dengan aman. Sama halnya, kapan pun kita ngelakuin operasi unsafe, juga sangat idiomatik buat nulis sebuah komentar yang diawali dengan SAFETY buat ngejelasin gimana aturan-aturan keamanan tersebut dijunjung tinggi.

Selain itu, compiler secara default bakal melarang usaha apa pun buat ngebikin referensi ke sebuah variabel statis yang mutable lewat mekanisme lint (peringatan kode) compiler. Kita wajib secara eksplisit keluar (opt-out) dari perlindungan lint tersebut dengan nambahin anotasi #[allow(static_mut_refs)] atau ngakses variabel statis mutable tersebut lewat sebuah raw pointer yang dibikin pakai salah satu dari operator raw borrow. Hal ini termasuk juga kasus-kasus di mana referensi tersebut dibikin secara kasatmata, kayak pas dia dipakai di println! di dalam kode ini. Mewajibkan supaya referensi ke variabel statis yang mutable harus dibikin lewat raw pointers ngebantu ngebikin persyaratan keamanannya jadi lebih terlihat jelas saat kita memakai mereka.

Dengan data mutable yang bisa diakses secara global, itu susah sekali buat memastikan kalau tidak bakal ada data races, yang mana inilah alasan kenapa Rust menganggap variabel statis mutable itu sebagai unsafe. Kalau memungkinkan, jauh lebih baik buat memakai teknik-teknik konkurensi dan smart pointers yang thread-safe yang udah kita bahas di Bab 16 supaya compiler bisa ngecek kalau akses data dari berbagai threads yang berbeda dilakukan dengan aman.

Mengimplementasikan Sebuah Unsafe Trait

Kita bisa memakai unsafe buat mengimplementasikan sebuah trait unsafe. Sebuah trait itu dianggap unsafe saat minimal salah satu dari method-methodnya punya aturan mutlak (invariant) yang mana tidak bisa diverifikasi sama compiler. Kita mendeklarasikan kalau sebuah trait itu unsafe dengan menambahkan keyword unsafe sebelum keyword trait dan menandai implementasi dari trait tersebut sebagai unsafe juga, seperti yang ditunjukin di Listing 20-12.

unsafe trait Foo {
    // methods go here
}

unsafe impl Foo for i32 {
    // method implementations go here
}

fn main() {}
Listing 20-12: Mendefinisikan dan mengimplementasikan sebuah unsafe trait

Dengan memakai unsafe impl, kita lagi berjanji kalau kita bakal menjunjung tinggi aturan-aturan mutlak (invariants) yang tidak bisa diverifikasi sama compiler.

Sebagai contoh, ingat kembali marker traits Send dan Sync yang kita bahas di “Konkurensi yang Bisa Diperluas dengan Trait Send dan Sync di Bab 16: compiler mengimplementasikan trait-trait ini secara otomatis kalau tipe-tipe kita sepenuhnya disusun dari tipe-tipe lain yang udah mengimplementasikan Send dan Sync. Kalau kita ngimplementasiin sebuah tipe yang mengandung sebuah tipe yang tidak mengimplementasikan Send atau Sync, contohnya raw pointers, dan kita mau menandai tipe tersebut sebagai Send atau Sync, kita wajib memakai unsafe. Rust tidak bisa memverifikasi kalau tipe kita itu mematuhi jaminan bahwa dia bisa dikirim ke thread lain dengan aman atau diakses dari banyak threads dengan aman; makanya, kita perlu ngelakuin pengecekan itu sendiri secara manual dan ngasih tahu hal itu lewat keyword unsafe.

Mengakses Field dari Union

Tindakan (action) terakhir yang cuma bisa bekerja dengan keyword unsafe adalah mengakses field (bidang) dari sebuah union. Sebuah union (gabungan) itu mirip sama sebuah struct, tapi cuma satu aja dari field yang dideklarasikan yang bisa dipakai di dalam sebuah instance pada satu waktu tertentu. Unions biasanya dipakai buat berinteraksi dengan unions yang ada di dalam kode C. Mengakses field dari union itu unsafe karena Rust tidak bisa ngejamin tipe dari data yang saat itu lagi disimpan di dalam instance union tersebut. Kita bisa belajar lebih banyak soal unions di Rust Reference.

Memakai Miri Buat Mengecek Kode Unsafe

Pas kita lagi nulis kode unsafe, kita mungkin pengen ngecek apakah kode yang udah kita tulis itu benar-benar aman dan udah betul (correct) atau tidak. Salah satu cara terbaik buat ngebuktiin itu adalah dengan memakai Miri, sebuah tool resmi Rust buat mendeteksi perilaku yang tidak terdefinisi (undefined behavior). Kalau borrow checker itu adalah sebuah tool yang statis (static) yang bekerja pas compile time, Miri itu adalah tool yang dinamis (dynamic) yang bekerja pas runtime. Miri mengecek kode kita dengan cara menjalankan program kita, atau seperangkat pengujiannya (test suite), lalu mendeteksi kapan kita melanggar aturan-aturan yang Miri pahami tentang gimana seharusnya Rust bekerja.

Memakai Miri mewajibkan kita punya instalasi Rust versi nightly (yang mana kita obrolin lebih banyak di Lampiran G: Gimana Rust Dibuat dan “Nightly Rust”). Kita bisa menginstal baik Rust versi nightly sekaligus tool Miri dengan mengetikkan perintah rustup +nightly component add miri. Hal ini tidak bakal ngubah versi Rust apa yang lagi dipakai sama project kita; dia cuma nambahin tool tersebut ke sistem kita biar kita bisa memakainya pas kita mau aja. Kita bisa menjalankan Miri di sebuah project dengan ngetik cargo +nightly miri run atau cargo +nightly miri test.

Sebagai contoh betapa bergunanya ini, bayangin apa yang terjadi pas kita menjalankan Miri buat nge-tes kode di Listing 20-7.

$ cargo +nightly miri run
   Compiling unsafe-example v0.1.0 (file:///projects/unsafe-example)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.01s
     Running `file:///home/.rustup/toolchains/nightly/bin/cargo-miri runner target/miri/debug/unsafe-example`
warning: integer-to-pointer cast
 --> src/main.rs:5:13
  |
5 |     let r = address as *mut i32;
  |             ^^^^^^^^^^^^^^^^^^^ integer-to-pointer cast
  |
  = help: this program is using integer-to-pointer casts or (equivalently) `ptr::with_exposed_provenance`, which means that Miri might miss pointer bugs in this program
  = help: see https://doc.rust-lang.org/nightly/std/ptr/fn.with_exposed_provenance.html for more details on that operation
  = help: to ensure that Miri does not miss bugs in your program, use Strict Provenance APIs (https://doc.rust-lang.org/nightly/std/ptr/index.html#strict-provenance, https://crates.io/crates/sptr) instead
  = help: you can then set `MIRIFLAGS=-Zmiri-strict-provenance` to ensure you are not relying on `with_exposed_provenance` semantics
  = help: alternatively, `MIRIFLAGS=-Zmiri-permissive-provenance` disables this warning
  = note: BACKTRACE:
  = note: inside `main` at src/main.rs:5:13: 5:32

error: Undefined Behavior: pointer not dereferenceable: pointer must be dereferenceable for 40000 bytes, but got 0x1234[noalloc] which is a dangling pointer (it has no provenance)
 --> src/main.rs:7:35
  |
7 |     let values: &[i32] = unsafe { slice::from_raw_parts_mut(r, 10000) };
  |                                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Undefined Behavior occurred here
  |
  = help: this indicates a bug in the program: it performed an invalid operation, and caused Undefined Behavior
  = help: see https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html for further information
  = note: BACKTRACE:
  = note: inside `main` at src/main.rs:7:35: 7:70

note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace

error: aborting due to 1 previous error; 1 warning emitted

Miri ngingetin kita dengan tepat kalau kita lagi nge-cast (mengubah tipe) sebuah integer jadi sebuah pointer, yang mana mungkin aja jadi masalah tapi Miri tidak bisa mendeteksi apakah emang ada masalah atau tidak karena Miri tidak tahu dari mana asal-usul si pointer tersebut. Terus, Miri ngembaliin sebuah error di mana Listing 20-7 punya perilaku yang tidak terdefinisi (undefined behavior) karena kita punya sebuah dangling pointer. Berkat Miri, sekarang kita jadi tahu kalau ada risiko terjadinya perilaku yang tidak terdefinisi, dan kita bisa mikirin gimana caranya ngebikin kodenya jadi aman. Di beberapa kasus, Miri bahkan bisa ngasih saran gimana cara buat ngeberesin error-error tersebut.

Miri tidak bisa menangkap semua hal yang mungkin keliru pas kita nulis kode unsafe. Miri itu adalah sebuah tool analisis yang dinamis, jadi dia cuma bisa nangkap masalah pada kode yang emang benar-benar dijalankan. Itu artinya kita perlu memakainya barengan sama teknik-teknik pengujian (testing techniques) yang oke buat ningkatin rasa percaya diri kita terhadap kode unsafe yang udah kita tulis. Miri juga tidak mencakup setiap kemungkinan yang ada yang bikin kode kita jadi cacat (unsound).

Dengan kata lain: Kalau Miri emang nangkap sebuah masalah, kita jadi tahu kalau ada sebuah bug, tapi cuma karena Miri tidak nangkap sebuah bug itu tidak berarti kalau tidak ada masalah di situ. Walaupun begitu, dia bisa nangkap sangat banyak lho. Cobain deh ngejalanin Miri di contoh-contoh kode unsafe lain di bab ini dan lihat apa aja yang dia bilang!

Kita bisa belajar lebih lanjut soal Miri di repositori GitHub-nya.

Kapan Harus Memakai Kode Unsafe

Memakai unsafe buat melakukan salah satu dari lima superpowers yang baru aja kita bahas itu bukanlah hal yang salah atau yang dilarang keras, tapi emang lebih tricky (susah) buat bikin kode unsafe itu jadi benar (correct) karena compiler tidak bisa ngebantu buat menjunjung tinggi keamanan memori. Pas kita punya alasan buat memakai kode unsafe, kita bebas aja buat melakukannya, dan dengan adanya anotasi unsafe yang eksplisit itu malah ngebikin kita lebih gampang buat ngelacak sumber dari suatu masalah kalau sampai masalah itu muncul nanti. Kapan pun kita nulis kode unsafe, kita bisa memakai Miri buat ngebantu kita jadi lebih yakin kalau kode yang udah kita tulis itu benar-benar mematuhi aturan-aturan Rust.

Buat penjelajahan yang jauh lebih dalam soal gimana cara kerja efektif dengan unsafe Rust, silakan baca panduan resmi dari Rust soal subjek ini, yaitu Rustonomicon.