İçeriğe geç
academia.sh

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. &deger 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

  • unsafe bloğu derleyicinin bütün denetimini kapatmaz; yalnız birkaç belirli işlemi (ham işaretçi okuma/yazma, unsafe iş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 unsafe iş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 unsafe bloğu içinde de çalışmaya devam eder — kapanan bir denetleyici değil, yalnızca açılan birkaç işlem söz konusudur
  • unsafe iş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.

Aramak için yazmaya başlayın.

↑↓ ↵ Esc gezin · aç · kapat