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

Güvenli Olmayan (Unsafe) Rust

Şimdiye kadar ele aldığımız tüm kodlarda Rust’ın bellek güvenliği garantileri derleme zamanında zorunlu kılınıyordu. Ancak Rust’ın içinde gizli, bu bellek güvenliği garantilerini zorunlu kılmayan ikinci bir dil daha vardır: Buna güvenli olmayan Rust (unsafe Rust) denir; tıpkı normal Rust gibi çalışır ama bize ek süper güçler verir.

Güvenli olmayan Rust’ın var olma nedeni, statik analizin doğası gereği temkinli olmasıdır. Derleyici bir kodun güvenlik garantilerini koruyup korumadığını belirlemeye çalışırken, bazı geçersiz programları kabul etmektense bazı geçerli programları reddetmesi daha iyidir. Kod gerçekte sorunsuz olabilse bile, Rust derleyicisi bundan emin olacak yeterli bilgiye sahip değilse kodu reddeder. Bu gibi durumlarda derleyiciye “Bana güven, ne yaptığımı biliyorum” demek için güvenli olmayan kod kullanabilirsiniz. Ancak güvenli olmayan Rust’ı tüm riski size ait olmak üzere kullandığınızı unutmayın: Güvenli olmayan kodu hatalı kullanırsanız, boş (null) işaretçinin başvurusunu çözmek gibi bellek güvenliği ihlallerinden kaynaklanan sorunlar meydana gelebilir.

Rust’ın güvenli olmayan bir ikinci yüzünün (alter ego) bulunmasının bir diğer nedeni de alttaki bilgisayar donanımının doğası gereği güvenli olmamasıdır. Rust güvenli olmayan işlemler yapmanıza izin vermeseydi bazı görevleri yerine getiremezdiniz. İşletim sistemiyle doğrudan etkileşim kurmak veya hatta kendi işletim sisteminizi yazmak gibi düşük seviyeli sistem programlama işlerini yapabilmenize Rust’ın olanak tanıması gerekir. Düşük seviyeli sistem programlamayla çalışabilmek, dilin temel hedeflerinden biridir. Gelin güvenli olmayan Rust ile neler yapabileceğimizi ve bunları nasıl gerçekleştireceğimizi inceleyelim.

Güvenli Olmayan Süper Güçleri Kullanmak

Güvenli olmayan Rust’a geçmek için unsafe anahtar kelimesini kullanır ve ardından güvenli olmayan kodu barındıran yeni bir blok başlatırsınız. Güvenli olmayan Rust içinde, güvenli Rust’ta yapamadığınız beş işlemi gerçekleştirebilirsiniz; bunlara güvenli olmayan süper güçler adını veriyoruz. Bu süper güçler şunları yapabilmeyi kapsar:

  1. Bir ham işaretçinin başvurusunu çözmek.
  2. Güvenli olmayan bir fonksiyon veya metot çağırmak.
  3. Değiştirilebilir bir statik değişkene erişmek veya onu değiştirmek.
  4. Güvenli olmayan bir trait uygulamak.
  5. Birleşim (union) türlerinin alanlarına erişmek.

unsafe anahtar kelimesinin ödünç alma denetleyicisini kapatmadığını veya Rust’ın diğer güvenlik denetimlerini devre dışı bırakmadığını anlamak çok önemlidir: Güvenli olmayan kod içinde bir referans kullanırsanız bu referans yine denetlenir. unsafe anahtar kelimesi yalnızca, derleyici tarafından bellek güvenliği açısından denetlenmeyen bu beş özelliğe erişmenizi sağlar. Yani güvenli olmayan bir bloğun içinde bile belirli ölçüde güvenlik elde etmeye devam edersiniz.

Buna ek olarak unsafe, blok içindeki kodun mutlaka tehlikeli olduğu ya da kesinlikle bellek güvenliği sorunları barındıracağı anlamına gelmez: Buradaki amaç, programcı olarak sizin unsafe bloğu içindeki kodun belleğe geçerli bir biçimde erişmesini güvence altına almanızdır.

İnsanlar hata yapabilir ve hatalar kaçınılmaz olarak yaşanacaktır; ancak bu beş işlemi unsafe bloklarıyla işaretlemek, bellek güvenliğini elle doğrulamanız gereken noktaları görünür kılar. Bir ihlalin nedeni mutlaka bu blokta olmak zorunda değildir: Güvenli kodda değiştirilen bir alan da unsafe kodun dayandığı bir ön koşulu bozabilir. Bu nedenle güvenli sarmalayıcının koruduğu bütün değişmezleri birlikte değerlendirmelisiniz. unsafe bloklarını küçük tutun; ileride bellek hatalarını araştırırken bunun çok faydasını göreceksiniz.

Güvenli olmayan kodu mümkün olduğunca yalıtmak için en iyi yaklaşım, bu kodu güvenli bir soyutlamanın içine alıp dışarıya güvenli bir API sunmaktır; bunu bölümün ilerleyen kısımlarında güvenli olmayan fonksiyon ve metotları incelerken ele alacağız. Standart kütüphanenin bazı bölümleri, denetlenmiş güvenli olmayan kodların üzerine inşa edilen güvenli soyutlamalar olarak uygulanmıştır. Güvenli olmayan kodu güvenli bir soyutlamayla sarmalamak, sizin veya kullanıcılarınızın unsafe kodla uygulanmış işlevselliği kullanmak istediği her yere unsafe kullanımının sızmasını önler; çünkü güvenli bir soyutlamayı kullanmak güvenlidir.

Şimdi bu beş güvenli olmayan süper gücün her birini sırayla inceleyelim. Aynı zamanda güvenli olmayan koda güvenli bir arayüz sunan bazı soyutlamalara da göz atacağız.

Ham İşaretçinin Başvurusunu Çözmek

Bölüm 4’teki “Sarkan Referanslar” bölümünde, derleyicinin referansların her zaman geçerli olmasını sağladığından bahsetmiştik. Güvenli olmayan Rust, referanslara benzeyen ham işaretçiler (raw pointers) adında iki yeni türe sahiptir. Tıpkı referanslarda olduğu gibi ham işaretçiler de değiştirilemez veya değiştirilebilir olabilir ve sırasıyla *const T ile *mut T şeklinde yazılır. Buradaki yıldız işareti (*) başvuru çözme (dereferencing) operatörü değildir; tür adının bir parçasıdır. Ham işaretçiler bağlamında değiştirilemez olmak, işaretçinin başvurusu çözüldükten sonra ona doğrudan atama yapılamaması anlamına gelir.

Referanslardan ve akıllı işaretçilerden farklı olarak ham işaretçiler:

  • Aynı konuma hem değiştirilemez hem değiştirilebilir işaretçilere ya da birden fazla değiştirilebilir işaretçiye sahip olarak ödünç alma kurallarını göz ardı edebilir
  • Geçerli bir bellek alanına işaret ettikleri garanti edilmez
  • Boş (null) olabilir
  • Herhangi bir otomatik temizlik uygulamaz

Rust’ın bu garantileri zorunlu kılmasından vazgeçtiğinizde garanti edilen güvenlikten feragat edersiniz; karşılığında daha yüksek performans ya da Rust’ın garantilerinin geçerli olmadığı başka bir dil veya donanımla etkileşim kurabilme imkânı kazanırsınız.

Liste 20-1, değiştirilemez ve değiştirilebilir birer ham işaretçinin nasıl oluşturulacağını göstermektedir.

fn main() {
    let mut num = 5;

    let r1 = &raw const num;
    let r2 = &raw mut num;
}
Liste 20-1: Ham ödünç alma operatörleriyle ham işaretçiler oluşturmak

Bu kodda unsafe anahtar kelimesini kullanmadığımıza dikkat edin. Güvenli kod içinde ham işaretçiler oluşturabiliriz; yalnızca birazdan göreceğiniz gibi güvenli olmayan bir bloğun dışında ham işaretçilerin başvurusunu çözemeyiz.

Ham işaretçileri ham ödünç alma operatörlerini (raw borrow operators) kullanarak oluşturduk: &raw const num ifadesi *const i32 türünde değiştirilemez bir ham işaretçi, &raw mut num ifadesi ise *mut i32 türünde değiştirilebilir bir ham işaretçi oluşturur. Bunları doğrudan yerel bir değişkenden ürettiğimiz için bu ham işaretçilerin geçerli olduğunu biliyoruz; ancak herhangi bir ham işaretçi hakkında aynı varsayımda bulunamayız.

Bunu göstermek için şimdi, ham ödünç alma operatörünü kullanmak yerine bir değeri as anahtar kelimesiyle dönüştürerek geçerliliğinden o kadar da emin olamayacağımız bir ham işaretçi oluşturacağız. Liste 20-2, bellekteki keyfi bir konuma işaret eden bir ham işaretçinin nasıl oluşturulacağını gösterir. Keyfi bir bellek alanını kullanmaya çalışmak tanımsız davranıştır: O adreste veri bulunabilir de bulunmayabilir de; derleyici kodu hiçbir bellek erişimi yapılmayacak şekilde optimize edebilir ya da program bir segmentasyon hatasıyla (segmentation fault) sonlanabilir. Özellikle bunun yerine ham ödünç alma operatörü kullanabildiğiniz durumlarda böyle bir kod yazmak için genellikle hiçbir geçerli neden yoktur; ancak bunu yapmak mümkündür.

fn main() {
    let address = 0x012345usize;
    let r = address as *const i32;
}
Liste 20-2: Keyfi bir bellek adresine ham işaretçi oluşturmak

Güvenli kod içinde ham işaretçiler oluşturabildiğimizi, ancak ham işaretçilerin başvurusunu çözüp işaret edilen veriyi okuyamadığımızı hatırlayın. Liste 20-3’te ham işaretçiler üzerinde başvuru çözme operatörü *’ı kullanıyoruz; bu işlem bir unsafe bloğu gerektirir.

fn main() {
    let mut num = 5;

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

    unsafe {
        println!("r1 değeri: {}", *r1);
        println!("r2 değeri: {}", *r2);
    }
}
Liste 20-3: Bir unsafe bloğu içinde ham işaretçilerin başvurusunu çözmek

Bir işaretçi oluşturmanın tek başına hiçbir zararı yoktur; geçersiz bir değerle karşı karşıya kalma ihtimalimiz yalnızca işaretçinin gösterdiği değere erişmeye çalıştığımızda ortaya çıkar.

Ayrıca Liste 20-1 ve 20-3’te, her ikisi de num değerinin saklandığı aynı bellek konumuna işaret eden *const i32 ve *mut i32 ham işaretçileri oluşturduğumuza dikkat edin. Bunun yerine num değişkenine hem değiştirilemez hem de değiştirilebilir bir referans oluşturmaya çalışsaydık kod derlenmezdi; çünkü Rust’ın sahiplik kuralları herhangi bir değiştirilemez referansla aynı anda değiştirilebilir bir referans bulunmasına izin vermez. Ham işaretçilerle ise aynı konuma hem değiştirilebilir bir işaretçi hem de değiştirilemez bir işaretçi oluşturabilir ve değiştirilebilir işaretçi üzerinden veriyi değiştirerek potansiyel bir veri yarışı yaratabiliriz. Dikkatli olun!

Tüm bu tehlikelere rağmen neden ham işaretçi kullanasınız ki? Başlıca kullanım alanlarından biri, bir sonraki bölümde göreceğiniz gibi C koduyla etkileşim kurmaktır. Bir diğer durum ise ödünç alma denetleyicisinin anlayamadığı güvenli soyutlamalar inşa etmektir. Şimdi güvenli olmayan fonksiyonları tanıtacak, ardından güvenli olmayan kod kullanan güvenli bir soyutlama örneğine bakacağız.

Güvenli Olmayan Bir Fonksiyon veya Metot Çağırmak

Güvenli olmayan bir blok içinde gerçekleştirebileceğiniz ikinci işlem türü, güvenli olmayan fonksiyonları çağırmaktır. Güvenli olmayan fonksiyonlar ve metotlar görünüm olarak sıradan fonksiyon ve metotlarla birebir aynıdır; ancak tanımın geri kalanından önce fazladan bir unsafe anahtar kelimesi taşırlar. Bu bağlamda unsafe anahtar kelimesi, fonksiyonu çağırırken uymamız gereken bazı gereksinimler bulunduğunu belirtir; çünkü Rust bu gereksinimleri karşıladığımızı garanti edemez. Güvenli olmayan bir fonksiyonu bir unsafe bloğu içinde çağırarak, bu fonksiyonun dokümantasyonunu okuduğumuzu ve fonksiyonun sözleşmelerini yerine getirme sorumluluğunu üstlendiğimizi beyan etmiş oluruz.

İşte gövdesinde hiçbir iş yapmayan dangerous adlı güvenli olmayan bir fonksiyon:

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

    unsafe {
        dangerous();
    }
}

dangerous fonksiyonunu ayrı bir unsafe bloğu içinde çağırmamız gerekir. dangerous fonksiyonunu unsafe bloğu olmadan çağırmayı denersek bir hata alırız:

$ cargo run
   Compiling guvensiz-ornek v0.1.0 (file:///projects/guvensiz-ornek)
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 `guvensiz-ornek` (bin "guvensiz-ornek") due to 1 previous error

unsafe bloğuyla Rust’a fonksiyonun dokümantasyonunu okuduğumuzu, onu nasıl doğru kullanacağımızı anladığımızı ve fonksiyonun sözleşmesini yerine getirdiğimizi doğruladığımızı beyan ederiz.

Bir unsafe fonksiyonun gövdesinde güvenli olmayan işlemler gerçekleştirmek için, tıpkı sıradan bir fonksiyonun içinde olduğu gibi yine bir unsafe bloğu kullanmanız gerekir ve bunu unutursanız derleyici sizi uyarır. Güvenli olmayan işlemlere fonksiyon gövdesinin tamamında ihtiyaç duyulmayabileceğinden, bu kural unsafe bloklarını olabildiğince küçük tutmamıza yardımcı olur.

Güvenli Olmayan Kod Üzerine Güvenli Bir Soyutlama Oluşturmak

Bir fonksiyonun güvenli olmayan kod içermesi, fonksiyonun tamamını güvenli olmayan (unsafe) olarak işaretlememiz gerektiği anlamına gelmez. Hatta güvenli olmayan kodu güvenli bir fonksiyonun içine sarmalamak oldukça yaygın bir soyutlamadır. Örnek olarak standart kütüphanedeki, biraz güvenli olmayan kod gerektiren split_at_mut fonksiyonunu inceleyelim ve onu nasıl uygulayabileceğimizi keşfedelim. Bu güvenli metot, değiştirilebilir dilimler üzerinde tanımlanmıştır: Bir dilim alır ve argüman olarak verilen indeksten bölerek onu iki dilime ayırır. Liste 20-4, split_at_mut kullanımını göstermektedir.

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]);
}
Liste 20-4: Güvenli split_at_mut fonksiyonunu kullanmak

Bu fonksiyonu yalnızca güvenli Rust kullanarak uygulayamayız. Böyle bir deneme Liste 20-5’teki gibi görünebilir, ancak bu kod derlenmeyecektir. Basitlik adına split_at_mut’u bir metot yerine fonksiyon olarak ve jenerik bir T türü yerine yalnızca i32 değerlerinden oluşan dilimler için uygulayacağız.

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);
}
Liste 20-5: Yalnızca güvenli Rust kullanarak split_at_mut uygulama denemesi

Bu fonksiyon önce dilimin toplam uzunluğunu alır. Ardından parametre olarak verilen indeksin uzunluktan küçük veya ona eşit olup olmadığını kontrol ederek dilim sınırları içinde kaldığını doğrular. Bu doğrulama, dilimi bölmek için uzunluktan daha büyük bir indeks geçirirsek fonksiyonun o indeksi kullanmaya çalışmadan önce panikleyeceği anlamına gelir.

Daha sonra bir demet içinde iki değiştirilebilir dilim döndürürüz: biri orijinal dilimin başlangıcından mid indeksine kadar, diğeri ise mid indeksinden dilimin sonuna kadar.

Liste 20-5’teki kodu derlemeyi denediğimizde şu hatayı alırız:

$ cargo run
   Compiling guvensiz-ornek v0.1.0 (file:///projects/guvensiz-ornek)
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 `guvensiz-ornek` (bin "guvensiz-ornek") due to 1 previous error

Rust’ın ödünç alma denetleyicisi dilimin farklı bölümlerini ödünç aldığımızı anlayamaz; yalnızca aynı dilimden iki kez ödünç aldığımızı bilir. İki dilim birbiriyle örtüşmediği için bir dilimin farklı bölümlerini ödünç almak temelde hiçbir sorun oluşturmaz, ancak Rust bunu bilecek kadar akıllı değildir. Kodun sorunsuz olduğunu bildiğimiz fakat Rust’ın bunu anlayamadığı durumlarda güvenli olmayan koda başvurma zamanı gelmiştir.

Liste 20-6, split_at_mut uygulamasının çalışmasını sağlamak için bir unsafe bloğunun, bir ham işaretçinin ve güvenli olmayan fonksiyonlara yapılan bazı çağrıların nasıl kullanılacağını göstermektedir.

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);
}
Liste 20-6: split_at_mut fonksiyonunun uygulanışında güvenli olmayan kod kullanmak

Bölüm 4’teki “Dilim (Slice) Türü” bölümünden, bir dilimin bazı verilere yönelen bir işaretçi ve dilimin uzunluğundan oluştuğunu hatırlayın. Bir dilimin uzunluğunu almak için len metodunu, dilimin ham işaretçisine erişmek içinse as_mut_ptr metodunu kullanırız. Bu örnekte i32 değerlerine ait değiştirilebilir bir dilimimiz olduğundan as_mut_ptr, *mut i32 türünde bir ham işaretçi döndürür ve biz de bunu ptr değişkeninde saklarız.

mid indeksinin dilim içinde kaldığını doğrulayan kontrolü koruruz. Ardından güvenli olmayan koda geçeriz: slice::from_raw_parts_mut fonksiyonu bir ham işaretçi ve bir uzunluk alır, bunlardan bir dilim oluşturur. Bu fonksiyonu ptr adresinden başlayan ve mid öğe uzunluğunda olan bir dilim oluşturmak için kullanırız. Sonra ptr üzerinde mid argümanıyla add metodunu çağırarak mid konumundan başlayan bir ham işaretçi elde ederiz; bu işaretçiyi ve uzunluk olarak mid’den sonra kalan öğe sayısını (len - mid) kullanarak ikinci dilimi oluştururuz.

slice::from_raw_parts_mut fonksiyonu güvenli değildir; çünkü bir ham işaretçi alır ve bu işaretçinin geçerli olduğuna güvenmek zorundadır. Ham işaretçiler üzerindeki add metodu da kaydırılan konumun geçerli bir işaretçi olduğuna güvenmek zorunda olduğu için güvenli değildir. Bu nedenle slice::from_raw_parts_mut ve add çağrılarımızı gerçekleştirebilmek için etraflarına bir unsafe bloğu koymamız gerekti. Koda bakarak ve mid değerinin len değerinden küçük veya ona eşit olmasını şart koşan doğrulamayı ekleyerek, unsafe bloğu içinde kullanılan tüm ham işaretçilerin dilim içindeki verilere işaret eden geçerli işaretçiler olacağını söyleyebiliriz. Bu, unsafe için kabul edilebilir ve yerinde bir kullanımdır.

Ortaya çıkan split_at_mut fonksiyonunu unsafe olarak işaretlememiz gerekmediğine ve bu fonksiyonu güvenli Rust kodundan çağırabildiğimize dikkat edin. unsafe kodu güvenli bir biçimde kullanan bir fonksiyon uygulamasıyla güvenli olmayan kod üzerine güvenli bir soyutlama oluşturmuş olduk; bu uygulama güvenlidir, çünkü yalnızca bu fonksiyonun erişebildiği verilerden geçerli işaretçiler üretir.

Buna karşılık, Liste 20-7’deki slice::from_raw_parts_mut kullanımı dilim kullanıldığında büyük olasılıkla çökecektir. Bu kod keyfi bir bellek konumu alır ve 10.000 öğe uzunluğunda bir dilim oluşturur.

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) };
}
Liste 20-7: Keyfi bir bellek konumundan dilim oluşturmak

Bu keyfi konumdaki belleğin sahibi değiliz ve bu kodun oluşturduğu dilimin geçerli i32 değerleri içerdiğine dair hiçbir garanti yoktur. Geçerli bellek koşulları sağlanmıyorsa slice::from_raw_parts_mut çağrısının kendisi tanımsız davranıştır; dilimin elemanlarına sonradan erişmemek bu çağrıyı güvenli kılmaz.

Dış Kodu Çağırmak İçin extern Fonksiyonlarını Kullanmak

Bazen Rust kodunuzun başka bir dilde yazılmış kodlarla etkileşim kurması gerekebilir. Bunun için Rust, bir Yabancı Fonksiyon Arayüzü (Foreign Function Interface — FFI) oluşturmayı ve kullanmayı kolaylaştıran extern anahtar kelimesine sahiptir. FFI, bir programlama dilinin fonksiyonlar tanımlamasına ve farklı (yabancı) bir programlama dilinin bu fonksiyonları çağırmasına olanak tanıyan bir yöntemdir.

Liste 20-8, C standart kütüphanesindeki abs fonksiyonuyla nasıl entegrasyon kurulacağını göstermektedir. extern blokları içinde bildirilen fonksiyonları Rust kodundan çağırmak genellikle güvenli değildir; bu nedenle extern blokları da unsafe olarak işaretlenmelidir. Bunun nedeni, diğer dillerin Rust’ın kurallarını ve garantilerini zorunlu kılmaması ve Rust’ın bunları denetleyememesidir; dolayısıyla güvenliği sağlama sorumluluğu programcıya düşer.

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

fn main() {
    unsafe {
        println!("C'ye göre -3'ün mutlak değeri: {}", abs(-3));
    }
}
Liste 20-8: Başka bir dilde tanımlanmış bir extern fonksiyonunu bildirmek ve çağırmak

unsafe extern "C" bloğu içinde, başka bir dilden çağırmak istediğimiz dış fonksiyonların adlarını ve imzalarını listeleriz. "C" kısmı, dış fonksiyonun hangi uygulama ikili arayüzünü (application binary interface — ABI) kullandığını tanımlar: ABI, fonksiyonun assembly düzeyinde nasıl çağrılacağını belirler. "C" ABI’si en yaygın olanıdır ve C programlama dilinin ABI’sini takip eder. Rust’ın desteklediği tüm ABI’ler hakkındaki bilgilere Rust Referansı’ndan ulaşabilirsiniz.

Bir unsafe extern bloğu içinde bildirilen her öğe örtük olarak unsafe kabul edilir. Bununla birlikte, bazı FFI fonksiyonlarını çağırmak gerçekten güvenlidir. Örneğin C standart kütüphanesindeki abs fonksiyonunun bellek güvenliğiyle ilgili herhangi bir çekincesi yoktur ve herhangi bir i32 değeriyle çağrılabileceğini biliriz. Bu gibi durumlarda, bir unsafe extern bloğu içinde yer alsa bile bu belirli fonksiyonu çağırmanın güvenli olduğunu ifade etmek için safe anahtar kelimesini kullanabiliriz. Bu değişikliği yaptığımızda, Liste 20-9’da gösterildiği gibi onu çağırmak artık bir unsafe bloğu gerektirmez.

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

fn main() {
    println!("C'ye göre -3'ün mutlak değeri: {}", abs(-3));
}
Liste 20-9: Bir unsafe extern bloğu içindeki fonksiyonu açıkça safe olarak işaretlemek ve güvenle çağırmak

Bir fonksiyonu safe olarak işaretlemek onu kendiliğinden güvenli hâle getirmez! Bunun yerine, Rust’a o fonksiyonun güvenli olduğuna dair verdiğiniz bir söz gibidir. Bu sözün tutulduğundan emin olmak yine sizin sorumluluğunuzdadır!

Rust Fonksiyonlarını Başka Dillerden Çağırmak

Diğer dillerin Rust fonksiyonlarını çağırmasına olanak tanıyan bir arayüz oluşturmak için de extern kullanabiliriz. Bütün bir extern bloğu oluşturmak yerine, ilgili fonksiyonun fn anahtar kelimesinden hemen önce extern anahtar kelimesini ekler ve kullanılacak ABI’yi belirtiriz. Ayrıca Rust derleyicisine bu fonksiyonun adını dönüştürmemesini (mangle etmemesini) söylemek için #[unsafe(no_mangle)] özniteliğini eklememiz gerekir. Ad dönüştürme (mangling), derleyicinin bir fonksiyona verdiğimiz adı, derleme sürecinin diğer aşamalarının kullanabileceği daha fazla bilgi içeren ancak insanlar tarafından okunması daha zor olan farklı bir ada dönüştürmesidir. Her programlama dilinin derleyicisi adları biraz farklı biçimde dönüştürür; bu nedenle bir Rust fonksiyonunun diğer diller tarafından adıyla çağrılabilmesi için Rust derleyicisinin ad dönüştürme işlemini devre dışı bırakmamız gerekir. Yerleşik ad dönüştürmesi olmadığında kütüphaneler arasında ad çakışmaları yaşanabileceğinden bu işlem güvenli değildir; dolayısıyla seçtiğimiz adın dönüştürülmeden dışa aktarılmasının güvenli olduğundan emin olmak bizim sorumluluğumuzdadır.

Aşağıdaki örnekte, paylaşılan bir kütüphane olarak derlenip C tarafından bağlandıktan sonra call_from_c fonksiyonunu C kodundan erişilebilir hâle getiriyoruz:

#![allow(unused)]
fn main() {
#[unsafe(no_mangle)]
pub extern "C" fn call_from_c() {
    println!("Az önce C'den bir Rust fonksiyonu çağrıldı!");
}
}

extern anahtar kelimesinin bu kullanımı, extern bloğu üzerinde değil yalnızca öznitelik içinde unsafe gerektirir.

Değiştirilebilir Bir Statik Değişkene Erişmek veya Onu Değiştirmek

Bu kitapta şimdiye dek küresel değişkenlerden bahsetmedik; Rust bunları destekler ancak Rust’ın sahiplik kurallarıyla birlikte sorun yaratabilirler. İki thread aynı değiştirilebilir küresel değişkene erişiyorsa bu durum bir veri yarışına yol açabilir.

Rust’ta küresel değişkenlere statik (static) değişkenler denir. Liste 20-10, değeri bir string dilimi olan statik bir değişkenin bildirim ve kullanım örneğini göstermektedir.

Dosya adı: src/main.rs
static HELLO_WORLD: &str = "Merhaba, dünya!";

fn main() {
    println!("değer: {HELLO_WORLD}");
}
Liste 20-10: Değiştirilemez bir statik değişkeni tanımlamak ve kullanmak

Statik değişkenler, Bölüm 3’teki “Sabitleri Bildirmek” bölümünde ele aldığımız sabitlere benzer. Gelenek gereği statik değişkenlerin adları SCREAMING_SNAKE_CASE biçiminde yazılır. Statik değişkenler yalnızca 'static ömrüne sahip referansları saklayabilir; bu da Rust derleyicisinin ömrü kendisinin çıkarabileceği ve bizim bunu açıkça belirtmemize gerek olmadığı anlamına gelir. Değiştirilemez bir statik değişkene erişmek güvenlidir.

Sabitler ile değiştirilemez statik değişkenler arasındaki ince bir fark, statik değişkendeki değerlerin bellekte sabit bir adrese sahip olmasıdır. Bu değeri kullanmak her zaman aynı veriye erişecektir. Öte yandan sabitlerin, her kullanıldıklarında verilerini çoğaltmasına izin verilir. Bir diğer fark ise statik değişkenlerin değiştirilebilir olabilmesidir. Değiştirilebilir statik değişkenlere erişmek ve onları değiştirmek güvenli değildir. Liste 20-11, COUNTER adlı değiştirilebilir bir statik değişkenin nasıl bildirileceğini, ona nasıl erişileceğini ve nasıl değiştirileceğini göstermektedir.

Dosya adı: src/main.rs
static mut COUNTER: u32 = 0;

/// SAFETY: Bu fonksiyonu aynı anda birden fazla thread'den çağırmak tanımsız
/// davranıştır; bu yüzden aynı anda *yalnızca* tek bir thread'den çağırdığınızı
/// garanti etmelisiniz.
unsafe fn add_to_count(inc: u32) {
    unsafe {
        COUNTER += inc;
    }
}

fn main() {
    unsafe {
        // SAFETY: Bu fonksiyon yalnızca `main` içindeki tek thread'den çağrılıyor.
        add_to_count(3);
        println!("COUNTER: {}", *(&raw const COUNTER));
    }
}
Liste 20-11: Değiştirilebilir bir statik değişkenden okumak veya ona yazmak güvenli değildir.

Sıradan değişkenlerde olduğu gibi değiştirilebilirliği mut anahtar kelimesiyle belirtiriz. COUNTER değişkeninden okuma veya ona yazma yapan her kod bir unsafe bloğu içinde yer almalıdır. Liste 20-11’deki kod tek thread’li olduğu için derlenir ve beklediğimiz gibi COUNTER: 3 çıktısını verir. COUNTER’a senkronizasyon olmadan eşzamanlı erişilir ve erişimlerden en az biri yazma yaparsa veri yarışı, dolayısıyla tanımsız davranış oluşur. Sadece birden fazla thread bulunması tek başına tanımsız davranış değildir. Bu nedenle, fonksiyonu çağıran herkesin güvenli bir şekilde neleri yapıp neleri yapamayacağını bilmesi için fonksiyonun tamamını unsafe olarak işaretlememiz ve güvenlik kısıtlamasını belgelememiz gerekir.

Güvenli olmayan bir fonksiyonun belgelendirmesinde # Safety başlığı altında, çağıran kodun sağlaması gereken ön koşulları açıklamak yaygın bir Rust kullanım biçimidir. unsafe bloğunun yanındaki // SAFETY: yorumu ise bu çağrı noktasında söz konusu koşulların nasıl sağlandığını açıklar. Böylece fonksiyonun sözleşmesi ile belirli bir kullanımın gerekçesi birbirinden ayrılır.

Ayrıca derleyici, değiştirilebilir bir statik değişkene referans oluşturma girişimlerini bir derleyici denetimi (lint) aracılığıyla varsayılan olarak reddeder. Ya #[allow(static_mut_refs)] özniteliğini ekleyerek bu lint korumasından açıkça vazgeçmeniz ya da değiştirilebilir statik değişkene ham ödünç alma operatörlerinden biriyle oluşturulmuş bir ham işaretçi üzerinden erişmeniz gerekir. Bu kural, bu listedeki println! kullanımında olduğu gibi referansın görünmez biçimde oluşturulduğu durumları da kapsar. Ham işaretçi kullanmak, ara bir referans oluşturmadan erişim sağlamamıza ve güvenlik sorumluluğunu açıkça göstermemize yardımcı olur. Ham işaretçiden sonradan bir referans oluşturulursa o referansın bütün geçerlilik ve ödünç alma kuralları yine sağlanmalıdır; lint’i susturmak bu yükümlülüğü kaldırmaz.

Küresel olarak erişilebilen değiştirilebilir verilerde veri yarışı olmadığından emin olmak zordur; Rust’ın değiştirilebilir statik değişkenleri güvenli olmayan kabul etmesinin nedeni budur. Mümkün olan her durumda, farklı thread’lerden yapılan veri erişiminin güvenli bir şekilde gerçekleştirildiğini derleyicinin denetleyebilmesi için Bölüm 16’da ele aldığımız eşzamanlılık tekniklerini ve thread güvenli akıllı işaretçileri kullanmak tercih edilmelidir.

Not: Rust 2024 Sürümü ve Güvenli Olmayan Kod Sıkılaştırmaları: Eski ders kaynaklarında veya Rust 2021 ve öncesi sürümlerle yazılmış crate’lerde farklı unsafe sözdizimleriyle karşılaşabilirsiniz. Rust 2024 Sürümü (Ek E: Sürümler), güvenlik sorumluluklarını kod üzerinde çok daha açık ve görünür kılmak amacıyla önemli iyileştirmeler getirmiştir:

  • Ham ödünç alma operatörleri (&raw const ve &raw mut): Eski kodlarda ham işaretçi oluşturmak için kullanılan &num as *const i32 veya std::ptr::addr_of!(num) yerine artık doğrudan &raw const num ve &raw mut num operatörleri kullanılır (bu operatörler Rust 1.82 ile kararlı hâle gelmiştir ve yalnızca 2024 Sürümü’ne özgü değildir). Bu operatörler ara aşamada geçici bir referans oluşturmadığı için hizalanmamış (unaligned) alanlara veya static mut değişkenlere tanımsız davranış riski olmadan işaretçi üretebilir.
  • unsafe fn gövdesinde açık unsafe bloğu: Eskiden bir fonksiyon unsafe fn olarak işaretlendiğinde tüm gövdesi örtük bir unsafe bloğu sayılıyordu. Rust 2024 ile birlikte, Liste 20-11’deki add_to_count fonksiyonunda gördüğünüz gibi unsafe fn içinde dahi güvenli olmayan işlemleri dar kapsamlı unsafe { ... } blokları içine almanız gerekir (aksi hâlde derleyici uyarı verir).
  • unsafe extern ve safe fn: Önceki sürümlerde yalnızca extern "C" { ... } şeklinde yazılan bloklar, Rust 2024’te dış imzaların doğruluğunu garanti etme sorumluluğunu vurgulamak için unsafe extern "C" { ... } olarak yazılır; buna karşılık her geçerli girdiyle çağrılması güvenli olan dış fonksiyonlar blok içinde safe fn ile işaretlenebilir.
  • Güvenli olmayan öznitelikler (#[unsafe(...)]): Sembol çakışmalarına yol açabilecek #[no_mangle] gibi öznitelikler artık güvenlik sözleşmesini açıkça göstermek üzere #[unsafe(no_mangle)] biçiminde yazılır.
  • static_mut_refs lint engeli: Değiştirilebilir statik değişkenlere doğrudan veya println!("{COUNTER}") gibi makrolar aracılığıyla örtük referans oluşturulması derleyici tarafından varsayılan olarak reddedilir; Liste 20-11’deki *(&raw const COUNTER) ifadesinde olduğu gibi ham işaretçi kullanılması gerekir.

Sürümler arasındaki bu geçişlerin ayrıntılarını Rust Sürüm Rehberi üzerinden inceleyebilirsiniz.

Güvenli Olmayan Bir Trait Uygulamak

Güvenli olmayan bir trait uygulamak için unsafe kullanabiliriz. Metotlarından en az biri derleyicinin doğrulayamadığı bir değişmeze (invariant) sahip olduğunda o trait güvenli değildir. Bir trait’in unsafe olduğunu, Liste 20-12’de gösterildiği gibi trait öncesine unsafe anahtar kelimesini ekleyerek ve trait uygulamasını da unsafe olarak işaretleyerek bildiririz.

unsafe trait Foo {
    // metotlar buraya gelir
}

unsafe impl Foo for i32 {
    // metot uygulamaları buraya gelir
}

fn main() {}
Liste 20-12: Güvenli olmayan bir trait tanımlamak ve uygulamak

unsafe impl kullanarak, derleyicinin doğrulayamadığı değişmezleri koruyacağımıza söz vermiş oluruz.

Örnek olarak, Bölüm 16’daki “Send ve Sync ile Genişletilebilir Eşzamanlılık” bölümünde ele aldığımız Send ve Sync işaretleyici trait’lerini hatırlayın: Türlerimiz tamamen Send ve Sync uygulayan diğer türlerden oluşuyorsa derleyici bu trait’leri otomatik olarak uygular. Ham işaretçiler gibi Send veya Sync uygulamayan bir tür içeren bir tür tanımlarsak ve bu türü Send ya da Sync olarak işaretlemek istersek unsafe kullanmamız gerekir. Rust, türümüzün thread’ler arasında güvenle gönderilebilme veya birden fazla thread’den güvenle erişilebilme garantilerini koruduğunu doğrulayamaz; bu nedenle bu denetimleri elle yapmamız ve bunu unsafe ile belirtmemiz gerekir.

union Alanlarına Erişmek

Yalnızca unsafe ile çalışan son işlem, bir birleşimin (union) alanlarına erişmektir. Bir union, struct’a benzer; ancak belirli bir örnekte herhangi bir anda bildirilen alanlardan yalnızca biri kullanılır. union’lar öncelikle C kodundaki union’larla etkileşim kurmak amacıyla kullanılır. Rust, union örneğinde o anda saklanan verinin türünü garanti edemediği için union alanlarına erişmek güvenli değildir. union’lar hakkında daha fazla bilgiyi Rust Referansı’nda bulabilirsiniz.

Not: Bu örnekte #[repr(C)] kullanan bir union, bütün alanlarını aynı başlangıç adresinde tutar. Boyutu, en büyük alanı barındıracak kadar büyük olmalı ve hizalama koşulunu da sağlamalıdır; bu yüzden dolgu baytları nedeniyle en büyük alanın boyutundan daha büyük olabilir. Hangi alana en son yazıldığını yalnızca siz bilirsiniz; Rust bunu takip etmez. Bu yüzden bir alana yazmak güvenliyken bir alanı okumak unsafe gerektirir:

#[repr(C)]
union TamSayiVeyaKayan {
    tam: u32,
    kayan: f32,
}

fn main() {
    let mut deger = TamSayiVeyaKayan { tam: 1 };
    deger.kayan = 1.0; // Yazmak güvenlidir; artık `kayan` alanı geçerlidir.

    // SAFETY: En son `kayan` alanına yazdık; dolayısıyla okuduğumuz bitler
    // geçerli bir `f32` değeridir.
    let okunan = unsafe { deger.kayan };
    println!("{okunan}");
}

Bir union’ın alanları yalnızca Copy olan türlerden (ya da ManuallyDrop<T> ile sarmalanmış türlerden) oluşabilir; ayrıntılar için Rust Referansı’na bakın.

Güvenli Olmayan Kodu Denetlemek İçin Miri Kullanmak

Güvenli olmayan kod yazarken, yazdığınız kodun gerçekten güvenli ve doğru olup olmadığını denetlemek isteyebilirsiniz. Bunu yapmanın en iyi yollarından biri, tanımsız davranışları tespit etmeye yönelik resmî bir Rust aracı olan Miri’yi kullanmaktır. Ödünç alma denetleyicisi derleme zamanında çalışan statik bir araçken, Miri çalışma zamanında çalışan dinamik bir araçtır. Programınızı veya test paketini çalıştırarak ve Rust’ın nasıl çalışması gerektiğine dair bildiği kuralları ihlal ettiğiniz anları yakalayarak kodunuzu denetler.

Miri’yi kullanmak Rust’ın nightly (gecelik) sürümünü gerektirir (bunu Ek G: Rust Nasıl Geliştirilir ve “Nightly Rust” bölümünde daha ayrıntılı ele alıyoruz). rustup +nightly component add miri komutunu yazarak hem Rust’ın nightly sürümünü hem de Miri aracını kurabilirsiniz. Bu işlem projenizin kullandığı Rust sürümünü değiştirmez; yalnızca istediğiniz zaman kullanabilmeniz için aracı sisteminize ekler. Bir proje üzerinde Miri’yi cargo +nightly miri run veya cargo +nightly miri test yazarak çalıştırabilirsiniz.

Bunun ne kadar faydalı olabileceğine bir örnek olarak, Miri’yi Liste 20-7 üzerinde çalıştırdığımızda neler olduğuna bakalım.

$ cargo +nightly miri run
   Compiling guvensiz-ornek v0.1.0 (file:///projects/guvensiz-ornek)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.17s
     Running `file:///home/.rustup/toolchains/nightly/bin/cargo-miri runner target/miri/debug/guvensiz-ornek`
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

error: Undefined Behavior: constructing invalid value of type &mut [i32]: encountered a dangling reference (0x1234[noalloc] 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: 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 haklı olarak bizi bir tam sayıyı işaretçiye dönüştürdüğümüz konusunda uyarır (integer-to-pointer cast); bu bir sorun olabilir, ancak Miri işaretçinin nasıl ortaya çıktığını (köken / provenance bilgisini) bilmediği için ortada bir sorun olup olmadığını belirleyemez. Ardından Miri, sarkan bir referansla karşılaşıldığı (encountered a dangling reference — 0x1234 adresine ait bir bellek tahsisi ve köken bilgisi olmadığı) için Liste 20-7’nin tanımsız davranış içerdiği noktada bir hata döndürür. Miri sayesinde artık tanımsız davranış riski bulunduğunu biliyoruz ve kodu nasıl güvenli hâle getireceğimiz üzerine düşünebiliriz. Hatta bazı durumlarda Miri, hataların nasıl düzeltilebileceğine dair önerilerde bile bulunabilir.

Miri, güvenli olmayan kod yazarken yapabileceğiniz her hatayı yakalayamaz. Miri dinamik bir analiz aracı olduğundan yalnızca gerçekte çalıştırılan kodlardaki sorunları yakalar. Bu da yazdığınız güvenli olmayan koda duyduğunuz güveni artırmak için onu iyi test teknikleriyle birlikte kullanmanız gerektiği anlamına gelir. Ayrıca Miri, kodunuzun sağlam olmayan (unsound) —yani güvenli koddan çağrıldığında bile tanımsız davranışa yol açabilen— her olası yolunu kapsamaz.

Başka bir deyişle: Miri bir sorun yakalarsa ortada bir hata (bug) olduğunu kesin olarak bilirsiniz; ancak Miri’nin bir hata yakalamaması, ortada hiçbir sorun olmadığı anlamına gelmez. Yine de pek çok hatayı yakalayabilir. Onu bu bölümdeki diğer güvenli olmayan kod örnekleri üzerinde çalıştırmayı deneyin ve neler söylediğine bakın!

Miri hakkında daha fazla bilgiyi GitHub deposunda bulabilirsiniz.

Güvenli Olmayan Kodu Doğru Kullanmak

Az önce ele aldığımız beş süper güçten birini kullanmak amacıyla unsafe yazmak yanlış değildir, hatta hoş karşılanmayan bir durum bile sayılmaz; ancak derleyici bellek güvenliğini korumaya yardımcı olamadığı için unsafe kodu doğru yazmak daha inceliklidir. unsafe kod kullanmak için geçerli bir nedeniniz olduğunda bunu yapabilirsiniz; üstelik açık unsafe işaretlemesinin bulunması, bir sorun ortaya çıktığında kaynağının izini sürmeyi kolaylaştırır. Güvenli olmayan kod yazdığınız her seferde, yazdığınız kodun Rust’ın kurallarını koruduğundan daha emin olmanıza yardımcı olması için Miri’yi kullanabilirsiniz.

Güvenli olmayan Rust ile etkili bir şekilde nasıl çalışılacağını çok daha derinlemesine keşfetmek için Rust’ın resmî unsafe rehberi olan The Rustonomicon kitabını okuyabilirsiniz.