Ders 02 / 12
Prosedürel Makrolar
Bir program std::process::Command ile derleyiciyi iki kez çağırır — önce bir prosedürel makro kasasını, sonra onu kullanan kaynağı — ve yalnız derlendi/derlenmedi ile çıktı eşleşmesini basar; üretilen gövde yine sürdürüyor sınıfına düşer, ama bu kez üretim aynı dosyada değil, ayrı bir derleme adımında gerçekleşir.
Yayın Güncelleme
Bu derste
- Bir prosedürel makronun neden kendi kasasında derlenmek zorunda olduğunu açıklar
- Üretim adımını program içinden iki ayrı derleyici çağrısıyla kurar ve yalnız derlendi/derlenmedi ile çıktı eşleşmesini basar
- Ayrı bir derleme adımında üretilen gövdenin de tür denetiminden geçtiğini koşturarak gösterir
İçindekiler
Önceki ders, bir makro tanımının ve makronun çağrısının aynı dosyada durabildiğini gösterdi: derleyici tek bir çağrıda hem tanımı okudu, hem genişletti, hem de ürettiği kaynağı denetledi. Bu ders aynı “sürdürüyor” sınıfını bambaşka bir kurgudan ölçüyor. Bir prosedürel makro, kaynak metnini elle işleyen ve yeni kaynak metni döndüren sıradan bir Rust işlevidir — ama bu işlev kendi başına ayrı bir kasada (crate) derlenmiş olmak zorundadır ve onu kullanan kaynak, ancak derleyici o kasayı çoktan derlenmiş bir ikili olarak bulduğunda çalışabilir. Üretim artık aynı okumada değil, iki ayrı derleme adımında gerçekleşiyor.
Bu iki adımlılık, dilin kendi kelime dağarcığında da görünür. Bildirimsel makrolar dilin
kendi desen eşleme düzeneğiyle çalışıyordu — bir match dalının bir değerle karşılaştığı
gibi, gelen andaçlar bir desenle karşılaştırılıyor ve yerine sabit bir kalıp konuyordu.
Prosedürel bir makro böyle bir desen eşleme düzeneğine bağlı değil: girdi olarak aldığı
andaç akışını istediği
herhangi bir Rust kodu gibi işleyebilir — döngü kurabilir, koşul yazabilir, bir yardımcı veri
yapısında biriktirebilir — ve sonunda yeni bir andaç akışı üretip döndürebilir. Bu, bildirimsel
makronun sözdizimsel şablonculuğundan farklı bir güç: üretim artık programlanabilir. Bu
gücün bedeli de burada başlıyor — programlanabilir bir üretim, derleyicinin çalıştırabileceği
bağımsız bir program gerektiriyor ve bağımsız bir programın kaynağın geri kalanıyla aynı
okumada var olması mümkün değil.
Bu farkı tek bir soruyla özetlemek mümkün: bir bildirimsel makro yalnız desen eşleyip yer değiştirir, bir prosedürel makro ise koşar. Aradaki fark, çekirdeğin hangi araçla üretildiğine değil, üretimin ne zaman ve nerede gerçekleştiğine bakan bu kursun ölçü ekseniyle birebir örtüşüyor.
Üretim Neden Ayrı Bir Adıma Taşınıyor
Bir prosedürel makro işlevi, girdi olarak bir andaç akışı (TokenStream) alır ve çıktı olarak
başka bir andaç akışı döndürür. Bu dönüşüm çalışma zamanında değil, derleme zamanında
çalışır — ama çalışması için derleyicinin kendisinin, henüz derlemekte olduğu kaynağı işlerken,
bu işlevi bir alt program gibi çağırabilmesi gerekir. Bir işlevin derleme sürecinin
ortasında çağrılabilmesi için, o işlevin derleme başlamadan önce zaten bağlanabilir bir ikili
olarak var olması gerekiyor — kaynağın geri kalanıyla aynı okumada, aynı anda derlenmesi
mümkün değil. Bu yüzden dilin kendisi bir kısıtlama koyuyor: prosedürel makro içeren bir kasa,
proc-macro adlı ayrı bir kasa türüyle derlenmek zorunda ve onu kullanan her kasa, bu kasayı
kendi derlemesine bir bağımlılık olarak, önceden derlenmiş halde almak zorunda.
Bu, önceki dersteki bildirimsel makrodan temel bir ayrım. Orada makro tanımı ve çağrısı aynı dosyada durabiliyordu, çünkü genişleme metinsel bir yer değiştirmeydi ve derleyici bunu tek okumada yapabiliyordu. Burada üretim bir metin yer değiştirmesi değil, çalıştırılan bir programın çıktısı; o program önce kendi başına, tam bir derleme adımıyla var edilmek zorunda.
Rust’ta üç ayrı prosedürel makro biçimi var: girdi olarak bir çağrı süsü alan biçim, bir
öznitelik satırının etkilediği öğeyi alan öznitelik makrosu ve türetme özniteliğinin
kendisini genişleten türetme makrosu. Üçü de aynı kısıtı taşıyor — üçü de kendi kasalarında
derlenmek zorunda — ve üçü de aynı türde bir işlev imzasını paylaşıyor: bir andaç akışı
girdi, bir andaç akışı çıktı. Öznitelik makrosu bir işlev ya da yapı bildiriminin üstüne
yazılan bir satırla tetiklenir ve altındaki öğeyi bir andaç akışı olarak alır; türetme makrosu
ise Türetilen Trait’ler dersinde görülen #[derive(...)] satırının kendisini genişletir ve
yalnız yeni bir gerçekleştirim üretebilir, üzerine yazıldığı öğeyi değiştiremez. Bu ders en
yalın biçimi, çağrı süslü olanı, ölçüyor; çünkü bu biçim girdisini ayrıştırmadan da sabit bir
çıktı üretebiliyor ve ölçümün odağını üretim mekaniğinden uzaklaştırmıyor.
Ölçüm: İki Aşamalı Derleme
Ölçüm program içinden kurulur. Bir Rust programı, std::process::Command ile derleyiciyi iki
kez çağırıyor: önce prosedürel makro kasasını --crate-type proc-macro ile derliyor, sonra
onu kullanan kaynağı --extern ile bu kasaya bağlayıp derliyor ve son olarak üretilen ikiliyi
çalıştırıp çıktısını bekleneni karşılaştırıyor. Kalıp, Rust’ın Tasarım Hedefleri dersinde
Go’nun Temelleri kursundan (M09/K01) alınan desenle aynı: derleyici bir alt süreç olarak
çağrılıyor, yalnız çıkış durumu ve çıktısı okunuyor; hata metni, yol, süre ve makine adı hiç
okunmuyor.
Ölçümün kendisi iki şeyi birden sağlıyor. Birincisi, ölçüm belirlenimci: iki kaynak da programın içinde sabit dizgi olarak duruyor, hiçbiri dışarıdan okunmuyor, bu yüzden aynı program her koşumda aynı iki kaynağı derliyor ve aynı üç satırı basıyor. İkincisi, ölçüm kendi izini siliyor: çalışma dizini geçici bir klasörde açılıyor ve programın son satırı bu klasörü tamamen kaldırıyor — ne kalıcı bir dosya bırakıyor ne de bir sonraki koşumu önceki koşumun kalıntısından etkiliyor.
MK4. İki kasanın kaynak metni birer sabit dizgi olarak programın içinde tutuluyor. Kasa
kaynağı extern crate proc_macro; satırıyla başlıyor — bu, derleyicinin sysroot’unda hazır
duran, üçüncü taraf olmayan tek kasayı içe alıyor. Üretilen andaç akışı, elle andaç ağacı
kurmak yerine bir dize sabitinin ayrıştırılmasıyla (.parse().unwrap()) kuruluyor; bu, kasa
dışı hiçbir bağımlılık gerektirmeyen en doğrudan yol.
Gerçek prosedürel makrolar çoğunlukla girdi andaç akışını da işler — hangi adın, hangi
alanların geçtiğine bakıp çıktıyı ona göre biçimlendirir. Andaç akışını elle, andaç ağacı
kurarak ayrıştırmak, üçüncü taraf kasa kullanmadan da mümkündür, ama bu ders girdiyi hiç
okumuyor: _girdi parametresi kullanılmadan bırakılıyor, çünkü ölçülen şey andaç işleme
becerisi değil, üretimin hangi derleme adımında gerçekleştiği. Girdiyi okuyup ona göre
dallanan bir makro da aynı iki aşamalı derlemeye tabi olurdu; girdiyi okumak üretimin yerini
değiştirmez, yalnız üretilen metnin içeriğini değiştirir.
// olusum.rs — bir prosedurel makro kasasi program icinden derlenir, sonra
// onu kullanan kaynak ikinci bir cagriyla derlenip kosturulur.
use std::env;
use std::fs;
use std::path::PathBuf;
use std::process::Command;
const KASA_KAYNAK: &str = r#"
extern crate proc_macro;
use proc_macro::TokenStream;
#[proc_macro]
pub fn govde_uret(_girdi: TokenStream) -> TokenStream {
"fn uretilen_deger() -> i32 { 42 }".parse().unwrap()
}
"#;
const TUKETEN_KAYNAK: &str = r#"
makro_kasa::govde_uret!();
fn uygula<F: Fn() -> i32>(islev: F) -> i32 {
islev()
}
fn main() {
let dogrudan: i32 = uretilen_deger();
let genel_sinir_icinde: i32 = uygula(uretilen_deger);
println!("uretilen_deger={} genel_sinir_icinde={}", dogrudan, genel_sinir_icinde);
}
"#;
const BEKLENEN_CIKTI: &str = "uretilen_deger=42 genel_sinir_icinde=42\n";
fn calisir_mi(komut: &mut Command) -> bool {
komut.output().map(|c| c.status.success()).unwrap_or(false)
}
fn siniflandir(basarili: bool) -> &'static str {
if basarili { "derlendi" } else { "derlenmedi" }
}
fn main() {
let calisma = env::temp_dir().join(format!("prosedurel-{}", std::process::id()));
fs::create_dir_all(&calisma).unwrap();
let kasa_yolu = calisma.join("kasa.rs");
fs::write(&kasa_yolu, KASA_KAYNAK).unwrap();
let kasa_ikili: PathBuf = calisma.join(format!("libmakro_kasa.{}", env::consts::DLL_EXTENSION));
let kasa_derlendi = calisir_mi(
Command::new("rustc")
.args(["--edition", "2021", "--crate-type", "proc-macro", "--crate-name", "makro_kasa"])
.arg("-o").arg(&kasa_ikili)
.arg(&kasa_yolu),
);
println!("kasa: {}", siniflandir(kasa_derlendi));
let tuketen_yolu = calisma.join("tuketen.rs");
fs::write(&tuketen_yolu, TUKETEN_KAYNAK).unwrap();
let tuketen_ikili = calisma.join("tuketen");
let tuketen_derlendi = kasa_derlendi
&& calisir_mi(
Command::new("rustc")
.args(["--edition", "2021"])
.arg("--extern").arg(format!("makro_kasa={}", kasa_ikili.display()))
.arg("-o").arg(&tuketen_ikili)
.arg(&tuketen_yolu),
);
println!("tuketen: {}", siniflandir(tuketen_derlendi));
let eslesti = tuketen_derlendi
&& Command::new(&tuketen_ikili)
.output()
.map(|c| c.stdout == BEKLENEN_CIKTI.as_bytes())
.unwrap_or(false);
println!("cikti eslesti: {}", eslesti);
let _ = fs::remove_dir_all(&calisma);
}
kasa: derlendi tuketen: derlendi cikti eslesti: true
MK5. İkili dosyanın uzantısı hiçbir yerde sabit yazılmıyor; env::consts::DLL_EXTENSION
platformun kendi paylaşımlı kütüphane uzantısını veriyor ve program bunu okuyup dosya adına
katıyor. Bu, gerçek bir makine adı ya da dizin yolu basmadan taşınabilir bir örnek kurmanın
yolu: uzantı ekrana hiç yazılmıyor, yalnız dosya adının içinde, görünmeden kullanılıyor.
--extern makro_kasa=<yol> bağı, ilk derlemenin ürettiği ikiliyi ikinci derlemeye bir kasa
adıyla tanıtıyor — tüketen kaynak, üretim makrosunun çağrısını tıpkı standart kitaplığın
herhangi bir kasasını kullanır gibi, kasa adını önüne koyarak yazıyor.
Bu bağlama adımı, bir paket kaydı ya da bir bağımlılık çözümleyici gerektirmiyor; --extern
yalnız bir ad ile bir dosya yolunu eşliyor ve derleyici o yoldaki ikiliyi doğrudan okuyor.
Ölçüm bu yüzden bir ağ bağlantısı ya da bir dış kayıt defteri açmadan, yalnız iki rustc
çağrısı ve aralarında geçen bir dosya yoluyla kuruluyor — kursun üçüncü taraf kasa
kullanmama kuralı burada da bozulmuyor: bağlanan tek kasa, programın kendi ürettiği
makro_kasa.
Sonuç üç satır: kasa derlendi, tüketen kaynak derlendi, çıktı bekleneni karşıladı. Üçü de
sınıflandırıcı sonuçlar — hiçbiri hata metni, süre ya da yol taşımıyor. tuketen kaynağı
üretilen işlevi iki ayrı biçimde kullanıyor: bir kez doğrudan çağrıyla, bir kez de uygula
işlevinin F: Fn() -> i32 sınırının içinden. İkisi de aynı sonucu (42) veriyor ve ikisi de
derleniyor — üretilen gövde, önceki derste görüldüğü gibi burada da bir genel sınırın içinde
sıradan bir işlevmiş gibi denetleniyor. Fark yalnız üretimin nerede olduğunda: önceki
derste üretim aynı okumanın içindeydi, burada üretim çoktan bitmiş, derlenmiş bir ikili olarak
ikinci derlemeye giriyor.
MK6. tuketen kaynağının genel sınırı sınayan ikinci satırı tesadüfen eklenmiş bir
süsleme değil: bu ders, önceki dersteki “sürdürüyor” iddiasının yalnız o kursun tek bir
kurgusuna özgü olmadığını, üretim ayrı bir derleme adımına taşındığında da geçerli kaldığını
göstermek için bilerek aynı sınamayı tekrarlıyor. İki ders farklı üretim mekanizmaları
kullanıyor — biri metinsel yer değiştirme, öbürü ayrı bir programın çalıştırılması — ama
ikisinin de ürettiği kaynak, derleyicinin gözünde aynı statüde: sıradan Rust kaynağı.
Bu benzerliğin bir sınırı var. Go’nun Tasarım Amaçları dersinde (M09/K01) kurulan derlenirMi
işlevi, tek bir kaynağı tek bir derleyici çağrısıyla sınıyordu; burada iki kaynak ve iki
sıralı çağrı gerekiyor, çünkü ikinci çağrı birincinin çıktısına bağımlı. tuketen_derlendi
değişkeninin kasa_derlendi && ile başlaması bu bağımlılığı kodun içinde görünür kılıyor:
kasa derlenmediyse tüketen kaynağı derlemeye çalışmanın hiçbir anlamı yok, çünkü bağlanacak
bir ikili yok. Aynı mantık eslesti değişkeninde bir kez daha tekrarlanıyor — tüketen kaynak
derlenmediyse çalıştırılacak bir ikili de yok. Bu zincirleme kısa devre, kodun kendisinde
üretimin sırasını da belgeliyor: bir adım başarısız olduğunda sonraki adımlar hiç
denenmiyor ve son satırdaki false değeri, hangi adımda durulduğunu ayırt etmeden yalnız
“zincir tamamlanmadı” diyor.
Türetilen Trait’ler dersi (M10/K03), bir öznitelik satırının kaynakta hiç gövdesi olmayan bir gerçekleştirim doğurduğunu ve bu gövdenin kaç kez koştuğunun sayılamadığını göstermişti. Bu ders o sayı iddiasını tekrarlamıyor: burada ölçülen kaç kez koştuğu değil, üretimin hangi derleme adımında gerçekleştiği ve o adımın gerçekten geçip geçmediği. İki ders farklı sorular soruyor ama aynı gözlemi paylaşıyor: kaynakta elle yazılmamış bir gövde de, derleyici açısından sıradan bir gövdeden ayırt edilmiyor.
Sınırlayıcı Ölçüm
Prosedürel makro kendi kasasında yaşamak zorundadır ve bu bir dil kısıtıdır — bir tercih değil.
Kasa kaynağını (KASA_KAYNAK) ve tüketen kaynağı (TUKETEN_KAYNAK) tek bir dosyada, tek bir
derleme çağrısıyla derlemeye çalışan bir yazım derleyici tarafından reddedilir: proc-macro
kasa türü yalnız makro tanımları dışa açabilir ve bir kasa kendi tanımladığı prosedürel
makroyu kendi içinde çağıramaz. Bu ret düzyazıda anlatılıyor, kod bloğuna konmuyor, çünkü
kursun genel kuralı gereği derlenmeyen bir kaynak hiçbir zaman kod bloğuna girmiyor.
Bu kısıt, ölçümün kurgusunu doğrudan belirledi. Önceki derste tek bir rustc çağrısı
yetiyordu, çünkü makro tanımı ve çağrısı aynı okumanın parçasıydı. Burada iki ayrı rustc
çağrısı gerekiyor, çünkü kasa önce var olmak zorunda — ikinci çağrı, birincinin ürettiği
ikiliyi bir girdi olarak alıyor. Prosedürel makronun “sürdürüyor” sınıfına düşmesi bu yüzden
aynı zamanda dilin bir mimari kararını da açığa çıkarıyor: derleyicinin genişletme sırasında
çalıştırdığı kod, derlenmekte olan kaynağın kendisinden önce ve ayrı var olmalı.
Üretilen gövdenin denetimi değişmiyor, ama o denetimin ne zaman ve kaç aşamada gerçekleştiği
değişiyor.
Kısıtın ikinci yüzü, kasanın neyi dışa açabileceğinde. proc-macro kasa türüyle derlenen
bir kasa, yalnız prosedürel makro tanımlarını dışa açabilir; sıradan bir pub fn ya da bir
pub struct aynı kasadan dışa açılamaz. Bu, önceki paragraftaki reddin bir uzantısı: kasa
türü kasanın rolünü belirliyor ve bu rol yalnız “makro üretimi” ile sınırlı. Bir proje
hem sıradan bir işlev hem bir prosedürel makro sunmak istiyorsa, ikisini iki ayrı kasaya
bölmek zorunda — biri proc-macro türünde, öbürü sıradan bir kitaplık kasası. Bu ayrım
ölçümün kurgusunda da izleniyor: kasa.rs, üretim makrosu dışında hiçbir şey dışa açmıyor.
Bu kısıtın bir bedeli daha var: iki kasalı bir yapı, tek kasalı bir yapıdan her zaman daha fazla derleme adımı gerektiriyor. Önceki derste bir değişiklik yapıldığında derleyici tek bir dosyayı okuyup tek bir ikili üretiyordu; burada kasa kaynağında yapılan en küçük bir değişiklik bile, önce kasanın yeniden derlenmesini, sonra tüketen kaynağın o yeni ikiliyle yeniden derlenmesini gerektiriyor. Bu, üretimin programlanabilir olmasının getirdiği gücün karşılığı: metinsel bir yer değiştirmenin ötesine geçen her üretim biçimi, kendi ayrı derleme zaman çizelgesini de beraberinde getiriyor.
Özet
- Prosedürel makro, girdi andaç akışını işleyip çıktı andaç akışı döndüren sıradan bir Rust
işlevidir; bu işlev kendi
proc-macrokasasında derlenmek zorundadır ve bu kasadan yalnız makro tanımları dışa açılabilir. - Ölçüm program içinden kuruldu: derleyici iki kez, sırayla çağrıldı, yalnız derlendi/ derlenmedi ve çıktı eşleşmesi basıldı — hata metni, yol, süre ve makine adı hiç okunmadı.
- Üretilen gövde bu kurguda da sürdürüyor sınıfına düşüyor: hem doğrudan çağrıldığında hem de bir genel sınırın içinde kullanıldığında sıradan kaynak gibi denetleniyor; fark üretimin aynı okumada değil, ayrı bir derleme adımında gerçekleşmesinde.
- Prosedürel makronun kendi kasasında yaşamak zorunda olması bir tercih değil, dilin kısıtı: bu yüzden ölçüm tek bir derleme yerine iki ayrı, sıralı derleme çağrısı gerektirdi.
- Bu kısıt bir bedel taşıyor: kasa kaynağındaki en küçük değişiklik bile, hem kasanın hem onu kullanan kaynağın yeniden derlenmesini gerektiriyor.
Sonraki Adım
Bu iki ders, üretilen kaynağın nasıl denetlendiğini iki farklı kurguda gösterdi: bir kez aynı okumada metinsel yer değiştirmeyle, bir kez ayrı bir derleme adımında çalıştırılan bir programla. İkisinde de ortak bir soru henüz sorulmadı — üretim neden tercih ediliyor, elle yazmak yerine. Sıradaki ve bu konunun son dersi bu soruyu ele alıyor: aynı yapı bir makro yazımıyla ve elle kurulmuş bir yazımla ayrı ayrı oluşturuluyor, aynı sonucu verdiği gösteriliyor ve makro yazımının aynı işi kaç satır kısalttığı sayılıyor. Ardından bu kısalmanın bir doğruluk garantisi getirmediği, yalnız bir okunabilirlik kazancı olduğu bir örnekle gösteriliyor.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.