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:
- Men-dereferensi sebuah raw pointer (pointer mentah)
- Memanggil fungsi atau method unsafe
- Mengakses atau memodifikasi variabel static yang mutable
- Mengimplementasikan trait unsafe
- 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;
}
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;
}
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);
}
}
unsafeMembikin 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]);
}
split_at_mutKita 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);
}
split_at_mut yang cuma memakai Rust yang amanFungsi 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);
}
split_at_mutIngat 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) };
}
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.
unsafe extern "C" {
fn abs(input: i32) -> i32;
}
fn main() {
unsafe {
println!("Absolute value of -3 according to C: {}", abs(-3));
}
}
extern yang didefinisikan di bahasa pemrograman lainDi 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.
unsafe extern "C" {
safe fn abs(input: i32) -> i32;
}
fn main() {
println!("Absolute value of -3 according to C: {}", abs(-3));
}
safe di dalam sebuah blok unsafe extern dan memanggilnya dengan amanMenandai 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.
static HELLO_WORLD: &str = "Hello, world!";
fn main() {
println!("value is: {HELLO_WORLD}");
}
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.
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));
}
}
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() {}
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.