Ders 04 / 12
`unsafe` Rust
unsafe bloğunun derleyicinin bütün denetimini değil, yalnız birkaç belirli işlemi kapsayan denetimini kapattığı; aynı blok içinde bir ham işaretçi okumasının denetimsiz kaldığı ama bir dilim sorgusunun hâlâ denetlendiği koşturularak gösterilir.
Yayın Güncelleme
Bu derste
- unsafe bloğun derleyicinin denetimini toptan değil, yalnız belirli birkaç işlem için kapattığını aynı blok içinde koşturarak gösterir
- Ham işaretçi okuma, sınır denetimsiz erişim ve sözleşmeli bir işlevin üçünün de güvenli karşılığıyla aynı sonucu verdiğini karşılaştırarak kanıtlar
- Devredilen bir güvencenin doğru kullanıldığında sonuçta görünmez kaldığını, yanlış kullanımın sorumluluğunu kimin taşıdığını ayırt ederek açıklar
İçindekiler
Önceki konu, bir makronun ürettiği gövdenin biçimi ne kadar farklı görünürse görünsün
yine sıradan kaynak gibi tip denetiminden geçtiğini gösterdi: derleyici üretilen bir
işlevi de, elle yazılmış bir işlev kadar sıkı denetledi. Bu konu aynı soruyu ters yönden
sorar: derleyicinin bu denetimi hiç yapmadığı, yapmayı bırakıp yazan tarafa bıraktığı bir
yazım var mıdır? Yanıt unsafe anahtar sözcüğüdür — ama bu dersin bulgusu, o sözcüğün
göründüğü kadar geniş bir kapı açmadığıdır.
Ödünç Denetleyicisi dersi (M10/K02) derleyicinin sahiplik ve ödünç kurallarını nasıl sınadığını, hangi yazımları reddettiğini kurdu. Bu ders o denetleyicinin kapandığını göstermez — kapanmadığını, yalnızca birkaç işlemin önündeki engelin kaldırıldığını gösterir. Standart Kütüphane ve Akışlar dersi (M08/K03) bir davranışın garantisini kimin verdiğini sormuştu: arayüz mü, gerçekleştirim mi, çağıran mı — bir sözleşmenin tarafları. Bu derste sorulan farklıdır: derleyicinin kendi denetiminin, sözleşmenin hiçbir tarafına bakmaksızın, nerede durduğu.
Bu kapının var olmasının bir nedeni var: ödünç denetleyicisinin sınadığı kurallar,
gerçekleştirilebilecek her doğru programı kapsamıyor. Bir dairesel tampon, bir düğüm
havuzu ya da bir donanım kaydına doğrudan erişen bir sürücü gibi düzenekler, ödünç
denetleyicisinin izleyebileceğinden daha karmaşık bir sahiplik örüntüsü taşıyabilir —
denetleyici bu örüntüyü doğru bulsa bile reddedebilir, çünkü sınama kuralı örüntünün
kendisinden değil, denetleyicinin izleyebildiği örüntü sınıfından gelir. unsafe, bu
uyuşmazlığı kapatan bir kaçış menfezidir: yazan taraf, denetleyicinin sınayamadığı bir
gerekçeyle “bu doğru” diyebilir. Standart kütüphanenin kendi büyüyen dizi ve dilim
tipleri de böyle kurulmuştur — dışarıdan bakan koda güvenli bir arayüz sunarlarken,
kendi gövdelerinde aynı ham işaretçi işlemlerini kullanırlar. Bu ders o gövdelerin
içine bakmaz; yalnızca aynı iznin sıradan bir programda nasıl çalıştığını ölçer.
unsafe Bloğunun Sınırlı İzni
Bir ham işaretçi kurmak unsafe gerektirmez. °er as *const i32 yazımı sıradan bir
ifadedir, derlenir ve hiçbir denetimi atlamaz — kurulan şey yalnız bir adrestir, henüz
okunmamıştır. Ödünç kuralları da bu satırda devrede değildir, çünkü ham işaretçiler ödünç
denetleyicisinin izlediği tipler arasında yer almaz; onlar için sahiplik, yaşam süresi ve
tekillik gibi bir muhasebe tutulmaz.
Kısıt, işaretçiyi okumaya çalışıldığında başlar. Bir ham işaretçinin gösterdiği yerde
gerçekten geçerli bir değer olduğunu derleyici bilemez — işaretçi boş olabilir, ait olduğu
bellek geri verilmiş olabilir, ya da yalnızca uydurma bir adres taşıyor olabilir. Derleyici
bu üç olasılığı ayırt edemediği için, okuma işlemini unsafe bloğunun içine almayı zorunlu
kılar: kod derlenir, ama “burada bir sözleşme var ve onu ben karşılıyorum” beyanı yazan
tarafa aittir. Aynı kısıt, gövdesi unsafe işaretli bir işlevi çağırmak için de geçerlidir
— işlevin adı, çağıranın bir ön koşulu sağlaması gerektiğini söyler, ama bu koşulu
derleyici sınamaz.
Bu izin listesi kısadır ve dilin kendisi tarafından sabitlenmiştir: derleyici hangi
işlemlerin unsafe gerektirdiğini kaynaktan okur, çağıranın niyetinden değil. Aşağıdaki
koşum, unsafe gerektirmeyen üç işlemi yan yana koyuyor: bir ham işaretçi kurmak, onu
taşmaya karşı tanımlı bir biçimde kaydırmak ve iki işaretçiyi birbiriyle karşılaştırmak.
Üçü de değeri okumadığı için, geçersiz bir adresle çalışıyor olsalar bile hiçbir şeyi
bozmazlar — bozacak olan, o adresteki veriye erişmeye kalkmaktır.
// izin.rs — hangi isaretci islemleri unsafe gerektirmiyor
fn main() {
let v = vec![10, 20, 30, 40];
let isaretci = v.as_ptr(); // kurma
let kaydirilmis = isaretci.wrapping_add(1); // tasmaya karsi tanimli kaydirma
let kendiyle_esit = isaretci == isaretci;
let kaydirilmisla_esit = isaretci == kaydirilmis;
println!("kendiyle esit mi: {}", kendiyle_esit);
println!("kaydirilmisla esit mi: {}", kaydirilmisla_esit);
}
kendiyle esit mi: true kaydirilmisla esit mi: false
Bu üç işlemin ortak yanı, sonuçlarının kendi başlarına hiçbir belleği açığa çıkarmamasıdır:
bir adresin kendisiyle eşit olup olmadığını bilmek, o adresteki değeri bilmekten farklıdır.
DS1. wrapping_add çağrısı burada bilinçli seçildi — dilin bu adı taşıyan işaretçi
yöntemi, kaydırma sonucu bir taşmaya uğrasa bile tanımlı bir değer döndürür ve bu yüzden
unsafe istemez; buna karşılık aynı kaydırmayı yapan add yöntemi, sonucun aynı ayrılmış
bellek bloğunun içinde ya da bir bayt ötesinde kalmasını çağırandan ön koşul olarak
ister ve bu ön koşul derleyici tarafından sınanmadığı için çağrısı unsafe gerektirir. İki
yöntem aynı aritmetiği yapar; aralarındaki fark, taşma durumunda neyin garanti edildiğidir.
Buna karşılık bir ham işaretçiyi okumak ya da yazmak ve gövdesi unsafe işaretli
bir işlevi çağırmak unsafe bloğu ister — çünkü bu işlemlerin ikisi de, derleyicinin
sınayamadığı bir varsayıma (adresin geçerli olduğuna, işlevin ön koşulunun sağlandığına)
dayanır. Aşağıdaki iki bölüm bu ayrımı aynı blok içinde koşturarak gösteriyor.
Aynı Blok İçinde Süren ve Duran Denetim
Bir unsafe bloğu, içine yazılan her satırı aynı ölçüde etkilemez. Aşağıdaki koşum aynı
blok içine iki farklı işlem koyuyor: biri bir ham işaretçiden doğrudan okuma, öteki bir
dilim üzerinde sınır denetimli bir sorgu. İkisi de aynı unsafe anahtar sözcüğünün altında
duruyor, ama yalnız biri denetimden muaf.
DS2. Dört elemanlı bir tam sayı dilimi kullanılıyor ve sorgulanan indis (onuncu öge) bu dilimin sınırlarının dışında.
// blokA.rs — ayni unsafe blok icinde suren ve duran denetim
fn main() {
let v = vec![10, 20, 30, 40];
let isaretci = v.as_ptr();
// Ayni blokta iki islem: biri ham isaretci uzerinden okuyor (denetimsiz),
// digeri guvenli dilim sorgusu (v.get) yapiyor (hala denetimli).
let (ilk_ham, onuncu) = unsafe { (*isaretci, v.get(10)) };
println!("ham okuma: {}", ilk_ham);
println!("onuncu oge: {:?}", onuncu);
}
ham okuma: 10 onuncu oge: None
İki satır da aynı unsafe bloğunun içinde çalıştı, ama farklı yollardan geçti. *isaretci
ham işaretçiyi doğrudan okudu — burada hiçbir sınır kontrolü yoktur, adres ne gösteriyorsa
o okunur. v.get(10) ise dilimin kendi yönteminden geçti; bu yöntemin gövdesi unsafe
işaretli değildir ve içindeki sınır denetimi hâlâ çalışıyor, bu yüzden dördüncü elemandan
sonrasını istediğinde çökmek yerine None döndü. DS3. unsafe bloğu burada yalnızca
*isaretci satırının önündeki engeli kaldırdı; v.get çağrısı zaten güvenli bir yöntemdi
ve unsafe bloğunun içine girmesi onun gövdesindeki denetimi değiştirmedi.
Bu gözlem dersin ana bulgusudur: unsafe bir anahtar değildir, derleyicinin bütün
kilitlerini birden açmaz. Blok yalnızca içine yazılan belirli işlemler için izin verir
— o işlemlerin dışındaki her şey, blok içinde bile, kendi kurallarına tabi kalır. Ödünç
denetleyicisi burada da devrede: v’nin sahipliği, isaretcinin ondan türetilmiş olması,
her ikisinin kapsam kuralları — hiçbiri unsafe bloğu yüzünden askıya alınmadı. Askıya
alınan tek şey, ham işaretçi okumasının kendi sınır denetimiydi.
Yukarıdaki koşum, güvenli çağrıyı unsafe bloğunun içine koyarak bunu bilerek gösterdi;
sıradan bir yazımda böyle bir zorunluluk yoktur, v.get(10) kendi başına bir unsafe
bloğu gerektirmez. Yaygın alışkanlık, bir unsafe bloğunu yalnız gerçekten gerektiren
satırlarla sınırlı tutmaktır — blok ne kadar genişlerse, hangi satırın hangi sözleşmeye
dayandığını sonradan ayırt etmek o kadar zorlaşır. Bu derste iki işlemin aynı blokta
yazılması, bu alışkanlığın neden var olduğunu somutlaştırmak için: blok geniş tutulduğunda
bile denetimin nerede sürdüğü, nerede durduğu kaynaktan okunabiliyor, ama bu okunabilirlik
otomatik değil, satır satır izlemeyi gerektiriyor.
Devredilen Üç İşlem
Önceki bölümdeki *isaretci okuması gibi, denetimin devredildiği satırlar tek başlarına
durmaz — her biri, aynı işi yapan güvenli bir karşılıkla yan yana konabilir. Aşağıdaki
koşum üç ayrı devredilmiş işlemi güvenli karşılıklarıyla karşılaştırıyor: ham işaretçi
aritmetiğiyle okuma, dilimin denetimsiz erişim yöntemi ve bir sözleşmeyi taşıyan unsafe
işlevin çağrısı.
Bu üç yazımın işlevi aynı olsa da kullanım yerleri farklıdır. Ham işaretçi aritmetiği,
bir bellek bloğunu Rust’ın kendi dilim tipinden bağımsız olarak dolaşmak gerektiğinde
kullanılır — örneğin bir yabancı işlevden dönen bir bellek bölgesini okurken, ki bu
konu bir sonraki derste ele alınıyor. get_unchecked, bir dilim üzerinde çalışırken
indisin geçerliliği zaten başka bir yerde (bir döngü sınırıyla, bir arama sonucuyla)
kanıtlanmışsa kullanılır. Sözleşmeli bir unsafe işlev ise bu iki yazımı bir arada
paketleyip, çağıran tarafa yalnızca imzayı okumasının yeterli olduğu bir arayüz sunmak
için kullanılır — sözleşme hâlâ vardır, ama gövde başka bir dosyaya taşınabilir.
DS4. Karşılaştırılan dilim yine [10, 20, 30, 40]; ham işaretçi ikinci indisten
başlayıp iki konum kaydırılıyor, denetimsiz erişim doğrudan ikinci indise gidiyor.
// blokB.rs — devredilen uc islem ve guvenli karsiliklari
unsafe fn ikinci_ogeyi_al(v: &[i32]) -> i32 {
// Sozlesme: cagiran, v'nin en az iki oge tasidigini garanti eder.
*v.get_unchecked(1)
}
fn main() {
let v = vec![10, 20, 30, 40];
let isaretci = v.as_ptr();
let ham = unsafe { *isaretci.add(2) };
let denetimsiz = unsafe { *v.get_unchecked(1) };
let sozlesmeli = unsafe { ikinci_ogeyi_al(&v) };
println!("ham isaretci okuma: {} (guvenli karsiligi {})", ham, v[2]);
println!("denetimsiz erisim: {} (guvenli karsiligi {})", denetimsiz, v[1]);
println!("sozlesmesi devredilen islev: {} (guvenli karsiligi {})", sozlesmeli, v[1]);
}
ham isaretci okuma: 30 (guvenli karsiligi 30) denetimsiz erisim: 20 (guvenli karsiligi 20) sozlesmesi devredilen islev: 20 (guvenli karsiligi 20)
Üç satırın da sonucu, [] ile yapılan sınır denetimli erişimle birebir aynı. Bu bir
rastlantı değil: add(2) ile ilerletilen işaretçi de, get_unchecked(1) çağrısı da,
ikinci_ogeyi_al işlevinin içindeki çağrı da, dilimin bellekteki düzenini doğru
okuyor — yalnızca bu doğruluğu kimin garanti ettiği değişiyor. Güvenli [] erişiminde
garantiyi dilimin kendi yöntemi verir ve indis sınırın dışındaysa çalışma anında panikle
durur. get_unchecked ve ham işaretçi aritmetiğinde bu garantiyi kimse vermez; doğru
indisi seçmek çağıranın yükümlülüğüdür.
ikinci_ogeyi_al işlevi bu yükümlülüğü kaynakta görünür kılan bir yazım göstermek için
var: imzasının önündeki unsafe sözcüğü, gövdenin çağrılmadan önce bir ön koşulun
sağlanmış olmasını beklediğini söyler. Bu, dilin kendisinin tanıdığı bir yazım kalıbıdır —
işlevin adı ve imzası, gövdesindeki denetimsiz erişimi çağıranın gözünden gizlemez, tersine
işaretler. Bir işlevi unsafe işaretlemek, gövdesinin içindeki her satırı otomatik olarak
denetimsiz kılmaz da: gövde içinde de yalnız gerçekten unsafe gerektiren satırlar
(burada get_unchecked çağrısı) o statüyü taşır, geri kalanı sıradan denetimli koddur. DS5. İşlevi unsafe çağıran taraf, v’nin en az iki eleman taşıdığını
kendisi doğrulamak zorunda; bu koşum sırasında dilim dört eleman taşıdığı için sözleşme
karşılandı ve sonuç güvenli karşılığıyla eşleşti.
Bu üç yöntemin dilde bulunmasının bir gerekçesi var: güvenli [] erişimi her çağrıda bir
sınır denetimi adımı yapar — indisin dilimin uzunluğundan küçük olup olmadığını
sınar, sonra okur. get_unchecked bu adımı atlar; okuduğu tek şey, çağıranın zaten
başka bir yerde doğruladığı bir indistir. Bu ders o adım farkını saymıyor, çünkü koşum
tek bir erişimle sınırlı ve süre bu müfredatta hiçbir yerde ölçülmüyor; önemli olan,
atlanan adımın hangi denetim olduğunun kaynaktan okunabilir olmasıdır — get_unchecked
adı, sınır denetiminin atlandığını doğrudan söyler, gizlemez.
Sınırlayıcı Ölçüm
Bu üç işlemin ortak özelliği, doğru kullanıldıklarında sonuçta hiçbir iz bırakmamalarıdır.
Yukarıdaki koşumda hem ham okuma hem denetimsiz erişim hem sözleşmeli çağrı, güvenli
karşılıklarıyla aynı sayıyı verdi — bir gözlemci yalnız çıktıya bakarak hangi satırın
unsafe olduğunu ayırt edemez. Fark sonuçta değil, yanlış kullanıldığında ne olacağının
kimin sorumluluğunda olduğundadır.
Yanlış kullanım bu derste koşturulmuyor, çünkü sınırın dışına taşan bir okuma tanımsız
davranıştır ve sonucu bu makineye, bu koşuma özgüdür — bir sonraki koşumda aynı hatayı
tekrar üretmeyebilir, sessizce yanlış bir değer okuyabilir ya da çöküşe yol açabilir.
Düzyazıyla tarif etmek yeterli: isaretci.add(100) çağrısı, dört elemanlık dilimin çok
ötesine geçen bir adresi okumaya çalışırdı. Derleyici bu çağrıyı reddetmez — çünkü reddetmek
için adresin ne gösterdiğini bilmesi gerekirdi ve bilmiyor. Aynı şekilde, ikinci_ogeyi_al
işlevi iki elemandan az bir dilimle çağrılsaydı, sözleşme ihlal edilmiş olurdu ve derleyici
bunu da fark etmezdi; imza yalnızca çağıranın bir sözü tuttuğunu varsayar, sözü sınamaz.
Bu, unsafe denetiminin ne yaptığını netleştiriyor: derleyici artık “bu doğru mu” diye
sormuyor, yalnızca “bu, kaynakta izin verilmiş bir yazım mı” diye soruyor. Sınırın nerede
durduğunu ölçmek, sınırın ötesinde ne olduğunu koşturmaktan ayrı bir iştir — bu ders
yalnız ilkini yapıyor.
Bu ayrım, unsafe işlevlerin neden bir yazım kuralına ihtiyaç duyduğunu da açıklıyor.
ikinci_ogeyi_al gibi bir işlevin üstüne, hangi ön koşulun beklendiğini anlatan bir
belge satırı eklemek dilin zorunlu tuttuğu bir şey değildir — ama yaygın bir yazım
alışkanlığıdır, çünkü imzadaki unsafe sözcüğü tek başına “hangi” ön koşulu söylemez,
yalnızca “bir” ön koşul olduğunu söyler. Bu koşumdaki işlev bu belgeyi bir satırlık bir
yorumla taşıyor; Belgelendirme dersi ileride bu tür bir belge satırının doğruluğunun da
kendi başına denetlenmediğini, ayrı bir sınıfa (denetlenmiyor) düştüğünü gösterecek.
Burada önemli olan, unsafein kendisinin bu belgeyi zorunlu kılmaması, yalnızca dilin
kaynakta görünür bir işaret bırakmasıdır: unsafe sözcüğü olmadan bu işlev hiç
çağrılamaz, ama sözcüğün varlığı, çağıranın gerçekten doğru bir dilim geçirdiğini
göstermez.
Özet
unsafebloğu derleyicinin bütün denetimini kapatmaz; yalnız birkaç belirli işlemi (ham işaretçi okuma/yazma,unsafeişlev çağrısı gibi) kapsam dışına alır — aynı bloktaki güvenli bir çağrı (v.get) hâlâ sürdürüyor sınıfında kaldı- Ham işaretçi aritmetiğiyle okuma, dilimin denetimsiz erişim yöntemi ve sözleşmeli bir
unsafeişlevin çağrısı — üçü de denetimin yazana devredildiği işlemlerdir - Devredilen üç işlemin üçü de doğru kullanıldığında güvenli karşılığıyla birebir aynı sonucu verdi; fark sonuçta değil, yanlış kullanımın sorumluluğunda
- Ödünç denetleyicisi
unsafebloğu içinde de çalışmaya devam eder — kapanan bir denetleyici değil, yalnızca açılan birkaç işlem söz konusudur unsafeişaretli bir işlevin imzası, gövdesindeki ön koşulu çağırana görünür kılan bir yazım kalıbıdır; derleyici bu ön koşulu sınamaz, yalnız varlığını beyan ettirir
Sonraki Adım
Bu derste devredilen güvence hep aynı programın içinde kaldı — ham işaretçi de, denetimsiz erişim de Rust’ın kendi bellek düzenini okuyordu; derleyici, işaretçinin gösterdiği belleğin hangi tipten olduğunu ve dilimin gerçek uzunluğunu her zaman biliyordu, yalnız bu bilgiyi okuma anında sınamayı bırakmıştı. Sıradaki ders aynı devri bir adım öteye taşıyor: gövdesini derleyicinin hiç görmediği, başka bir dille yazılmış bir işlevi çağırmak. Orada sözleşme yalnız bir ön koşul değil, bir imzadır — derleyici artık karşı tarafın gövdesine hiç bakmadan, yalnız yazılan bildirime güvenerek derler.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.