unsafe anahtar kelimesi Rust'ta beş ek işleme izin verir: ham işaretçinin gösterdiği değeri okumak, unsafe bir fonksiyon veya metodu çağırmak, değiştirilebilir bir static değişkene erişmek, unsafe bir trait'i uygulamak ve union alanlarını okumak. Bunun dışında hiçbir şey değişmez: unsafe blok borrow checker'ı kapatmaz, tip denetimini de devre dışı bırakmaz. Yalnızca derleyicinin doğrulayamadığı bu beş işlemin sorumluluğunu size devreder.

Bu yazıda bu beş yeteneği çalışan örneklerle inceliyoruz, ardından güvenli bir soyutlama kurmayı, tanımsız davranışın Rust'taki sınırlarını ve Rust 2024 edition'ın unsafe kodla ilgili getirdiği kuralları ele alıyoruz. Ownership ve borrow kurallarını henüz oturtmadıysanız önce ownership ve referans ve borrowing yazılarını okumanızı öneririz; unsafe kodun doğru olması tam olarak bu kuralları elle korumanıza bağlıdır.

unsafe neden var?

unsafe neden var? Ham işaretçiyi dereference etmek, unsafe fn çağırmak, static mut okumak veya yazmak, unsafe trait uygulamak

Kitap iki gerekçe verir. Birincisi, statik analiz doğası gereği tutucudur: derleyici bir programın kuralları koruduğundan emin olamadığında, geçersiz bir programı kabul etmektense geçerli bir programı reddetmeyi tercih eder. Kodunuzun doğru olduğunu bildiğiniz ama derleyicinin bunu kanıtlayamadığı durumlarda unsafe devreye girer. İkincisi, altta yatan donanım zaten güvensizdir. İşletim sistemiyle doğrudan konuşmak, başka dillerdeki kodu çağırmak ya da düşük seviyeli sistem programlaması yapmak için bu kapının açık olması gerekir.

Rustonomicon bu ayrımı güvenli Rust ve güvensiz Rust diye adlandırır. Güvenli Rust'ın temel özelliği, ne yaparsanız yapın tanımsız davranışa yol açamamasıdır. unsafe ise bu garantiyi sizin sağlamanızı ister. Kitabın önerisi açıktır: unsafe blokları küçük tutun; bellek hatası araştırırken sorunun bu bloklardan birinde olduğunu bilmek işinizi çok kolaylaştırır.

YetenekÖrnekNeden derleyici denetleyemez?
Ham işaretçiyi dereference etmek*pİşaretçinin geçerli belleği gösterdiği bilinmez
unsafe fn çağırmakslice::from_raw_parts_mutFonksiyonun ön koşullarını çağıran sağlar
static mut okumak veya yazmakSAYAC += 1Eşzamanlı erişim veri yarışına yol açabilir
unsafe trait uygulamakunsafe impl SendTrait değişmezlerini derleyici doğrulayamaz
union alanı okumaku.tamO anda hangi alanın geçerli olduğu bilinmez

Ham işaretçiler: *const T ve *mut T

Ham işaretçiler referanslara benzer ama referansların garantilerini taşımaz. Kitaba göre ham işaretçiler aynı konuma hem değiştirilemez hem değiştirilebilir işaretçi tutarak borrow kurallarını yok sayabilir, geçerli belleği gösterdikleri garanti edilmez, null olabilir ve otomatik temizlik yapmaz. Tip adındaki yıldız dereference operatörü değil, tip adının parçasıdır. Ham işaretçi oluşturmak güvenli koddur; tehlikeli olan yalnızca gösterdiği değere erişmektir.

rust
fn main() {
    let mut sayi = 5;
    let r1 = &raw const sayi;
    let r2 = &raw mut sayi;

    unsafe {
        *r2 += 10;
        println!("r1: {}", *r1);
    }
    println!("boş mu: {}", r1.is_null());

    let bos: *const i32 = std::ptr::null();
    println!("null mu: {}", bos.is_null());
    println!("as_ref: {:?}", unsafe { bos.as_ref() });
}
// çıktı:
// r1: 15
// boş mu: false
// null mu: true
// as_ref: None

&raw const ve &raw mut ham ödünç alma operatörleridir ve doğrudan bir yerel değişkenden işaretçi ürettikleri için bu örnekteki işaretçiler geçerlidir. Aynı konuma hem okuma hem yazma işaretçisi tuttuğumuz halde derleme hatası almadık; referanslarla bu kod derlenmezdi. Kitap tam da bu yüzden dikkatli olunmasını ister, çünkü değiştirilebilir işaretçi üzerinden yazma bir veri yarışına kapı açabilir. Bir tamsayıyı as ile işaretçiye çevirip keyfi bir adres oluşturmak da mümkündür, ancak o adresi kullanmak tanımsız davranıştır. as_ref null işaretçide None döndürdüğü için null kontrolünü güvenli biçimde yapmanın bir yoludur, yine de unsafe içinde çağrılır çünkü null olmayan işaretçinin geçerli olduğunu varsayar.

unsafe fn ve unsafe blok

Bir fonksiyonun önüne unsafe yazmak, onu çağıranın yerine getirmesi gereken ve derleyicinin denetleyemediği koşullar olduğunu bildirir. Böyle bir fonksiyonu yalnızca unsafe blok içinden çağırabilirsiniz; blok, fonksiyonun belgesini okuduğunuzu ve sözleşmesine uyduğunuzu beyan etmenin yoludur.

rust
/// # Safety
/// `p` geçerli ve hizalı bir `i32` göstermelidir.
unsafe fn oku(p: *const i32) -> i32 {
    unsafe { *p }
}

fn main() {
    let x = 42;
    let deger = oku(&raw const x);
    println!("{deger}");
}
// error[E0133]: call to unsafe function `oku` is unsafe and requires unsafe block

Çağrıyı let deger = unsafe { oku(&raw const x) }; biçiminde yazdığınızda kod derlenir ve 42 yazdırır. Fonksiyon gövdesindeki unsafe { *p } bloğuna da dikkat edin. Rust 2024 edition'da unsafe_op_in_unsafe_fn lint'i varsayılan olarak uyarı verir: unsafe fn gövdesindeki güvensiz işlemler de ayrıca bir unsafe blok içine alınmalıdır. Edition Guide bunun gerekçesini, unsafe kelimesinin iki rolü olmasıyla açıklar: fonksiyonu çağırmanın ön koşul taşıdığını bildirmek ve gövdede güvensiz işlemlere izin vermek. İkinci rol, açık blok olmadan fazla riskli bulunmuştur.

rust
unsafe fn ilk<T>(x: &[T]) -> &T {
    x.get_unchecked(0)
}

fn main() {
    let v = [7, 8];
    println!("{}", unsafe { ilk(&v) });
}
// warning[E0133]: call to unsafe function `core::slice::<impl [T]>::get_unchecked` is unsafe and requires unsafe block
// çıktı:
// 7

Kod uyarıyla derlenir ve çalışır; uyarıyı gidermek için gövdeyi unsafe { x.get_unchecked(0) } olarak yazmanız yeterlidir. Belgelerde ve kitapta kullanılan bir gelenek de şudur: unsafe bir fonksiyonun belgesine # Safety başlıklı bir bölüm ekleyip çağıranın neyi sağlaması gerektiğini yazmak, her unsafe bloğun önüne de SAFETY ile başlayan bir yorumla kuralların nasıl korunduğunu açıklamak.

Güvenli soyutlama: split_at_mut örneği

Güvenli soyutlama: split_at_mut örneği

Bir fonksiyonun içinde unsafe kod olması, fonksiyonun kendisinin unsafe olması gerektiği anlamına gelmez. Standart kütüphanenin büyük kısmı denetlenmiş güvensiz kodun üzerine kurulmuş güvenli soyutlamalardır. Kitabın örneği bir dilimi iki değiştirilebilir parçaya bölen split_at_mut fonksiyonudur. Yalnızca güvenli Rust ile yazmaya çalışınca derleyici reddeder:

rust
fn ikiye_bol(degerler: &mut [i32], orta: usize) -> (&mut [i32], &mut [i32]) {
    let uzunluk = degerler.len();
    assert!(orta <= uzunluk);
    (&mut degerler[..orta], &mut degerler[orta..])
}
// error[E0499]: cannot borrow `*degerler` as mutable more than once at a time

Borrow checker aynı dilimin örtüşmeyen iki parçasını ödünç aldığımızı anlayamaz; yalnızca aynı dilimden iki kez değiştirilebilir ödünç alındığını görür. Bu hata ve benzerleri E0499 ve E0502 yazısında ayrıntılı anlatılıyor. Kodun doğru olduğunu biz biliyoruz, derleyici bilmiyor; işte unsafe tam bu durum için var:

rust
use std::slice;

fn ikiye_bol(degerler: &mut [i32], orta: usize) -> (&mut [i32], &mut [i32]) {
    let uzunluk = degerler.len();
    let ptr = degerler.as_mut_ptr();
    assert!(orta <= uzunluk);
    // SAFETY: orta <= uzunluk denetlendi; iki dilim örtüşmüyor ve
    // ikisi de `degerler` ile aynı ayırmanın içinde kalıyor.
    unsafe {
        (
            slice::from_raw_parts_mut(ptr, orta),
            slice::from_raw_parts_mut(ptr.add(orta), uzunluk - orta),
        )
    }
}

fn main() {
    let mut v = vec![1, 2, 3, 4, 5, 6];
    let (sol, sag) = ikiye_bol(&mut v, 2);
    sol[0] = 100;
    sag[0] = 300;
    println!("{v:?}");
}
// çıktı:
// [100, 2, 300, 4, 5, 6]

slice::from_raw_parts_mut bir ham işaretçi ve uzunluktan dilim oluşturur; işaretçinin geçerli olduğuna güvenmek zorunda olduğu için unsafe bir fonksiyondur. add metodu da kaydırılmış konumun geçerli olduğuna güvenir. assert! satırı sayesinde bloğa giren her işaretçinin dilimin içinde kaldığını biliyoruz, bu yüzden ikiye_bol fonksiyonunu unsafe işaretlemeye gerek yok ve güvenli koddan çağrılabilir. Gerçek projede elbette standart kütüphanedeki hazır v.split_at_mut(2) çağrısını kullanırsınız; aynı sonucu verir.

Rustonomicon bu noktada önemli bir uyarı yapar: güvenlik yerel değildir. Sınır denetimi yapan küçük bir fonksiyonda < karşılaştırmasını <= yapmak, yani yalnızca güvenli koddaki tek bir karakteri değiştirmek, unsafe bloğu dokunulmamış olsa bile fonksiyonu sağlam (sound) olmaktan çıkarır. Yukarıdaki örnekte assert! satırını silmek de aynı etkiyi yapar. Bu yüzden unsafe kodu incelerken bloğun kendisine değil, onun dayandığı tüm durumu kuran güvenli koda da bakmalısınız.

extern ile başka dillerdeki kodu çağırmak

Başka bir dilde yazılmış fonksiyonları çağırmak için extern bloğu kullanılır; bu yabancı fonksiyon arayüzü (FFI) demektir. "C" ifadesi fonksiyonun kullandığı uygulama ikili arayüzünü (ABI) belirtir. Rust 2024 edition'da extern blokları unsafe extern olarak işaretlenmek zorundadır; işaretsiz bir blok error: extern blocks must be unsafe hatası verir. Edition Guide'a göre bu, imzaların doğru olmasının bloğu yazanın sorumluluğu olduğunu vurgular; yanlış bir imza tanımsız davranışa yol açabilir.

rust
unsafe extern "C" {
    safe fn abs(girdi: i32) -> i32;
    fn strlen(s: *const std::ffi::c_char) -> usize;
}

fn main() {
    println!("abs(-3) = {}", abs(-3));
    let metin = c"merhaba";
    // SAFETY: `metin` NUL ile biten geçerli bir C dizgesi.
    let n = unsafe { strlen(metin.as_ptr()) };
    println!("strlen = {n}");
}
// çıktı:
// abs(-3) = 3
// strlen = 7

Blok içindeki öğeler varsayılan olarak unsafe kabul edilir. C'nin abs fonksiyonu her i32 için güvenle çağrılabildiğinden onu safe ile işaretledik ve unsafe blok olmadan çağırdık. Kitap burada önemli bir hatırlatma yapar: safe yazmak fonksiyonu güvenli yapmaz, bu Rust'a verdiğiniz bir sözdür. strlen ise geçerli bir işaretçi istediği için unsafe kaldı. Ters yönde, Rust fonksiyonlarını başka dillere açmak için pub extern "C" fn tanımlayıp ad karıştırmayı (mangling) #[unsafe(no_mangle)] ile kapatırsınız. 2024 edition'da no_mangle, export_name ve link_section özniteliklerinin unsafe(...) içinde yazılması zorunludur; aksi halde error: unsafe attribute used without unsafe hatası alınır. Gerekçe, karıştırılmamış sembol adlarının tüm bağlanan kütüphanelerde ortak bir ad alanında çakışabilmesidir.

static mut ve daha güvenli alternatifler

Rust'ta küresel değişkenler static olarak tanımlanır. Değiştirilemez bir static okumak güvenlidir, ama static mut değişkene her erişim unsafe gerektirir, çünkü birden fazla iş parçacığı aynı anda erişirse veri yarışı oluşur. Rust 2024 edition'da bir adım daha atıldı: static_mut_refs lint'i varsayılan olarak deny düzeyinde ve static mut değişkene referans oluşturmayı reddediyor. Bu referans görünmez biçimde de oluşabilir; örneğin println! argümanı referans aldığı için şu kod derlenmez:

rust
static mut SAYAC: u32 = 0;

fn main() {
    unsafe {
        SAYAC += 1;
        println!("{}", SAYAC);
    }
}
// error: creating a shared reference to mutable static

Edition Guide'a göre böyle bir referansı kurallara aykırı biçimde oluşturmak, hiç okunmasa bile anında tanımsız davranıştır. Önerilen yollar, iç değiştirilebilirlik sağlayan bir tipi değiştirilemez static içinde kullanmak (örneğin atomik tipler) ya da gerçekten küresel akıl yürütme gerekiyorsa ham işaretçiyle erişmektir:

rust
use std::sync::atomic::{AtomicU32, Ordering};

static mut SAYAC: u32 = 0;
static ATOMIK: AtomicU32 = AtomicU32::new(0);

fn main() {
    unsafe {
        // SAFETY: tek iş parçacıklı main içinde, başka erişim yok.
        SAYAC += 1;
        println!("SAYAC: {}", *(&raw const SAYAC));
    }

    ATOMIK.fetch_add(5, Ordering::Relaxed);
    println!("ATOMIK: {}", ATOMIK.load(Ordering::Relaxed));
}
// çıktı:
// SAYAC: 1
// ATOMIK: 5

Atomik sürüm hiç unsafe gerektirmez ve iş parçacıkları arasında da doğrudur. Paylaşılan durum için atomikler, Mutex ve Arc seçenekleri Arc ve Mutex yazısında anlatılıyor.

unsafe trait ve union

Bir trait'in en az bir metodu derleyicinin doğrulayamadığı bir değişmez taşıyorsa trait unsafe trait olarak tanımlanır ve uygulaması da unsafe impl ile yazılır. En bilinen örnekler Send ve Sync işaretçi trait'leridir: derleyici bunları tamamen bu trait'leri uygulayan tiplerden oluşan tiplere otomatik uygular. Ham işaretçi gibi bu trait'leri uygulamayan bir alan içeren tipinizi yine de iş parçacıkları arasında güvenli ilan etmek istiyorsanız unsafe impl Send yazmanız ve gerekli denetimi kendiniz yapmanız gerekir.

union bir yapıya benzer, ama bir anda yalnızca bir alanı kullanılır ve tüm alanlar aynı belleği paylaşır. Kitaba göre esas kullanım alanı C kodundaki union'larla çalışmaktır. Alan okumak unsafe gerektirir, çünkü Rust o anda hangi alanın geçerli veriyi tuttuğunu bilemez:

rust
#[repr(C)]
union Sayi {
    tam: u32,
    kesir: f32,
}

fn main() {
    let s = Sayi { kesir: 1.0 };
    // SAFETY: u32 ve f32 aynı boyutta; her bit deseni geçerli bir u32.
    let bitler = unsafe { s.tam };
    println!("{bitler:#x}");
    println!("{:#x}", 1.0f32.to_bits());
}
// çıktı:
// 0x3f800000
// 0x3f800000

İkinci satırdaki to_bits aynı sonucu tamamen güvenli kodla verir. Bu küçük örnek genel bir dersi de özetliyor: standart kütüphanede güvenli bir karşılık varsa onu tercih edin.

Tanımsız davranış: Rust neyi yasaklar, neyi yasaklamaz?

Tanımsız davranış: Rust neyi yasaklar, neyi yasaklamaz? Sarkan (dangling) veya hizasız işaretçiyi dereference etmek., İşaretçi takma ad (aliasing) kurallarını çiğnemek...

Rustonomicon: What Unsafe Rust Can Do sayfası, Rust çekirdek dilinin önlemeye çalıştığı tanımsız davranışları kısa bir listeyle verir. Bunlardan başlıcaları şunlardır:

  • Sarkan (dangling) veya hizasız işaretçiyi dereference etmek.
  • İşaretçi takma ad (aliasing) kurallarını çiğnemek.
  • Fonksiyonu yanlış çağrı ABI'siyle çağırmak.
  • Veri yarışına neden olmak.
  • Geçersiz değer üretmek: 0 veya 1 dışında bir bool, geçersiz ayırt ediciye sahip bir enum, null fonksiyon işaretçisi, geçerli aralık dışında bir char, başlatılmamış bellekten okunan tamsayı, sarkan veya hizasız referans ya da Box.

Aynı sayfa, Rust'ın güvenli kabul ettiği ama büyük ihtimalle hatalı programlara işaret eden durumları da sayar: kilitlenme (deadlock), yarış durumu, bellek sızıntısı, yerleşik operatörlerle tamsayı taşması ve programın sonlandırılması. Yani güvenli Rust bu sorunları imkânsız kılmaz; yalnızca tanımsız davranış üretmelerini engeller. Daha ayrıntılı ve kesin liste için The Reference: Behavior considered undefined sayfasına bakın.

Sık yapılan hatalar

  • unsafe bloğun borrow checker'ı kapattığını sanmak; referanslar blok içinde de denetlenir.
  • Bütün fonksiyonu tek bir büyük unsafe blok içine almak; bloklar küçük ve gerekçeli olmalıdır.
  • SAFETY yorumu ve # Safety belgesi yazmadan güvensiz API yayımlamak.
  • Güvensiz bloğun dayandığı denetimi (örneğin bir assert!) sonradan güvenli kodda kaldırmak.
  • static mut değişkene referans almak; atomik tip veya &raw const kullanın.
  • extern bloğundaki imzayı C tarafındakiyle karşılaştırmadan safe olarak işaretlemek.
  • Standart kütüphanede güvenli karşılığı olan bir işi (split_at_mut, to_bits) elle unsafe kodla yazmak.

Sık Sorulan Sorular

unsafe kod yazmak kötü bir uygulama mı?

Hayır. Kitaba göre bu beş yetenekten birini bir gerekçeyle kullanmak yanlış ya da kınanacak bir şey değildir; yalnızca doğru yazmak daha zordur, çünkü derleyici bellek güvenliğini sizin yerinize koruyamaz. Açık unsafe işareti, sorun çıktığında kaynağını bulmayı kolaylaştırır.

Projemde hiç unsafe olmadığından nasıl emin olurum?

Crate kökünde #![forbid(unsafe_code)] özniteliği ya da Cargo.toml içinde [lints.rust] altında unsafe_code = "forbid" kullanabilirsiniz. Lint düzeyleri Clippy ve rustfmt yazısında ele alınıyor.

unsafe kullanınca program daha mı hızlı çalışır?

Kendiliğinden hayır. unsafe yalnızca derleyicinin doğrulayamadığı işlemlere izin verir. Kitap ham işaretçilerin bazı durumlarda garantilerden vazgeçip daha fazla performans ya da başka dillerle ve donanımla etkileşim imkânı verdiğini söyler, ama bunun bir ödünleşim olduğunu ve sorumluluğun size geçtiğini unutmayın.

Rust 2024 geçişinde unsafe ile ilgili neler değişti?

extern bloklarının unsafe extern olması, no_mangle gibi özniteliklerin unsafe(...) içinde yazılması, unsafe fn gövdesinde de unsafe blok istenmesi, static mut referanslarının reddedilmesi ve env::set_var ile env::remove_var fonksiyonlarının unsafe olması. Hepsi 2024 edition yazısında toplu halde anlatılıyor; set_var için ortam değişkenleri yazısına de bakabilirsiniz.

Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.

İlgili içerikler: Rust öğrenme rehberi · Rust nedir? · Referans ve borrowing · Arc, Mutex ve atomikler · Rust 2024 edition · C++ nedir?