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,
devredildiiledilin dışındası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.