Tüm Rust rehberleri
TEKNİK REHBER İLERİ UNSAFE 2026

unsafe & FFI
Raw pointer, C çağırma, bindgen

unsafe blokların beş süper gücü, raw pointer'lar, extern "C" ile C kütüphanesi çağırma ve bindgen; güvenli soyutlama kabuğu — C ABI ile birebir.

00 unsafe ne DEĞİLDİR

C/C++ dünyasından gelirken unsafe kelimesi yanıltıcıdır. "unsafe yazınca derleyici susar, ben de C gibi rahatça pointer çevirmeye başlarım" beklentisi baştan sona yanlıştır. unsafe bir kapıyı kapatmaz, sadece beş tane fazladan kapı açar.

En yaygın yanlış inanış şu: unsafe, borrow checker'ı kapatır. Kapatmaz. Bir unsafe bloğunun içinde de ownership, move semantiği, lifetime kontrolü ve &mut tekliği kuralları aynen geçerlidir. Normal bir referansı (&T, &mut T) yanlış kullanırsan derleyici unsafe bloğun içinde de seni durdurur. unsafe yalnızca derleyicinin doğrulayamadığı beş işlemi yapmana izin verir; geri kalan her şey hâlâ tam denetim altındadır.

DİKKAT

unsafe, "bu kod tehlikeli" demek değildir. Anlamı şudur: "Derleyici bu noktada bellek güvenliğini kanıtlayamıyor; kanıtı ben üstleniyorum." Sorumluluk derleyiciden programcıya geçer — tıpkı C'deki her satırın varsayılan durumu gibi. Fark, Rust'ta bunun yalnızca işaretli ve denetlenebilir adacıklarda geçerli olmasıdır.

C'de programın tamamı bir unsafe bloğudur; güvenlik kanıtı tümüyle senin kafandadır. Rust'ta felsefe terstir: varsayılan güvenli, çekirdek unsafe. Tehlikeli işlemi mümkün olan en küçük bloğa hapsedersin, etrafına invariant'ları koruyan güvenli bir API geçirirsin. Kullanıcılar bu güvenli kabuğu çağırır, asla unsafe'i görmez.

C:    tüm program unsafe (kanıt = programcının kafası)
Rust: güvenli kabuk → küçük unsafe çekirdek → kabuk invariant'ı korur

Bu yüzden iyi yazılmış Rust kodunda unsafe satır sayısı toplam kodun küçük bir yüzdesidir. std kütüphanesinin kendisi içeride bolca unsafe kullanır (Vec, String, Mutex hepsi unsafe çekirdeklidir) ama sana sunduğu yüzey %100 güvenlidir. Senin hedefin de aynısıdır: unsafe'i yutup dışarıya güvenli vermek.

NOT

Bir unsafe bloğu yazdığında, o bloğun gerektirdiği tüm önkoşulları (pointer geçerli, hizalı, aliasing yok, vb.) elle sağladığını taahhüt etmiş olursun. Bunu bir yorum satırıyla belgelemek (// SAFETY: ...) topluluğun değişmez geleneğidir; clippy bunu zorlayabilir.

Bu bölümde

  • unsafe borrow checker'ı kapatmaz; ownership/lifetime/&mut kuralları aynen geçerlidir.
  • unsafe yalnızca derleyicinin doğrulayamadığı beş ek işlemi açar; sorumluluk programcıya geçer.
  • Felsefe: güvenli kabuk + en küçük unsafe çekirdek (std'nin kendi modeli).
  • Her unsafe blok bir // SAFETY: gerekçesiyle belgelenir.

01 unsafe'in beş gücü

unsafe anahtar kelimesinin açtığı kapı sayısı tam olarak beştir — ne bir eksik, ne bir fazla. Bunları bilmek "unsafe içinde neye dikkat edeceğimi" tam olarak çerçeveler.

Güvenli Rust'ta yasak olup yalnızca bir unsafe bağlamında izin verilen işlemler şunlardır:

#GüçNeden derleyici doğrulayamaz
1Raw pointer deref (*p)Pointer'ın geçerli, hizalı ve canlı belleğe baktığını derleyici bilemez.
2unsafe fn / unsafe blok çağırmaÇağrılan fonksiyonun önkoşullarını çağıran sağlamalı.
3static mut erişimiGlobal değişken paylaşımlı + değiştirilebilir → veri yarışı riski.
4union alanı okumaHangi alanın "aktif" olduğunu tür sistemi takip etmez (etiketsiz).
5unsafe trait implementSend/Sync gibi trait'lerin garantisini programcı üstlenir.

FFI'da pratikte sürekli ilk iki gücü kullanırsın: yabancı (extern) fonksiyon çağırmak her zaman 2. güçtür (extern fonksiyon imzaları derleyici için unsafe'tir), C'den gelen pointer'ı okumak da 1. güçtür. Diğer üçü daha nadirdir.

main.rs
// 1) raw pointer deref
let x = 42;
let p = &x as *const i32;
let v = unsafe { *p };           // deref → unsafe gerekir

// 3) static mut erişimi (önerilmez; örnek için)
static mut SAYAC: u32 = 0;
unsafe { SAYAC += 1; }            // yazma da okuma da unsafe

// 4) union alanı okuma
union Reg { word: u32, bytes: [u8; 4] }
let r = Reg { word: 0xDEADBEEF };
let b0 = unsafe { r.bytes[0] };   // hangi alan aktif? sen bilirsin
NOT

Dikkat: bu beşinin içinde "integer'ı pointer'a çevirmek", "bir tipi başka bir tipe bit-bit dökmek (transmute)" gibi işlemler doğrudan birer "güç" değildir — bunlar 1. gücün (deref) ya da güvenli fonksiyonların önkoşullarını ihlal etme potansiyeli taşıyan ham yetenekler olarak unsafe gerektirir. Liste, dilin tanımladığı çekirdek beş izindir.

Bu bölümde

  • unsafe tam olarak beş ek güç açar: raw deref, unsafe çağrı, static mut, union okuma, unsafe trait impl.
  • Her gücün ortak noktası: derleyicinin doğrulayamadığı bir invariant'ı programcı üstlenir.
  • FFI'da neredeyse her zaman 1. (deref) ve 2. (extern çağrı) güçler devrededir.

02 Raw pointer'lar

Rust'ın raw pointer'ları (*const T, *mut T) C pointer'larının birebir karşılığıdır: aliasing garantisi yok, lifetime yok, null olabilir. Referansların (&T, &mut T) tüm güvenlik garantilerini bilerek kaybedersin.

Sözdizimi C'ye yakındır ama *const / *mut tek bir token gibi okunur: "T'ye bir const pointer". Bir referanstan raw pointer üretmek güvenlidir — tehlikeli olan onu deref etmektir.

main.rs
let mut n = 10;

// referanstan raw pointer üretmek: GÜVENLİ
let p: *const i32 = &n;
let pm: *mut i32 = &mut n;

// deref etmek: UNSAFE — geçerliliğin kanıtı sende
unsafe {
    println!("{}", *p);   // 10
    *pm = 20;             // n artık 20
}

Raw pointer ile referans arasındaki kavramsal fark, C'den gelen biri için kritik. Referans bir sözleşmedir; raw pointer yalnızca bir adrestir:

Özellik&T / &mut T*const T / *mut T
Null olabilir mi?AslaEvet
Geçerli belleğe bakma garantisiVar (borrow checker)Yok
Aliasing kuralıZorunlu (XOR mutation)Yok — C gibi serbest
Lifetime takibiVarYok
Deref güvenli mi?EvetHayır (unsafe)

Raw pointer deref ederken sırtladığın kurallar tam olarak C'dekilerle aynıdır: pointer null olmamalı, dangling olmamalı (gösterdiği veri hâlâ canlı), hizalı olmalı (T'nin alignment'ı), ve eğer &mut türetiyorsan o an başka aktif erişim olmamalı (aliasing ihlali UB'dir).

nullDeref'ten önce kontrol et; p.is_null() ya da NonNull<T> kullan.
danglingGösterilen veri serbest bırakıldıysa/scope dışına çıktıysa deref UB.
aliasingAynı bellek için &mut + başka erişim aynı anda → UB.
DİKKAT

Bir raw pointer'dan &T ya da &mut T ürettiğin an, o referansın yaşam süresince tüm Rust aliasing kuralları geçerli olmak zorundadır. C'de iki int*'in aynı yere bakması sorun değildir; ama bir &mut i32 türettiysen, o referans yaşarken aynı belleğe başka hiçbir erişim olamaz — aksi halde derleyici görünmese bile UB'dir.

Bu bölümde

  • *const T / *mut T = C pointer'ı: null'lanabilir, lifetime'sız, aliasing garantisiz.
  • Referanstan raw pointer üretmek güvenli; deref etmek unsafe.
  • Deref kuralları C ile aynı: null değil, dangling değil, hizalı; &mut tekliği korunmalı.
  • Raw pointer'dan referans türetince Rust'ın tüm aliasing kuralları yeniden devreye girer.

03 C fonksiyonu çağırma (extern C)

Bir C fonksiyonunu çağırmak için iki şey gerekir: imzasını extern "C" bloğunda bildirmek (C header'ındaki prototip gibi) ve linker'a sembolü nereden bulacağını söylemek. Çağrının kendisi her zaman unsafe'tir.

extern "C" burada ABI seçimidir: argümanların register/stack düzeni, isim mangling kuralları C kuralıyla aynı olsun demek. Rust'ın varsayılan ABI'si ("Rust") stabil değildir; C ile konuşacaksan mutlaka "C" dersin. libc gibi standart kütüphane fonksiyonları zaten link edilidir; harici kütüphaneler için #[link] kullanırsın.

main.rs
use std::os::raw::c_int;

// libc'deki int abs(int) prototipinin Rust karşılığı
unsafe extern "C" {
    fn abs(input: c_int) -> c_int;
}

fn main() {
    // çağrı unsafe: imzanın C tarafıyla eşleştiğini sen garanti edersin
    let r = unsafe { abs(-7) };
    println!("abs(-7) = {}", r);   // 7
}
NOT

Rust 2024 sürümünde extern "C" blokları unsafe extern "C" olarak yazılır — blok başlığındaki unsafe, "bu bildirimlerin doğruluğunu ben üstleniyorum" anlamına gelir. Eski sürümlerde sadece extern "C" { ... } yazılırdı. Her iki durumda da çağrı yeri ayrıca bir unsafe blok ister.

Harici bir kütüphaneyi (örneğin libfoo) link etmek için fonksiyon imzasının üstüne #[link] koyarsın; bu, linker'a -lfoo demekle eşdeğerdir:

main.rs
// size_t strlen(const char *s); — libc, genelde otomatik linkli
use std::os::raw::{c_char, c_ulong};

unsafe extern "C" {
    fn strlen(s: *const c_char) -> c_ulong;
}

// Harici kütüphane için:
// #[link(name = "foo")]              → -lfoo
// #[link(name = "foo", kind = "static")]  → statik link

İlkel tipleri C ABI ile eşleştirirken std::os::raw (veya libc crate'i) içindeki takma adları kullan: c_int, c_char, c_long, c_void... Bunlar platforma göre doğru genişliğe çözülür, C'deki int/char ile birebir uyumludur. Asla i32 ile c_int'i aynı kabul edip kestirme yapma — çoğu platformda eşittirler ama garanti değildir.

Bu bölümde

  • extern "C" ABI seçimidir: register/stack düzeni ve mangling C kuralıyla aynı.
  • extern fonksiyon = C header'daki prototip; çağrı her zaman bir unsafe blok ister.
  • #[link(name = "...")] = linker'a -l... demek; libc genelde otomatik linklidir.
  • İlkel tipler için std::os::raw / libc takma adları (c_int, c_char) kullanılır.

04 Rust'tan C'ye veri geçirme

Bellek düzeni (memory layout) FFI'nın kalbidir. Rust'ın varsayılan struct düzeni belirsizdir — alanları yeniden sıralayabilir. C'ye geçen her tip #[repr(C)] ile işaretlenmeli ki düzen C ABI ile bire bir eşleşsin.

#[repr(C)] derleyiciye "bu struct'ı C derleyicisinin yapacağı gibi diz: alan sırası bozulmasın, padding C kuralıyla aynı olsun" der. Bu olmadan struct'ı pointer ile C'ye geçirmek doğrudan UB'dir.

main.rs
// C tarafı: struct Point { int x; int y; };
#[repr(C)]
struct Point {
    x: c_int,
    y: c_int,
}

unsafe extern "C" {
    // void translate(struct Point *p, int dx, int dy);
    fn translate(p: *mut Point, dx: c_int, dy: c_int);
}

let mut pt = Point { x: 1, y: 2 };
unsafe { translate(&mut pt, 10, 20); }   // &mut → *mut otomatik

String geçirmek C'nin en sevdiği tuzaktır: C string'leri NUL ile biter, Rust String/str'leri ise uzunluk taşır ve NUL içerebilir. Köprü CString (sahipli, NUL-sonlu) ve CStr (ödünç alınmış, NUL-sonlu görünüm) tipleridir.

main.rs
use std::ffi::{CString, CStr};
use std::os::raw::c_char;

unsafe extern "C" {
    fn puts(s: *const c_char) -> c_int;
}

// Rust String → C char*  (NUL ekler, içte NUL varsa hata döner)
let s = CString::new("merhaba C").unwrap();
unsafe { puts(s.as_ptr()); }
// DİKKAT: s burada hâlâ canlı olmalı; as_ptr ödünç pointer verir

Dilim (slice) geçirmek için C'nin klasik "pointer + uzunluk" kalıbını kullanırsın. Rust dilimi (&[T]) zaten pointer+uzunluk çiftidir; ikisini ayrı argüman olarak verirsin:

main.rs
unsafe extern "C" {
    // long sum(const int *data, size_t len);
    fn sum(data: *const c_int, len: usize) -> c_long;
}

let v: Vec<c_int> = vec![1, 2, 3, 4];
let total = unsafe { sum(v.as_ptr(), v.len()) };
DİKKAT

Bellek sahipliği (ownership) FFI'nın en sık çökme noktasıdır. Kural: kim ayırdıysa o serbest bırakır. Rust'ta CString::into_raw() ile pointer verip sahipliği C'ye devredersen, o belleği geri alıp CString::from_raw() ile Rust'ta drop etmen gerekir — C'nin free()'siyle değil. Tersine C'nin malloc'ladığı belleği asla Rust'ın allocator'ıyla serbest bırakma. Karışık allocator = anında UB.

Bu bölümde

  • #[repr(C)] struct düzenini C ABI ile sabitler; FFI'ya geçen her tip için zorunlu.
  • String köprüsü CString (sahipli, NUL-sonlu) ve CStr (ödünç); as_ptr() pointer'ı canlı kalmalı.
  • Dilim "pointer + uzunluk" çifti olarak geçer: v.as_ptr() + v.len().
  • Sahiplik kuralı: kim ayırdıysa o free eder; allocator karıştırma UB'dir.

05 C'den Rust çağırma

Yön tersine döndüğünde Rust fonksiyonunu C'nin görebileceği bir sembol haline getirirsin: #[no_mangle] pub extern "C" fn. Bu, C dünyasına açtığın bir prototiptir.

#[no_mangle] isim mangling'i kapatır — sembol adı tam olarak fonksiyon adın olur, C linker'ı onu bulabilir. extern "C" ABI'yi C'ye sabitler. İkisi birlikte, C kaynağından extern ile çağrılabilen bir fonksiyon üretir.

lib.rs
use std::os::raw::c_int;

// C tarafı bunu çağırır: extern int rust_kare(int);
#[no_mangle]
pub extern "C" fn rust_kare(x: c_int) -> c_int {
    x * x
}

En sık kullanım callback'tir: C kütüphanesine bir Rust fonksiyon pointer'ı verirsin (örneğin qsort'un karşılaştırıcısı). Rust tarafında fonksiyon pointer tipi extern "C" fn(...) -> ... şeklinde yazılır:

main.rs
use std::os::raw::{c_int, c_void};

unsafe extern "C" {
    fn qsort(
        base: *mut c_void, n: usize, sz: usize,
        cmp: extern "C" fn(*const c_void, *const c_void) -> c_int,
    );
}

// C'nin çağıracağı karşılaştırıcı — extern "C" olmalı
extern "C" fn cmp_i32(a: *const c_void, b: *const c_void) -> c_int {
    let (a, b) = unsafe { (*(a as *const c_int), *(b as *const c_int)) };
    (a - b) as c_int
}
DİKKAT

Bir Rust panic'inin FFI sınırını geçmesi tanımsız davranıştır. Stack unwinding C frame'lerinin üzerinden geçemez. C'den çağrılan her extern "C" Rust fonksiyonunda panic ihtimali varsa, std::panic::catch_unwind ile sınırı içeride kapatmalısın; aksi halde program çöker ya da daha kötüsü UB üretir.

lib.rs
use std::panic::catch_unwind;
use std::os::raw::c_int;

#[no_mangle]
pub extern "C" fn islem(x: c_int) -> c_int {
    // panic'i FFI sınırını geçmeden yakala
    let r = catch_unwind(|| {
        if x < 0 { panic!("negatif!"); }
        x * 2
    });
    r.unwrap_or(-1)   // panic olursa C'ye hata kodu dön
}

Bu bölümde

  • #[no_mangle] pub extern "C" fn = C'den çağrılabilir sembol; mangling kapalı, ABI C.
  • Callback için fonksiyon pointer tipi extern "C" fn(...) -> ... olarak yazılır.
  • Panic FFI sınırını geçemez; catch_unwind ile sınırın Rust tarafında kapatılır.
  • C'ye dönüşte panic yerine hata kodu döndürmek doğru kalıptır.

06 bindgen

Büyük bir C header'ını elle Rust'a çevirmek hem sıkıcı hem hataya açıktır. bindgen, bir .h dosyasını okuyup ondan otomatik extern "C" bildirimleri, #[repr(C)] struct'lar ve sabitler üretir.

Tipik kullanım build-time'dadır: build.rs betiği derlemeden önce çalışır, bindgen'i çağırır, üretilen Rust kodunu OUT_DIR'a yazar, sen de include! ile dahil edersin. C derleyicisinin başlığı nasıl gördüğünü (clang üzerinden) aynen yansıtır.

Cargo.toml
[package]
name = "foo-sys"
version = "0.1.0"
edition = "2021"

[build-dependencies]
bindgen = "0.69"
build.rs
use std::path::PathBuf;
use std::env;

fn main() {
    // C kütüphanesini linkle
    println!("cargo:rustc-link-lib=foo");
    // header değişince yeniden üret
    println!("cargo:rerun-if-changed=wrapper.h");

    let bindings = bindgen::Builder::default()
        .header("wrapper.h")       // #include <foo.h> içeren başlık
        .generate()
        .expect("binding üretilemedi");

    let out = PathBuf::from(env::var("OUT_DIR").unwrap());
    bindings.write_to_file(out.join("bindings.rs")).unwrap();
}
lib.rs
// üretilen ham binding'leri dahil et
#![allow(non_upper_case_globals, non_camel_case_types, non_snake_case)]
include!(concat!(env!("OUT_DIR"), "/bindings.rs"));

Ekosistemde güçlü bir konvansiyon var: ham, otomatik üretilmiş binding'ler -sys crate'inde durur (örn. libgit2-sys, openssl-sys). Üzerine güvenli, deyimsel API koyan crate ise -sys son ekini taşımaz (git2, openssl). Bu ayrım sayesinde aynı C kütüphanesine birden çok yüksek seviye sarmalayıcı geçirilebilir.

YaklaşımNe zaman
Elle extern "C"Birkaç fonksiyon; basit, stabil imzalar; bağımlılık istemiyorsun.
bindgenBüyük/değişken header; bolca struct/enum/sabit; başlık üst sürümlerini takip.
NOT

bindgen "ham" binding üretir — pointer'lar, c_void, NUL-sonlu char dizileri. Bu binding doğrudan kullanılabilir ama hâlâ %100 unsafe yüzeydir. Asıl iş bir sonraki bölümdeki güvenli sarmalamadır; bindgen yalnızca sıkıcı çeviri kısmını otomatikleştirir.

Bu bölümde

  • bindgen, C header'ından otomatik extern "C" + #[repr(C)] binding üretir (clang tabanlı).
  • Tipik akış: build.rs → bindgen → OUT_DIR/bindings.rsinclude!.
  • Konvansiyon: ham binding foo-sys'te, güvenli API foo crate'inde durur.
  • Az fonksiyon → elle; büyük/değişken header → bindgen.

07 Güvenli sarmalama (safe wrapper)

FFI işinin asıl değeri burada: ham, unsafe binding'i küçük bir modüle hapsedip dışarıya güvenli, deyimsel ve kaynak-sızdırmayan bir API sunmak. Bu, s0'daki "güvenli kabuk, unsafe çekirdek" felsefesinin somut hali.

Klasik desen newtype + RAII'dir: C kaynağını tutan opak pointer'ı bir Rust struct'ı içine sarar, Drop ile otomatik temizlik bağlarsın. Kullanıcı C'nin foo_free()'sini hiç görmez; struct scope'tan çıkınca kaynak kendiliğinden serbest kalır — tıpkı C++ destructor'ı gibi, ama derleyici garantisiyle.

c — kütüphane
// Tampon: foo_open ayırır, foo_close serbest bırakır
typedef struct Foo Foo;
Foo  *foo_open(const char *ad);
int   foo_oku(Foo *f);
void  foo_close(Foo *f);
lib.rs
use std::ffi::CString;
use std::os::raw::{c_char, c_int};
use std::ptr::NonNull;

// ── ham, unsafe binding (bu modülde kalır) ──
enum FooRaw {}   // opak C tipi (alanı yok)
unsafe extern "C" {
    fn foo_open(ad: *const c_char) -> *mut FooRaw;
    fn foo_oku(f: *mut FooRaw) -> c_int;
    fn foo_close(f: *mut FooRaw);
}

// ── güvenli kabuk: newtype + RAII ──
pub struct Foo {
    raw: NonNull<FooRaw>,   // asla null değil — invariant
}

impl Foo {
    pub fn open(ad: &str) -> Option<Foo> {
        let c = CString::new(ad).ok()?;
        // SAFETY: c canlı; foo_open ya geçerli pointer ya null döner
        let p = unsafe { foo_open(c.as_ptr()) };
        NonNull::new(p).map(|raw| Foo { raw })
    }

    pub fn oku(&mut self) -> i32 {
        // SAFETY: raw invariant gereği geçerli
        unsafe { foo_oku(self.raw.as_ptr()) }
    }
}

impl Drop for Foo {
    fn drop(&mut self) {
        // SAFETY: raw bir kez, burada serbest bırakılır
        unsafe { foo_close(self.raw.as_ptr()); }
    }
}

Bu kabuğun kullanıcısı tek bir unsafe görmez:

main.rs
let mut f = Foo::open("veri.bin").expect("açılamadı");
println!("{}", f.oku());
// f scope'tan çıkınca Drop → foo_close otomatik; sızıntı yok
NOT

Güvenli wrapper'ın iki sorumluluğu vardır: (1) C'nin invariant'larını koru (pointer null değil, çift-free yok, kullanım-sonrası-serbest yok), (2) Rust kullanıcısına bunu yapamayacağı bir API sun. Eğer kullanıcı senin "güvenli" fonksiyonlarını herhangi bir sırada çağırarak UB tetikleyebiliyorsa, o API güvenli değildir — o zaman fonksiyonu unsafe işaretlemen gerekir.

Bu bölümde

  • Ham binding tek bir modülde hapsedilir; dışarıya yalnızca güvenli API sunulur.
  • Newtype + NonNull<T> + Drop = otomatik RAII; C kaynağı kendiliğinden serbest kalır.
  • Her unsafe blok bir // SAFETY: gerekçesiyle hangi invariant'a dayandığını belgeler.
  • API ancak her çağrı sırasında UB imkânsızsa "güvenli"dir; değilse unsafe fn olmalı.

08 UB tuzakları ve doğrulama

unsafe'in bedeli, derleyicinin artık tutamadığı invariant'ları senin tutmandır. FFI'da en sık karşılaşılan UB sınıflarını tanı, sonra Miri ve ASan gibi araçlarla bunları çalışma anında yakala.

C/C++'tan tanıdık olanların yanında Rust'a özgü olanlar da var. Tehlikeli olanların özeti:

UB sınıfıNe zaman olur
Hizalama (alignment)Hizasız adresten T okuma; read_unaligned kullan.
Dangling / use-after-freeSerbest bırakılmış ya da scope'tan çıkmış belleğe deref.
Null derefNull pointer'ı deref; NonNull / is_null() ile koru.
Aliasing ihlaliBir &mut T yaşarken aynı belleğe başka erişim.
Geçersiz değerbool=2, geçersiz enum diskriminantı, geçersiz char.

Son satır C'lilere yabancıdır: Rust'ta her tipin geçerli bit kalıpları kümesi vardır. Bir bool yalnızca 0 veya 1 olabilir; C'den gelen bir uint8_t 2 ise ve onu bool olarak yorumlarsan, hiçbir satır yazmadan UB üretmiş olursun. Geçersiz bir enum diskriminantı ya da 0x110000 üstü bir char de aynı şekilde anında UB'dir.

main.rs
// HATALI: hizasız okuma UB
let buf: [u8; 8] = [0; 8];
let p = buf.as_ptr().wrapping_add(1) as *const u32;
// let x = unsafe { *p };          // UB: 1 adresi u32 için hizasız
let x = unsafe { p.read_unaligned() };   // DOĞRU

Bu UB'lerin çoğu derleyicinin göremediği yerlerdir — kod çalışır, hatta testler geçer, sonra optimizasyon sürümünde ya da başka platformda patlar. Çözüm: Miri. Miri, Rust'ı bir yorumlayıcıda çalıştırıp her bellek erişiminde UB'yi yakalar (hizalama, aliasing, use-after-free dahil). Saf-Rust unsafe kodu için altın standarttır.

bash
# Miri'yi kur ve testleri yorumlayıcıda çalıştır
rustup +nightly component add miri
cargo +nightly miri test
cargo +nightly miri run
DİKKAT

Miri, gerçek C kodunu (extern fonksiyon gövdelerini) çalıştıramaz — yalnızca Rust tarafını yorumlar. Saf FFI çağrılarında C'ye geçen kısmı Miri göremez. Orada devreye AddressSanitizer (ASan) girer: RUSTFLAGS="-Zsanitizer=address" cargo +nightly run ile hem Rust hem C tarafını çalışma anında denetlersin (heap-overflow, use-after-free, vb.) — C'deki -fsanitize=address ile aynı mantık.

Bu bölümde

  • FFI UB'leri: hizalama, dangling/UAF, null deref, aliasing ihlali, geçersiz bit kalıbı.
  • Rust'a özgü: her tipin geçerli değer kümesi var (bool∈{0,1}, geçerli enum/char).
  • Hizasız okuma için read_unaligned; null için NonNull/is_null().
  • Doğrulama: saf-Rust UB için Miri (cargo +nightly miri test), C tarafı dahil için ASan.

09 Gerçek örnek

Tüm parçaları birleştirelim: küçük bir C kütüphanesi (sahipli bir sayaç nesnesi), build.rs ile derlenip linklensin, üzerine RAII tabanlı güvenli bir Rust API otursun. Bu, gerçek bir -sys + güvenli crate ikilisinin minyatürüdür.

C tarafı: bir sayaç nesnesini malloc'layan, artıran, değerini okuyan ve serbest bırakan klasik opak-handle API'si.

c — kütüphane
// counter.c — derlenip statik kütüphane olur
#include <stdlib.h>

struct Counter { int deger; };

struct Counter *counter_new(int baslangic) {
    struct Counter *c = malloc(sizeof *c);
    if (c) c->deger = baslangic;
    return c;                         // sahiplik çağırana geçer
}
void counter_add(struct Counter *c, int d) { c->deger += d; }
int  counter_get(const struct Counter *c) { return c->deger; }
void counter_free(struct Counter *c) { free(c); }

build.rs, cc crate'iyle C kaynağını derler ve linkler — make/cmake yazmaya gerek yok:

build.rs
// Cargo.toml → [build-dependencies] cc = "1"
fn main() {
    cc::Build::new()
        .file("src/counter.c")
        .compile("counter");   // libcounter.a üretir + linkler
    println!("cargo:rerun-if-changed=src/counter.c");
}

Rust tarafı: ham extern "C" bildirimleri + üzerine RAII'li güvenli Counter tipi. Sahiplik açıkça yönetilir: counter_new sahipliği verir, Drop içinde counter_free geri alır.

lib.rs
use std::os::raw::c_int;
use std::ptr::NonNull;

// ── ham binding ──
enum CounterRaw {}
unsafe extern "C" {
    fn counter_new(baslangic: c_int) -> *mut CounterRaw;
    fn counter_add(c: *mut CounterRaw, d: c_int);
    fn counter_get(c: *const CounterRaw) -> c_int;
    fn counter_free(c: *mut CounterRaw);
}

// ── güvenli kabuk ──
pub struct Counter { raw: NonNull<CounterRaw> }

impl Counter {
    pub fn new(baslangic: i32) -> Option<Counter> {
        // SAFETY: counter_new ya geçerli ya null döner; NonNull filtreler
        let p = unsafe { counter_new(baslangic) };
        NonNull::new(p).map(|raw| Counter { raw })
    }
    pub fn add(&mut self, d: i32) {
        // SAFETY: raw invariant gereği geçerli; &mut tekliği aliasing'i kapatır
        unsafe { counter_add(self.raw.as_ptr(), d); }
    }
    pub fn get(&self) -> i32 {
        // SAFETY: raw geçerli; salt okuma
        unsafe { counter_get(self.raw.as_ptr()) }
    }
}

impl Drop for Counter {
    fn drop(&mut self) {
        // SAFETY: tek sahip, tam bir kez free — çift-free yok
        unsafe { counter_free(self.raw.as_ptr()); }
    }
}
main.rs
fn main() {
    let mut c = Counter::new(10).expect("ayrılamadı");
    c.add(5);
    c.add(-2);
    println!("deger = {}", c.get());   // deger = 13
}   // c drop → counter_free otomatik; sızıntı yok, çift-free yok
counter.c → cc (build.rs) → libcounter.a → extern "C" ham binding → güvenli Counter (RAII) → main

Kapanış: FFI köprüsü iki yönlüdür ama disiplin tek yönlüdür — her zaman güvenli kabuğa doğru. C tarafının her belirsizliğini (null, sahiplik, ABI, hizalama, panic sınırı) en küçük unsafe çekirdekte karşılar, dışarıya derleyicinin koruduğu bir yüzey verirsin. Miri ve ASan ile çekirdeği doğrular, // SAFETY: ile gerekçeni belgelersin. Böylece C'nin gücünü Rust'ın garantileriyle birleştirmiş olursun.

Bu bölümde

  • Tam zincir: counter.ccc/build.rs → statik lib → ham extern "C" → RAII'li güvenli Counter.
  • Sahiplik açık: C new ile verir, Rust Drop ile tam bir kez geri alır (çift-free yok).
  • Her unsafe bir // SAFETY: gerekçesine dayanır; kullanıcı yüzeyi %100 güvenli.
  • Disiplin: tüm C belirsizliğini en küçük çekirdekte karşıla, Miri/ASan ile doğrula.