İçeriğe geç
academia.sh

Ders 05 / 12

Yabancı Fonksiyon Arayüzü

Derleyicinin gövdesini hiç görmediği bir C işlevinin, yalnız bildirilen imzasına güvenilerek çağrılması; iki yabancı çağrının sonucunun Rust'ın kendi karşılığıyla eşleştiği koşturularak gösterilir ve bildirimin doğruluğunun yazanın sorumluluğunda olduğu açıklanır.

Yayın Güncelleme

Bu derste

  • extern "C" bildirimiyle çağrılan bir işlevin gövdesini derleyicinin hiç görmediğini, yalnız bildirilen imzaya güvendiğini açıklar
  • İki yabancı çağrının sonucunu Rust'ın kendi karşılığıyla karşılaştırarak yalnızca imzaya güvenmenin doğru sonuç ürettiğini kanıtlar
  • Yanlış yazılmış bir bildirimi derleyicinin fark edemeyeceğini, doğruluğun yazanın sorumluluğunda olduğunu örnekle açıklar
İçindekiler

Önceki ders, güvencenin unsafe bir işlevin imzasına devredildiğini gösterdi: ikinci_ogeyi_al işlevinin gövdesi Rust’la yazılmıştı, derleyici o gövdeyi okuyabilirdi, yalnızca ön koşulu kendiliğinden sınamıyordu. Bu ders aynı devri bir adım öteye taşıyor: gövdesi Rust’la yazılmamış, derleyicinin hiç görmediği bir işlevi çağırmak. Orada sözleşme artık bir ön koşul cümlesi değil, yalnızca bir imzadır — parametre sayısı, parametre tipleri, dönüş tipi.

Bu sınırın var olma nedeni pratiktir: bir programın ihtiyaç duyduğu her işlevi kendi dilinde yeniden yazmak zorunlu değildir. İşletim sisteminin sağladığı çağrılar, uzun süredir kullanılan sayısal ve metin işleme kitaplıkları, donanıma özgü sürücüler — bunların büyük bölümü C ile yazılmıştır ve C’nin ikili uyumluluk kuralı, bir dile özgü olmayan ortak bir sınır görevi görür. Rust bu sınırı yeniden icat etmez; C’nin çağrı kuralını doğrudan konuşabilen bir yazım sunar ve karşı tarafın gövdesine dokunmadan onunla iletişim kurar.

Yalnızca İmzaya Güvenerek Çağırmak

Bir C kitaplığı işlevini Rust’tan çağırmak için o işlevin kaynağını görmeye, hatta aynı makinede derlenmiş olmasına bile gerek yoktur — yaygın C işlevleri, işletim sisteminin kendi standart kitaplığında zaten bağlanmış durumdadır. Rust programı yalnız bir bildirim yazar: işlevin adı, aldığı parametrelerin tipleri, döndürdüğü değerin tipi. Bu bildirim bir extern "C" bloğunun içine konur; "C" sözcüğü, çağrının hangi ikili uyumluluk kuralına (parametrelerin yığıtta ya da yazmaçlarda hangi sırayla taşınacağına) göre yapılacağını söyler — Rust’ın kendi işlevleri farklı bir kuralla derlenebilir, C kitaplığıyla konuşmak için bu kuralın açıkça C’ninkine çevrilmesi gerekir.

Bildirim yazıldıktan sonra ek bir derleme adımı ya da bağlama adımı gerekmez: derleyici, programı her zaman bağladığı sistem kitaplığına bu iki adı da arar ve bulur. Parametre tipleri de elle uydurulmaz; std::os::raw modülü, c_int ve c_char gibi, C’nin int ve char tiplerinin bu platformdaki genişliğine karşılık gelen adlar tanımlar — bunlar Rust’ın kendi i32 ya da u8 tipleriyle aynı bellek düzenini paylaşır, ama adlandırma bildirimin C tarafıyla eşleştiğini kaynaktan okunur kılar.

Sınır yalnız bu yönde işlemez. Bir Rust işlevi de, C tarafından çağrılabilecek bir imzayla dışarı açılabilir; bu durumda derleyici o işlevin adını değiştirmeden bırakır ki bağlayıcı onu C’nin beklediği adla bulabilsin. Bu ders yalnız Rust’tan C’ye giden yönü ölçüyor — abs ve strlen her ikisi de sistemin sağladığı, Rust programının çağırdığı taraf. Ters yön aynı sözleşme kuralına dayanır, yalnızca kaynakta hangi tarafın bildirim yazdığı değişir.

Bildirimin kendisi ile bildirilen işlevi çağırmak aynı statüyü taşımaz. Bir extern "C" bloğu yazmak, önceki derste ölçülen ham işaretçiyi kurmaya benziyor: derleyici yalnızca bir adı ve bir imzayı kaydeder, henüz karşı tarafa geçmemiştir. Tehlike, o adın gösterdiği yere gerçekten atlandığında başlar — bu yüzden yalnızca çağrı unsafe bloğu ister, bildirimin kendisi istemez.

// bildirim.rs — bildirimin kendisi unsafe gerektirmiyor, cagirmak gerektiriyor
use std::os::raw::c_int;

extern "C" {
    fn abs(x: c_int) -> c_int;
}

fn main() {
    // Islevi bir gosterge olarak tasimak onu cagirmaz; unsafe gerekmiyor.
    let gosterge: unsafe extern "C" fn(c_int) -> c_int = abs;
    println!("bildirim derlendi, cagrilmadi: {}", gosterge as usize != 0);
}
bildirim derlendi, cagrilmadi: true

Bu koşum absi hiç çağırmadı; yalnızca bildirilen işlevi bir değişkene taşıdı ve o değişkenin boş olmadığını (sıfır adresi göstermediğini) doğruladı — gerçek adresin kendisi hiçbir yerde basılmadı. DS6. Derleme bu satırda hiçbir uyarı vermedi çünkü bildirilen işlev gerçekten kullanıldı, yalnızca çağrılmadı; kullanılmayan bir bildirim derleyici tarafından ölü kod olarak işaretlenirdi. Bildirimin kendisinin unsafe istememesi, dilin “bu isim bir yere işaret ediyor” demekle “o yere şimdi atlıyorum” demek arasında kurduğu ayrımın bir başka örneğidir — önceki derste ham işaretçi kurmak ile onu okumak arasındaki ayrımın aynısı.

İki Yabancı Çağrının Ölçümü

Aşağıdaki koşum iki yabancı işlevi çağırıyor: mutlak değer alan abs ve bir bayt dizisinin uzunluğunu sayan strlen. İkisinin de gövdesi bu programın içinde yok — yalnız bildirimleri var. Sonuçlar, aynı işi yapan Rust karşılıklarıyla karşılaştırılıyor.

DS7. Mutlak değeri alınan sayı -7; uzunluğu ölçülen dizge "merhaba" (yedi bayt, hiçbir çok baytlı karakter taşımıyor).

// yabanci.rs — iki yabanci cagrinin sonucu ve rust karsiligi
use std::ffi::CString;
use std::os::raw::{c_char, c_int};

extern "C" {
    fn abs(x: c_int) -> c_int;
    fn strlen(s: *const c_char) -> usize;
}

fn main() {
    let mutlak = unsafe { abs(-7) };
    let metin = CString::new("merhaba").unwrap();
    let uzunluk = unsafe { strlen(metin.as_ptr()) };

    println!("yabanci mutlak: {} (rust karsiligi {})", mutlak, (-7i32).abs());
    println!("yabanci uzunluk: {} (rust karsiligi {})", uzunluk, "merhaba".len());
}
yabanci mutlak: 7 (rust karsiligi 7)
yabanci uzunluk: 7 (rust karsiligi 7)

İki çağrı da unsafe bloğunun içinde — ama önceki dersteki unsafeden farklı bir gerekçeyle. Orada tehlike, dilimin sınırlarının dışına taşan bir okumaydı; burada tehlike, derleyicinin karşı tarafın hiçbir şeyini bilmemesidir. abs ve strlen işlevlerinin kaynak kodu bu programın hiçbir yerinde yok; derleyici onları görmeden, yalnız yukarıdaki iki satırlık bildirime güvenerek çağrıyı üretti. DS8. Sonuç iki satırda da birebir eşleşti: yabancı abs çağrısı 7 döndürdü, Rust’ın kendi abs yöntemi de 7; yabancı strlen çağrısı 7 döndürdü, Rust’ın len yöntemi de 7. Eşleşme tesadüf değil — bildirimin C tarafındaki gerçek imzayla birebir örtüşmesinin sonucu.

CString::new çağrısı burada bilinçli bir adım: Rust’ın kendi dizgi tipi, sonunda sıfır baytı bulundurmaz ve uzunluğunu ayrı bir sayaçta tutar; C’nin dizgi kuralı ise sonu sıfır baytıyla biten bir bayt dizisidir. strleni doğrudan bir Rust dizgesiyle çağırmak, iki kuralın farklı olduğu bir sınırda tip uyuşmazlığına yol açar; CString, aradaki çeviriyi yaparak sonunda sıfır baytı olan bir bayt dizisi kurar ve yalnız o dizinin başlangıç adresini strlene geçirir.

Sınırın İki Tarafındaki Ayrı Kurallar

Bu derste sınırı geçen değerler yalnızca tek tam sayılar ve bayt dizileri; bir yapı (struct) sınırı geçseydi ayrı bir kural daha devreye girerdi. Rust’ın kendi yapı düzenleyicisi, alanları derleme sırasında istediği gibi yeniden sıralayabilir — bu, programın davranışını değiştirmeyen, yalnız bellekteki yerleşimi etkileyen bir serbestliktir. C tarafı ise bir yapının alanlarını bildirildiği sırayla, hizalama kurallarına göre art arda yerleştirmeyi garanti eder. İki taraf da aynı adları taşıyan bir yapı üzerinde anlaşsa bile, biri alanları yeniden sıralarken öteki sıralamayı sabit tutuyorsa, aynı bellek bloğu iki farklı yorumla okunur. Bu ders yapı geçirmiyor, yalnızca tam sayı ve işaretçi geçiriyor; sınırın bu parçası burada ölçülmüyor, yalnızca var olduğu kaynaktan anılıyor.

abs ve strlen seçilme nedeni, ikisinin de belleği yalnızca okumasıdır — hiçbiri yeni bir bellek bloğu ayırıp Rust tarafına bırakmıyor. Bu, FFI sınırındaki en önemli kısıtlardan birini gösteriyor: bir C işlevi kendi içinde bellek ayırıp bir işaretçi döndürseydi, o belleği kimin, hangi ayırıcıyla serbest bırakacağı ayrı bir sözleşme konusu olurdu. Rust’ın kendi büyüyen dizisi ve C’nin malloc işlevi aynı ayırıcıyı kullanmak zorunda değildir; C tarafında ayrılan bir bellek bloğunu Rust’ın kendi bırakma kuralıyla serbest bırakmaya kalkmak, iki ayrı ayırıcının defterini karıştırır ve bu da tanımsız davranış üretir. Bu ders bu durumu koşturmuyor — abs bir tam sayı, strlen bir uzunluk döndürüyor, ikisi de yeni bellek ayırmıyor — ama sınırın bu kısmının varlığını kaynaktan görünür kılmak için anılıyor.

Sınırın her parçası devredildi ya da dilin dışında sınıfına düşmüyor; CString::new çağrısı bunlardan birine ait değil. C’nin sıfır baytıyla biten dizgi kuralı, bir dizginin içinde sıfır bayt taşıyamayacağı anlamına gelir — sıfır bayt görüldüğü an dizgi orada biter. CString::new, verdiği bayt dizisinin içinde bir sıfır bayt olup olmadığını çalışma zamanında sınar ve varsa çağrıyı panik ile değil bir hata değeriyle durdurur.

// sifir_bayt.rs — CString::new govde icindeki sifir bayti denetliyor
use std::ffi::CString;

fn main() {
    let temiz = CString::new("merhaba");
    let bozuk = CString::new("me\0rhaba");

    println!("temiz dizge kuruldu mu: {}", temiz.is_ok());
    println!("govde ici sifir bayt reddedildi mi: {}", bozuk.is_err());
}
temiz dizge kuruldu mu: true
govde ici sifir bayt reddedildi mi: true

DS9. Bu, sınırın strlen çağrısındaki devredilmiş kısmından farklı bir yer: burada denetim derleyicide değil çalışma zamanında yapılıyor, ama yine de yapılıyor — Result tipi çağırana hatayı görmezden gelme izni vermiyor. Sınırın tamamı yazana devredilmiş değildir; C’nin kendi biçim kuralına uymayan bir girdi, daha yabancı çağrıya hiç ulaşmadan durduruluyor.

Hata bildirme biçimi de sınırın iki tarafında ayrıdır. Rust’ta bir işlemin başarısızlığı genellikle bir Result değeriyle taşınır ve derleyici bu değerin ele alınıp alınmadığını denetler. C’nin böyle bir tipi yoktur; bir işlevin başarısız olduğu genellikle özel bir dönüş değeriyle (örneğin negatif bir sayı ya da sıfır) ya da ayrı bir global durumla işaretlenir. extern "C" bildirimi bu farkı ortadan kaldırmaz — bildirilen dönüş tipi c_int ise, o değerin bir hata kodu mu yoksa sıradan bir sonuç mu olduğunu bilmek yine çağıranın C tarafının belgesini okumasına bağlıdır; derleyici bu ayrımı tipten çıkaramaz, çünkü tip ikisi için de aynıdır.

Karakter kodlaması, strlenin döndürdüğü sayının ne anlama geldiği konusunda ayrı bir tuzak taşıyor. Rust’ın kendi len yöntemi de, strlen de aynı şeyi sayar: dizginin bayt uzunluğunu — dizginin görsel olarak kaç karakterden oluştuğunu değil. Bu derste kullanılan "merhaba" yalnızca tek baytlık karakterlerden oluştuğu için bu ayrım görünmedi; birden çok baytla kodlanan bir karakter taşıyan bir dizgede iki sayı birbirinden ayrılır.

// bayt_karakter.rs — bayt uzunlugu ile karakter sayisinin ayrilmasi
use std::ffi::CString;
use std::os::raw::c_char;

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

fn main() {
    let dizge = "şafak";
    let dizi = CString::new(dizge).unwrap();
    let yabanci_uzunluk = unsafe { strlen(dizi.as_ptr()) };

    println!("rust bayt uzunlugu: {}", dizge.len());
    println!("rust karakter sayisi: {}", dizge.chars().count());
    println!("yabanci strlen: {}", yabanci_uzunluk);
}
rust bayt uzunlugu: 6
rust karakter sayisi: 5
yabanci strlen: 6

DS10. dizge.len() ile yabancı strlen çağrısı burada da birebir eşleşti (6) — ikisi de bayt sayıyor. Ayrı düşen, chars().count() yöntemiyle bulunan karakter sayısı (5): dizgedeki ş harfi Rust kaynağında tek bir karakter olarak yazılsa da UTF-8 kodlamasında iki bayt tutuyor. strlene güvenen bir C tarafı bu sayıyı her zaman bayt olarak okur; onu karakter sayısı sanan bir çağıran, tek baytlık olmayan bir dizgede yanlış bir varsayımla ilerlemiş olur. Derleyici bu yanlış varsayımı da fark edemez — dönüş tipi usizedir ve usizein bir bayt sayısı mı yoksa karakter sayısı mı taşıdığını söylemez.

Sınırlayıcı Ölçüm

Bu iki çağrının doğru çalışması, bildirimin doğru yazılmış olmasına bağlıydı — ve bu doğruluğu derleyici hiçbir noktada sınamadı. absin gerçek C imzası tek bir tam sayı alıp tek bir tam sayı döndürür; bildirim bunu doğru yazdığı için çağrı doğru çalıştı. Bildirim yanlış yazılsaydı — örneğin absin iki parametre aldığı ya da bir kayan noktalı sayı döndürdüğü bildirilseydi — derleyici bunu fark edemezdi. Derleyicinin elinde karşı tarafın gövdesi yok; yalnızca yazılan bildirime bakıyor ve o bildirime uygun bir çağrı üretiyor. Yanlış bir bildirimin sonucu bu derste koşturulmuyor, çünkü sonucu bu makineye özgü ve tanımsız davranış sınıfına girer — yığıttaki değerler yanlış tipte yorumlanabilir, program yanlış bir sayı üretebilir ya da çökebilir.

Bu, devredildi ile dilin dışında sınıflarının kesiştiği yerdir. Denetim, önceki derste olduğu gibi yazana devredildi — ama burada devredilen şey bir ön koşul cümlesi değil, karşı tarafın doğruluğuna dair hiçbir garantisi olmayan bir imzadır. Karşı tarafın kendisi Rust’ın dışında yazılmış, Rust’ın derleme sürecinin dışında derlenmiş ve sistem tarafından zaten bağlanmıştır; derleyicinin ona dair tek bilgisi, programcının yazdığı iki satırdır. Bildirim doğruysa iki dil arasındaki sınır görünmez olur — yukarıdaki koşum tam olarak bunu gösterdi. Bildirim yanlışsa, hatanın nerede olduğunu derleyici değil, yalnızca çalışma zamanındaki gözlem ortaya çıkarabilir.

Bu sınırın genişliği, tek bir yanlış bildirimle değil, bildirimin her alanıyla ölçülür. Parametre sayısı yanlışsa, çağrı yığıttan ya da yazmaçlardan okuduğu yerleri kaydırır ve sonraki her parametre de yanlış bir değer taşır. Parametre tipi doğru genişlikte değilse — örneğin gerçek C imzası dört baytlık bir tam sayı beklerken bildirim sekiz baytlık bir tam sayı yazıyorsa — çağıran taraf, karşı tarafın okumadığı fazladan baytları da göndermiş ya da eksik göndermiş olur. Dönüş tipi yanlışsa, çağrının döndürdüğü baytlar yanlış bir tipe göre yorumlanır. Üç durumun da ortak noktası, derleyicinin hiçbirini reddetmemesidir: extern "C" bloğu sözdizimsel olarak geçerli bir bildirimdir, C tarafındaki gerçek imzayla örtüşüp örtüşmediği derlemenin konusu değildir.

Özet

  • extern "C" bloğu, gövdesi Rust’ın dışında yazılmış bir işlevi yalnızca imzasıyla bildirir; derleyici o gövdeyi hiçbir zaman görmez, güvencesi tamamen dilin dışında kalır
  • C kitaplığının yaygın işlevleri sistemde zaten bağlıdır; bildirim yazıldıktan sonra ek bir derleme ya da bağlama adımı gerekmez
  • İki yabancı çağrının (abs, strlen) sonucu, Rust’ın kendi karşılığıyla birebir eşleşti — eşleşme, bildirimin gerçek C imzasıyla doğru yazılmış olmasının sonucudur
  • CString, Rust’ın uzunluk sayaçlı dizgesi ile C’nin sıfır baytıyla biten dizisi arasındaki kural farkını, sınırda açık bir dönüşümle kapatır
  • Bildirim yanlış yazılsaydı derleyici bunu fark edemezdi; doğruluk tamamen yazanın sorumluluğundadır — bu, devredildi ile dilin dışında sınıflarının kesiştiği nokta
  • Sınırın her parçası devredilmiş değildir: CString::new, gövde içinde bir sıfır bayt olup olmadığını çalışma zamanında sınar ve varsa bir hata değeriyle durdurur — bu denetim çalışma zamanında olsa da hâlâ sürdürüyor

Sonraki Adım

Bu derste sınır, iki dil arasındaydı; bildirim doğruysa hangi makinede koştuğunun önemi yoktu — abs ve strlen çağrıları, program hangi işlemcide ya da hangi işletim sisteminde koşarsa koşsun aynı sonucu üretti, çünkü ikisi de sistemin kendi standart kitaplığına bağlanıyordu. Sıradaki ders bu bağımsızlığı tersine çeviriyor: aynı kaynağın, koştuğu hedefe göre farklı bir gövdeye ihtiyaç duyması. Orada güvence bir bildirime değil, hedefin kendi verdiği kısıtlara bağlı olacak — ve o kısıt, bu derste olduğu gibi bir dış kitaplığın imzasında değil, kaynağın kendisinde koşullu olarak yazılacak.

İ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