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.
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.
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 |
|---|---|---|
| 1 | Raw pointer deref (*p) | Pointer'ın geçerli, hizalı ve canlı belleğe baktığını derleyici bilemez. |
| 2 | unsafe fn / unsafe blok çağırma | Çağrılan fonksiyonun önkoşullarını çağıran sağlamalı. |
| 3 | static mut erişimi | Global değişken paylaşımlı + değiştirilebilir → veri yarışı riski. |
| 4 | union alanı okuma | Hangi alanın "aktif" olduğunu tür sistemi takip etmez (etiketsiz). |
| 5 | unsafe trait implement | Send/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.
// 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
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.
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? | Asla | Evet |
| Geçerli belleğe bakma garantisi | Var (borrow checker) | Yok |
| Aliasing kuralı | Zorunlu (XOR mutation) | Yok — C gibi serbest |
| Lifetime takibi | Var | Yok |
| Deref güvenli mi? | Evet | Hayı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).
p.is_null() ya da NonNull<T> kullan.&mut + başka erişim aynı anda → UB.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ı;
&muttekliğ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.
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
}
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:
// 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
unsafeblok ister. #[link(name = "...")]= linker'a-l...demek; libc genelde otomatik linklidir.- İlkel tipler için
std::os::raw/libctakma 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.
// 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.
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:
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()) };
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) veCStr(ö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.
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:
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
}
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.
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_unwindile 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.
[package]
name = "foo-sys"
version = "0.1.0"
edition = "2021"
[build-dependencies]
bindgen = "0.69"
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();
}
// ü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şım | Ne zaman |
|---|---|
Elle extern "C" | Birkaç fonksiyon; basit, stabil imzalar; bağımlılık istemiyorsun. |
| bindgen | Büyük/değişken header; bolca struct/enum/sabit; başlık üst sürümlerini takip. |
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.rs→include!. - Konvansiyon: ham binding
foo-sys'te, güvenli APIfoocrate'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.
// 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);
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:
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
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
unsafeblok 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 fnolmalı.
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-free | Serbest bırakılmış ya da scope'tan çıkmış belleğe deref. |
| Null deref | Null pointer'ı deref; NonNull / is_null() ile koru. |
| Aliasing ihlali | Bir &mut T yaşarken aynı belleğe başka erişim. |
| Geçersiz değer | bool=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.
// 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.
# Miri'yi kur ve testleri yorumlayıcıda çalıştır
rustup +nightly component add miri
cargo +nightly miri test
cargo +nightly miri run
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çinNonNull/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.
// 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:
// 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.
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()); }
}
}
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.c→cc/build.rs→ statik lib → hamextern "C"→ RAII'li güvenliCounter. - Sahiplik açık: C
newile verir, RustDropile tam bir kez geri alır (çift-free yok). - Her
unsafebir// 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.