← Back to list

Rust Dilinde Makro Kavramı ve Meta Programlama (Metaprogramming)

Bu yazımızda Rust dilinde makro yazımı ve meta programlama (metaprogramming) konusunu derinlemsine inceliyoruz.

Murat Çabuk in Devops & AI Türkiye☁️ 🐧 🐳 ☸️ · 2024-07-24 19:41 · 0 claps · 31.2 min read
#rust #rust-programming-language #rustlang #macro #makro
Open on Medium ↗
Wiki topics: 💻 · Programming 🎮 · Gaming

Rust Dilinde Makro Kavramı ve Meta Programlama (Metaprogramming)

Resim Kaynak

Resim Kaynak

Bir süredir vakit buldukça Rust ve Go ile ilgilenmeye çalışıyorum. Modern diller arasında ikisi de çok popüler. İkisi de farklı alanlarda çok iyi iş çıkartıyor. Web programlama ve lokal uygulama geliştirme gibi daha üst seviye işler yapıyorsanız Go kesinlikle çok iyi bir dil. Biraz daha donanıma yakın alt seviye işler yapıyorsanız da Rust iyi bir tercih. Aktif kullanma şansım olamasa da yüzeysel olarak bütün diller hakkında birşey öğrenmeye çalışıyorum. Bu makalede de öğrenme sürecimde beni zorlayan konulardan biri olan Rust dilinde meta programlama konusuna değinmek istedim. Ancak çok uzun bir yazı oldu. Umarın sabırla okuyabilirsiniz. Şimdiden iyi okumalar diliyorum.

Rust dilinde makro en basit tanımıyla kod yazan kod olarak tanımlanır. Genel anlamda buna meta programlama (metaprogramming) diyoruz. Rust makro kavramı ve yapısı büyük ölcüde Lisp dilinden esinlenerek oluşturulmuştur. Bunun yanı sıra C, C++ ve Scheme dillerinden de esinlenilmiştir. Ancak Rust dilindeki makro kullanımı bu dillere göre daha kullanışlı ve daha güvenlidir. Ben de dahil bir cok yazılımcı Türkiye’de genellikle Java, C#, JavaScript ve Python kökenliyiz. Acıkcası bu dillerin hiç birinde doğrudan Rust dilindeki makro kavramını tamamen standart olarak kaşılayan bir teknoloji yok. Farklı teknikler veya kütüphanelerle tabii ki benzer şeyler yapılabiliyor ancak incelediğinizde hakikaten cok güclü bir yapı olduğunu farkedeceksiniz. Evet bu kadar pazarlamacılık yeter birazda ürünü inceleyelim.

Metaprogramming konusuna girmeden önce sade bir girişle başlayalım. Rust ile ufak tefek kodlama yaptıysanız illaki makroları kullanmışsınızdır. En azından println! yazmışsınızdır :). Println! ilk bakışta sadece bir fonksiyon gibi görünüyor. Acıkcası benim ilk aklıma gelen soru “eğer bir fonksiyon gibi yazıyorsam naden adı makro ve neden ünlem işareti var?” oldu.

Derinlere dalmadan önce makro kullanmanın avantajları nelerdir? Fonksiyon yerine makroları neden tercih etmeliyiz ya da etmemeliyiz? gibi sorulara cevap arayarak mevzuya yavaş yavaş girelim.

Makro Kullanmanın Avantajları ve Fonksiyon Arasındaki Farklar Nelerdir?

  • Fonksiyonlar, belirli bir görevi yerine getiren ve çağrıldıklarında çalıştırılan kod bloklarıdır. Fonksiyonlar sabit sayıda ve türde parametre alırlar.
fn add(a: i32, b: i32) -> i32 {
    a + b
}

fn main() {
    let sum = add(5, 3);
    println!("Sum: {}", sum);
}
  • Makrolar, derleme zamanında genişleyen ve kod üreten yapılar olup, değişken sayıda ve türde parametre alabilirler. Makrolar, Rust kodu yazarak diğer Rust kodlarını oluşturur.
macro_rules! multiply {
    ($num1:expr, $num2:expr) => {
        println!("{}", ($num1 as f64 * $num2 as f64));
    };

    ($num1:expr, $num2:expr, $num3:expr) => {
        println!("{}", ($num1 as f64 * $num2 as f64 * $num3 as f64));
    };
}

fn multiply(num1:f64, num2:f64) {
  println!("{}", (num1 * num2));
}

fn main() {
  multiply!(1.5, 2);
  multiply!(1.5, 1.5);
  multiply!((1.5 * 1.5), 2);
  multiply!((1.5 * 1.5), 2, -1);

  multiply(1.5, 2.0);
}

Şuan kodu anlayamayabilirsiniz buna takılmayın ileride detaylarına değineceğiz.

Bu makro, create_function! makrosu ile iki fonksiyon oluşturur: foo ve bar. Makro, verilen isimleri kullanarak bu fonksiyonların gövdesini oluşturur. Yani sistem bizim için iki adet fonksiyon oluşturuyor. Bunu ne kadar zenginleştirebilceğimizi, Bu iki fonksiyonun hangi durumda nasıl davranacağını, parametre sayılarını, vb daha bir cok genişlemeyi yapabildiğimizi, kod tekrarını ne kadar azaltyabilceğimizi hayal edin. Henüz daha hic birşey görmedik ancak bu kadarlık bilgi bile bize çok şeyi yapma özgürlüğü sağlıyor.

Fakat açıklamada makroların derleme zamanında kodu genişlettiğinden yani bizim için kod ürettiğinden bahsetmiştik. Bundan nasıl emin olacağız? Yani makroyu tanımladık ve aynı isimle çalıştırdık sanki bir kod üretilmemiş gibi görünüyor.

Hakikaten derleme aşamasında kod üretilip üretilmediğini görmek için cargo-expand adında bir crate kullanacağız. Bunun için öncelikle cargo install cargo-expand komutu ile crate'yi yüklüyoruz ve projemizin bulunduğu dizin de terminalde veya powershell'de cargo expand komutunu çalıştırıyoruz. Alttakine benzer bir çıktı alacağız.

fn multiply(num1: f64, num2: f64) {
    {
        ::std::io::_print(format_args!("{0}\n", (num1 * num2)));
    };
}
fn main() {
    {
        ::std::io::_print(format_args!("{0}\n", (1.5 as f64 * 2 as f64)));
    };
    {
        ::std::io::_print(format_args!("{0}\n", (1.5 as f64 * 1.5 as f64)));
    };
    {
        ::std::io::_print(format_args!("{0}\n", ((1.5 * 1.5) as f64 * 2 as f64)));
    };
    {
        ::std::io::_print(
            format_args!("{0}\n", ((1.5 * 1.5) as f64 * 2 as f64 * -1 as f64)),
        );
    };
    multiply(1.5, 2.0);
}

Dikkat ederseniz tanımladığımız fonksiyon olduğu gibi dururken yazıdğımız makroyu kullandığımız main fonksiyonu içinde kullandığımız kadar fonksiyon oluşturultuğunu görebiliyoruz.

Peki şimdi alttaki örneği inceleyerek makro ile fonksiyon arasındaki teknik farklılıkları inceleyelim.


macro_rules! multiply {
    ($num1:expr, $num2:expr) => {
        println!("{}", ($num1 as f64 * $num2 as f64));
    };

    ($num1:expr, $num2:expr, $num3:expr) => {
        println!("{}", ($num1 as f64 * $num2 as f64 * $num3 as f64));
    };
}

fn multiply(num1:f64, num2:f64) {
  println!("{}", (num1 * num2));
}

fn get_num()->i32{
    return -1
}

fn main() {
  multiply!(1.5, 2);
  multiply!(1.5, 1.5);
  multiply!((1.5 * 1.5), 2);
  multiply!((1.5 * 1.5), 2, get_num());

  multiply(1.5, 2.0);
}

Bir fonksiyon imzası, fonksiyonun sahip olduğu parametrelerin sayısını ve türünü belirtir. Yani bu yapı onun imzası gibidir. Multiply fonksiyonu bir kez tanımlanır ve kullanılır. Daha sonra runtime’da değiştirmek, genişletmek veya overload etme gibi işlemler yapılamaz.

Diğer taraftan multiply! makrosunu inceleyecek olursak, aldığı parametreler birer expression yani isteğimiz bir Rust kodunu sayısal bir değer döndürmsi koşuluyla parametre olarak gecebiliyoruz makroya.

Ayrıca makro 2 veya 3 parametre ile çalışabiliyor. Bu da bir nevi overload kavramını andırıyor.

Yukarıda bahsettiğimiz gibi makrolar derleme aşamasında kodu genişlertir. Makro pattern macthing kullanarak kaç farklı genişleme yapacağını kontol ediyor. Burada görüleceği üzere 2 ve 3 parametreli bir desenimiz var. Dolayısıyla kodumuz aslında 2 farklı şekilde genişlemiş oluyor.

Tabii ki bu iki genişlemeyi fonkisyon yazarak da yapabilirdik. Bu durumda kodumuz daha hızlı derlenirdi. Ancak tabi gerçek hayatta kod yazdığımızda makrosuz bir dünyada binlerce fonksiyon yazmak ve bakımını yapmak durumunda kalacaktık.

  • Ayrıca, makrolar derleyicinin kodun anlamını yorumlamasından önce genişletilir, bu nedenle bir makro, örneğin, belirli bir tür üzerinde bir trait uygulayabilir. Bir fonksiyon bunu yapamaz, çünkü çalıştırma zamanında çağrılır ve bir trait’in derleme zamanında uygulanması gerekir.

Alttaki örnek üzerinden ne anlatılmak istediğini anlamaya çalışalım.

Detayına ileride değinecceğiz ama burada sadece acıklama olarak verelim, #[derive(Debug)] aslında bir makro çağrısıdır. Rust’ta derive makrosu, belirli trait’leri otomatik olarak uygulamak için kullanılır.

use std::fmt;

// Trait'i uygulamak için makro tanımı
macro_rules! impl_display {
    ($t:ty) => {
        impl fmt::Display for $t {
            fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
                write!(f, "{:?}", self)
            }
        }
    };
}

// Debug trait'ini eklemek için derive kullanımı
#[derive(Debug)]
struct Point {
    x: i32,
    y: i32,
}

// Point struct'ı için Display trait'ini uygulamak
impl_display!(Point);

fn main() {
    let point = Point { x: 10, y: 20 };
    println!("{}", point); // Output: Point { x: 10, y: 20 }
}

Amacımız Display trait’ini Point struct’ına uygulamak. Bunun için de impl_display makrosunu yazdık. Display trait’i println! makrosunun Point Struct’ını yazdırabilmesini sağlıyor. Yukarıda yaptığımız gibi cargo expand komutuyla inceleyelim.

use std::fmt;
struct Point {
    x: i32,
    y: i32,
}
#[automatically_derived]
impl ::core::fmt::Debug for Point {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        ::core::fmt::Formatter::debug_struct_field2_finish(
            f,
            "Point",
            "x",
            &self.x,
            "y",
            &&self.y,
        )
    }
}
impl fmt::Display for Point {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        f.write_fmt(format_args!("{0:?}", self))
    }
}
fn main() {
    let point = Point { x: 10, y: 20 };
    {
        ::std::io::_print(format_args!("{0}\n", point));
    };
}

Dikkat ederseniz Display trait’i Point struct’ına implement edilmiş görünüyor. Kodun geri kalanına cok odaklanmamıza gerek yok. Diğer makroların da nasıl kod ürettiğini görebiliyoruz ancak şuan konumuz oralar değil.

Trait daha kod yorumlanmaya başlamadan once yazılmış oluyor. Eğer bu olmasaydı zaten println! fonksiyonu hic çalışmayacak hatta kodumuz derlenmeyecekti bile. Mesela ben RustRover kullanıyorum ve makro çağrısını yaptığım satırı açıklama satırına cevirdiğimde derlemeye bile izin vermeden syntax hatası alıyorum.

  • Makrolar ve fonksiyonlar arasındaki bir diğer önemli fark, makroları tanımlamak veya dosyada çağırmadan önce scope’a getirmek zorundayız; fonksiyonları ise herhangi bir yerde tanımlayıp herhangi bir yerde çağırabiliriz.

Alttaki kod bloğunda fonksiyon tanımı kullanıldığı main fonksiyonundan sonra tanımlandığı halde hata almadan çalışacaktır.

fn main() {
    // Fonksiyon burada çağrılıyor
    greet();
}

// Fonksiyon herhangi bir yerde tanımlanabilir
fn greet() {
    println!("Hello, world!");
}

Alltaki makro tanımı ise hata verecektir cünkü main fonksiyonundan sonra tanımlanmış.

fn main() {
    // Bu çağrı hata verecektir çünkü makro henüz tanımlanmamış
    greet!();
}

// Makro burada tanımlandığında hata alırsınız
macro_rules! greet {
    () => {
        println!("Hello, world!");
    };
}

Eğer makro bulunduğumuz scope’un dışından tanımlandıysa çalışmayacaktır. Alttaki örnekte my_macros.rs dosyasındaki makro #[macro_use] ile import ediliyor. Aynı zamanda makro üzerinde de #[macro_export] satırıyla da makro export ediliyor. İki tanımlamadan birini silebiliriz. Yani ya macro_ue yada macro_export kalmak zorunda.

// main.rs

#[macro_use]
mod my_macros;

fn main() {
    greet!();
}

// my_macros.rs

#[macro_export]
macro_rules! greet {
    () => {
        println!("Hello from macro!");
    };
}

Burada bilmemiz gereken konu makroyu kullanmadan önce scope’a dahil etmemiz gerekiyor. Yani mod my_macros; satırını main fonksiyonu içinde makroyu kullanmadan önce import etmiş olamamız gerekiyor.

Tabii ki bu kadar güzel olsalarda makroların da dezavantajlı olduğu durumlar var. Zaten makroların temel amacı fonksiyonların yerini almak değil. Yukarıda bahsettiğimiz konular sadece mokroların nasıl çalıştığını anlamak. Ama yine de negatif taraflarına da bakalım.

  • Hata Ayıklama ve Anlaşılabilirlik: Makrolar genellikle hata ayıklaması zor olan kodlar üretir. Hatalar makronun genişletildiği kodda değil, makronun tanımında bulunabilir ve bu hataları izlemek zor olabilir. Fonksiyonlar daha belirgin ve anlaşılır hatalar üretir, bu da hata ayıklamayı kolaylaştırır.
  • Tür Kontrolü: Fonksiyonlar belirli türlerle çalışır ve tür denetimi derleme zamanında yapılır. Bu, tür hatalarını erken yakalamaya yardımcı olur. Makrolar ise tür denetimi yapmazlar ve genişletildiklerinde tür hataları oluşabilir. Bu, derleme hatalarını anlamayı zorlaştırabilir.
  • Performans ve Derleme Süresi: Makrolar her çağrıldıklarında genişletilerek tekrar derlenir, bu da derleme süresini uzatabilir. Fonksiyonlar ise bir kere derlenir ve derleme süresini optimize eder.
  • Kod Yeniden Kullanımı ve Modülerlik: Fonksiyonlar modüler ve yeniden kullanılabilir bir yapı sağlar. Bu, kodun okunabilirliğini ve bakımını kolaylaştırır. Makrolar ise kodu genişletme yoluyla tekrar kullanır, bu da kodun karmaşıklığını artırabilir.
  • Hata Mesajları: Makroların oluşturduğu hata mesajları genellikle daha karmaşık ve anlaşılması zor olabilir. Fonksiyonlar daha doğrudan ve anlaşılır hata mesajları üretir, bu da geliştiricilerin hataları daha hızlı çözmelerine yardımcı olur.
  • Kapsam Yönetimi: Makrolar kapsam (scope) ile ilgili sorunlar yaratabilir. Özellikle makrolar iç içe kullanıldığında kapsam yönetimi zorlaşabilir. Fonksiyonlar ise belirli bir kapsama sahiptir ve kapsam yönetimi daha basittir.

Şimdi biraz daha derinlere dalabiliriz.

Abstract Syntax Tree (AST)

Konuya başlamadan önce Abstract Syntax Tree kavramını anlamamız gerekiyor. Abstract Syntax Tree (AST), bir programın kaynak kodunu, programın sözdizimsel yapısını temsil eden bir veri yapısına dönüşmüş halini temsil eden birkavramdır. AST, programın yapısını ve hiyerarşisini anlamayı ve işlemeyi kolaylaştırır.

AST’de her düğüm (node), programın belirli bir kısmını temsil eder ve bu düğümler birbirleriyle ilişkili olarak bir ağaç yapısı oluşturur. Rust dilinde AST’yi anlamak ve kullanmak, özellikle derleyici yazımı, kod analizi ve araç geliştirme gibi alanlarda oldukça önemlidir.

Örnek üzerinden gidelim.

fn add(a: i32, b: i32) -> i32 {
    a + b
}

Üstteki kodun AST’si şuna benzer birşey olacaktır.

Function
  ├─ Ident: "add"
  ├─ Inputs
  │   ├─ Argument
  │   │   ├─ Ident: "a"
  │   │   └─ Type: i32
  │   └─ Argument
  │       ├─ Ident: "b"
  │       └─ Type: i32
  ├─ Output: i32
  └─ Body
      └─ Expression
          └─ BinaryOperation: "+"
              ├─ Identifier: "a"
              └─ Identifier: "b"

Bu yapı coğu modern programlama dilinde bulunmaktadır. Örneğin altta C# Roselyn derleyisinden bir örnek görebilirsiniz.

resim kaynak

AST’nin bize sağladığı bazı faydalar.

Sözdizimsel Analiz:

  • Kodun Anlaşılması: AST, kaynak kodun sözdizimsel yapısını temsil eder ve kodun yapılandırılmış bir şekilde analiz edilmesine olanak tanır.
  • Hata Tespiti: Derleyiciler ve analiz araçları, AST’yi kullanarak sözdizim hatalarını tespit edebilir ve raporlayabilir.

Kodun Dönüştürülmesi ve Optimizasyonu:

  • Kod Dönüşümleri: AST, kodun farklı biçimlere dönüştürülmesini sağlar. Örneğin, bir derleyici AST’yi kullanarak kaynak kodu makine koduna veya ara koda dönüştürebilir.
  • Optimizasyonlar: Derleyiciler, AST üzerinde çeşitli optimizasyonlar yaparak daha verimli kod üretebilir.

Kod Analizi ve Araçlar:

  • Statik Kod Analizi: AST, statik analiz araçlarının kodu detaylı bir şekilde incelemesini sağlar. Bu araçlar, kodun kalitesini artırmak için hataları ve potansiyel sorunları tespit eder.
  • Kod Formatlayıcılar ve Refaktör Araçları: AST, kodun belirli stil kurallarına göre formatlanmasını veya kodun yeniden düzenlenmesini sağlar.

Rust Dilinde AST’nin Sağladığı Faydalara da bir göz atalım

Güvenlik ve Hata Yönetimi:

  • Tip Güvenliği: Rust, güçlü tip sistemiyle bilinir. AST, tip kontrolünü sağlayarak derleme zamanında tip hatalarını tespit eder ve güvenli bir şekilde kodun çalışmasını sağlar.
  • Bellek Güvenliği: Rust’ın sahiplik modeli, bellek güvenliği sağlar. AST, bu modeli uygulayarak bellekle ilgili hataları ve bellek sızıntılarını önler.

Performans ve Optimizasyon:

  • Optimizasyonlar: Rust derleyicisi, AST üzerinde çeşitli optimizasyonlar yaparak yüksek performanslı kod üretir. Bu optimizasyonlar, bellek ve işlemci kullanımını minimize eder.
  • Zero-Cost Abstractions: Rust, soyutlamaların performans maliyeti olmadan kullanılmasını sağlar. AST, bu soyutlamaların verimli bir şekilde uygulanmasını ve optimize edilmesini sağlar.

Makrolar ve Metaprogramlama:

  • Makro Sistemi: Rust’ın güçlü makro sistemi, kodun otomatik olarak oluşturulmasını ve tekrarlayan kodların azaltılmasını sağlar. AST, makro işlemleri sırasında kodun analiz edilmesini ve dönüştürülmesini sağlar.
  • Derive Makroları: Rust, derive makroları ile belirli özelliklerin otomatik olarak uygulanmasını sağlar. AST, bu makroların işlenmesi ve uygun kodun üretilmesi için kullanılır.

Gelişmiş Derleme Süreci:

  • Hızlı Derleme: Rust derleyicisi, AST’yi kullanarak hızlı ve verimli bir derleme süreci sağlar. Bu, büyük projelerde bile derleme sürelerinin kısa olmasını sağlar.
  • Detaylı Hata Mesajları: AST, derleme sırasında detaylı ve anlaşılır hata mesajları üretilmesini sağlar. Bu, geliştiricilerin hataları hızlıca tespit edip düzeltmelerine yardımcı olur.

Artık makrolarla devam edebiliriz.

Makro Çeşitleri ve Metaprogramming

Rust dilinde makrolar iki şekilde oluşturulabiliyor.

  1. Declarative: macro-rules! makrosu kullanılarak yapılır. Makro tanımlamanın en kolay yoludur.
  2. Procedural: Rust sözdizimini hem tüketen hem de üreten Rust sözdizimi (syntax) üzerinden çalışan kodu derleme zamanında çalıştırmanıza olanak tanır. Prosedürel makroları bir AST’den (abstract syntax tree) başka bir AST’ye giden işlevler olarak düşünebiliriz. Üç farklı şekilde tanımlanabilir.
  • Custom derive macro
  • Attribute-like macro
  • Function-like macro

Şimdi tek tek inceleyelim.

Declarative Makro

Daha önce yüzeysel görmüştük declarative makroları ancak değinmediğimiz bir cok kısım var.

Match expression’lara benzer bir yapısı vardır. Eşleşen pattern’deki kod bloğu çalıştırlır. macro-rules! makrosu kullanılarak yazılır.


macro_rules! add{
    // 2 parametre alan bir pattern
    // a ve b parametre adı iken expr kelimesi ise bu parametrenin bir expression yani bir rust ifadesi olduğunu ifade eder
    ($a:expr,$b:expr)=>{
    // a ile b parametreleri toplanarak sonuçu geri döndürür.
    // bu sayede bu makro iki expression'un toplamıyle kodumuzu genişletmiş olur.
    // deerleme aşamasında kodumuza bu makronun kullanıldıuğı her yer için kod eklemsi yapılır.
            $a+$b
        }
    }
}

fn main(){
 // $a=1 and $b=2
    println!("{}", add!(1,2));

    println!("{}", add!((3*5), (2.0 + 4.0)));
}

Bu kodu cargo expand komutu ile inceleyecek olursak alttaki gibi bir sonuç göreceğiz.

fn main() {
    {
        ::std::io::_print(format_args!("{0}\n", 1 + 2));
    };
    {
        ::std::io::_print(format_args!("{0}\n", (3 * 5) + (2.0 + 4.0)));
    };
}

Görüleceği üzere makro kac kez çalıştırıldıysa o kadar kod üretilmiş.

Tabii ki tek yapabildiğimiz parametre tanımlayıp kullanmak değil. expr dışında başka token tiplerimiz de var.

  • item: function, struct, module vb
  • block: süslü parantez içinde bir kod bloğu
  • stmt: statement (let x = 5; bir statement’dır)
  • pat: pattern
  • expr: expression (let x = 5 + 6; // 5 + 6 bir expression’dır)
  • ty: type
  • ident: identifier (mesela bir fonkisyon ismi gibi)
  • path: path (e.g., foo, ::std::mem::replace, transmute::<_, int>, …)
  • meta: meta öğesi, #[…] ve #![…] attribute’larının içine gelcek ifadeler
  • tt: tek bir token tree
  • vis: pub (public olup olamama durumunu ifade eder)

Şimdi bunlar ve daha fazlası için biraz örnek yapalım.

macro_rules! print{

    ($a:expr,$b:expr,$typ:ty)=>{
        println!("{}", ($a as $typ + $b as $typ));
    };

    ($a:expr)=>{
        println!("{}", ($a as &str));
    }

}

fn main(){
    print!(0,2,u16);
    print!("Merhaba");

}

İki farklı farklı pattern tanımlamış ve kullanmış olduk.

  • Declarative Makrolarda Yineleme
macro_rules! create_vector {

    // yinelenen expression: * sayesinde expression'ın tekrarlanması sağlanabiliyor.
    ($($elem:expr),*) => {
        {
            let mut vec = Vec::new();
            $(vec.push($elem);)*
            vec
        }
    };
}

fn main() {

    // 2 saynın toplamını döndüren bir closure
    let num=|x| 3 + x;

    // tanımladığımız makroya istediğimiz kadar expression gonderebiliyoruz.
    let v = create_vector![1, 2, (1+2), (2*2), num(2)];
    println!("{:?}", v);
}

Acıklama satırlarından da anlaşılacağı üzere parametlerimizi tekrarlamak istersek * kullanabiliriz.

  • ident (Identifier): Bir tanımlayıcı, yani bir değişken, fonksiyon veya tür adı.
macro_rules! example {
    ($i:ident) => {
        let $i = 42;
    };
}

fn main() {

    example!(my_number);
    println!("{:?}", my_number);
}

Example adlı makromuz my_number adında bir değişken tanımlıyor. bunucargo expand komutuyla çağıracak olursak alttaki gibi bir kod üretildiğini göreceğiz.

fn main() {
    let my_number = 42;
    {
        ::std::io::_print(format_args!("{0:?}\n", my_number));
    };
}
  • ty (Type): Bir tür. Örneğin, i32, f64, String, vb.

Alttaki kod da bize ismini ve tipini verdiğimiz 2 tane değiken oluşturacak

macro_rules! create_variable {
    ($v:ident, $t:ty) => {
        let $v: $t = Default::default();
        println!("{}: {:?}", stringify!($v), $v);
    };
}

fn main() {
    create_variable!(x, i32);   // x: 0
    create_variable!(y, String); // y: ""
}macro_rules! create_variable {
    ($v:ident, $t:ty) => {
        let $v: $t = Default::default();
        println!("{}: {:?}", stringify!($v), $v);
    };
}
  • pat (Pattern): Bir desen. Örneğin, Some(x), None, Ok(v), vb.
macro_rules! match_pattern {

     // pattern parametre olarak alınıyor
    ($p:pat) => {
        match Some(42) {
            // pattern eşleşirse pattern eşleşti yazacak
            $p => println!("Pattern eşleşti!"),
            _ => println!("Pattern eşleşmedi."),
        }
    };
}

fn main() {
    match_pattern!(Some(x) if x == 42); // eşleşen pattern
}
  • path (Path): Bir yol. Örneğin, std::cmp::Ordering, self::module::Type, vb.
macro_rules! create_type {

    // path parametre olarak geciliyor ve _x adınca bu tipte bir değişken oluşturluyor
    ($p:path) => {
        let _x: $p = Default::default();
        println!("oluşturulan tip: {:?}", std::any::type_name::<$p>());
    };
}

fn main() {
    create_type!(std::collections::HashMap<String, i32>); // oluşturulan tip:
    // std::collections::HashMap<alloc::string::String, i32>
}
  • meta (Meta Item): Bir attribute meta öğesi. Örneğin, cfg(test), derive(Debug), vb.
macro_rules! apply_attribute {

    // Test dından oluştulan bir struct a parametre olarak gecilen attribute uygulanıyor
    ($m:meta) => {
        #[$m]
        struct Test;
    };
}

fn main() {
    apply_attribute!(derive(Debug));
    let t = Test;
    println!("{:?}", t); // Test
}
  • tt (Token Tree): Bir token ağacı. Herhangi bir token dizisi.

tt token tipi, bir token ağacı belirtir. Bu, herhangi bir token dizisi olabilir ve genellikle makroların iç içe geçmesi gerektiğinde kullanılır.

macro_rules! print_token_tree {
    // burada * karakterini karmaşık tree leri de parametre olarak algılayabilsin diye ekledik.
    ($($t:tt)*) => {
        println!("Token tree: {:?}", stringify!($($t)*));
    };
}

fn main() {
    print_token_tree!(foo(1, 2)); // Token tree: "foo ( 1 , 2 )"
    print_token_tree!({ let x = 42; x + 1 }); // Token tree: "{ let x = 42 ; x + 1 }"
}

Şimdi buraya kadar öğrendiklerimizle bir örnek yapalım. Amacımız makro kullanarak SQL benzeri bir DSL dili oluşturmak.


macro_rules! sql_query {
    // SELECT * FROM table
    (SELECT * FROM $table:ident) => {
        format!("SELECT * FROM {}", stringify!($table))
    };

    // SELECT columns FROM table WHERE condition
    (SELECT $($columns:ident),* FROM $table:ident WHERE $condition:expr) => {
        format!(
            "SELECT {} FROM {} WHERE {}",
            stringify!($($columns),*),
            stringify!($table),
            stringify!($condition)
        )
    };

    // SELECT columns FROM table
    (SELECT $($columns:ident),* FROM $table:ident) => {
        format!(
            "SELECT {} FROM {}",
            stringify!($($columns),*),
            stringify!($table)
        )
    };

    // INSERT INTO table (columns) VALUES (values)
    (INSERT INTO $table:ident ($($columns:ident),*) VALUES ($($values:expr),*)) => {
        format!(
            "INSERT INTO {} ({}) VALUES ({})",
            stringify!($table),
            stringify!($($columns),*),
            stringify!($($values),*)
        )
    };

}

fn main() {

    /*bu satırlarda yukarı belirlediğimiz pattern lere uymayan bir sql cümleceği yazdığımızda hata alırız. 
    mesela lattkai satırda SELECT cümlesinden sonra Test yazdığımız için hata alırız. 
    Güzel tarafı ise dikket ederseniz burada yazdığımız SQL cümlesi bir string ifade değil.
    Baya bildiğimiz kendi keyword lerimiz de bir SQL cümles iyazıyoruz ve bu derleme aşamasınd kontrol ediliyor.

    let select_query = sql_query!(SELECT test name, age FROM users WHERE age > 30);*/

    let select_query = sql_query!(SELECT name, age FROM users WHERE age > 30);
    let insert_query = sql_query!(INSERT INTO users (name, age) VALUES ("Alice", 30));

    println!("{}", select_query);  // SELECT name, age FROM users WHERE age > 30
    println!("{}", insert_query);  // INSERT INTO users (name, age) VALUES ("Alice", 30)

}
  • item (Item): Bir madde. Örneğin, fonksiyonlar, modüller, türler, sabitler, vb.

Altta mesela bir fonksiyon oluşturmuş oluyoruz.

macro_rules! create_function {
    ($i:item) => {
        $i
    };
}

create_function!(
    fn hello() {
        println!("Hello, world!");
    }
);

fn main() {
    hello(); // Hello, world!
}

cargo expand komutu ile kontrol edecek olursak yeni bir fonksiyon oluşturulduğunu görebiliriz.

fn hello() {
    {
        ::std::io::_print(format_args!("Hello, world!\n"));
    };
}
fn main() {
    hello();
}
  • block (Block): Bir blok. Örneğin, { … } blokları.

Altta birkode bloğu makroya parametre olarak geciliyor. Burada sadecce tek bir item değil bir çok şey yazmak mümkün. Mesel alttaki örnekte içice makro tanımladık.

macro_rules! exec_block {
    ($b:block) => {
        $b
    };
}
fn main() {
    exec_block!({
        macro_rules! nested_macro {
            ($e:expr) => {
                    println!("From nested macro : {}",$e);
            };
        }
        let x = 42;
        println!("Block executed, x = {}", x); // Block executed, x = 42
        nested_macro!(3);
    });
}
  • stmt (Statement): Bir ifade. Örneğin, bir let bildirimi, bir ifade vb.
macro_rules! exec_statement {
    ($s:stmt) => {
        $s
    };
}

fn main() {
    exec_statement!(let x = 42);
    // Statement çalıştırıldı, x = 42
    exec_statement!(println!("Statement çalıştırıldı, x = {}", x)); 

    exec_statement!(let y = "Merhaba");
    // Statement çalıştırıldı, y = Merhaba
    exec_statement!(println!("Statement çalıştırıldı, y = {}", y)); 
}
  • literal (Literal): Bir sabit değer. Örneğin, sayılar, karakterler, dizeler vb. literal token tipi, bir sabit değeri belirtir. Bu, sayılar, karakterler, dizeler vb. olabilir.

dikkat ederseniz expression’dan farklı bir tip literal. Burada bir kod değil sadece sabit değeri veriyoruz.

macro_rules! print_literal {
    ($l:literal) => {
        println!("Literal: {}", $l);
    };
}

fn main() {
    print_literal!(42); // Literal: 42
    print_literal!("Hello, world!"); // Literal: Hello, world!
}
  • vis (Visibility): Bir görünürlük belirleyicisi. Örneğin, pub, pub(crate) vb.

vis token tipi, bir görünürlük belirleyicisini belirtir. Bu, pub, pub(crate) vb. olabilir.

macro_rules! define_struct {
    ($v:vis $name:ident) => {
        $v struct $name;
    };
}

define_struct!(pub MyStruct);

fn main() {
    let _ = MyStruct;
}

Oluşturulan struct’ın public olup olmayacağını belirtmiş olduk. Burada MyStruct adından public bir struct oluşturuluyor.

  • Örnek Uygulama

Şimdi detaylı bir örnek uygulama yapalım. Amacımız oluştuduğumuz struct tiplerimiz için getter ve setter metodlarını tanımlamak. Bunun için paste adında bir de crate kullancağız.

Paste tam olarak alttaki örnekteki gibi bir iş yapıyor. Yani ident’leri birleştirmemizi sağlıyor.

use paste::paste;

paste! {
    // verdiğimiz her bir harfi birleştirerek bizim için QRST adında bir contant oluşturacak
    const [<Q R S T>]: &str = "success!";

}

fn main() {

        paste! { [<Q R S T>].len() };
        println!("{}", QRST); // "success!"
}

Paste crate’sini de kullanarak alttaki örneği yazıyoruz. Toml dosyaınıza paste crate’sini bağımlılığını eklemeyi unutmayın.

// Cargo.toml
// [dependencies]
// paste = "1.0"

use paste::paste;

macro_rules! define_struct {
    (
        struct $name:ident {
            $(
                $field:ident : $ftype:ty
            ),* $(,)?
        }
    ) => {
        // Yapıyı tanımla
        struct $name {
            $(
                $field: $ftype,
            )*
        }

        // Getter ve Setter metotlarını ekle
        impl $name {
            $(
                // Getter
                pub fn $field(&self) -> &$ftype {
                    &self.$field
                }

                // Setter
                paste! {
                    pub fn [<set_ $field>](&mut self, value: $ftype) {
                        self.$field = value;
                    }
                }
            )*
        }
    };
}

// Makroyu kullanarak bir struct tanımlayalım
// define_scruect makromuz bizim için setter ve getter metodlarını yazacak
// hatta en güzeli butun filed lar için set_field şeklinde setter'larımız yazmış olacak.
// altta kullanımını da görebilirsiniz.
define_struct!(
    struct Person {
        name: String,
        age: u32,
        email: String,
    }
);

fn main() {

    println!("------------------- öğrenci ------------------");
    // Yapıyı örnekleyelim ve getter/setter metotlarını kullanalım
    let mut student = Person {
        name: String::from("Murat"),
        age: 18,
        email: String::from("murate@example.com"),
    };

    // Getter metotları
    println!("Name: {}", student.name());
    println!("Age: {}", student.age());
    println!("Email: {}", student.email());

    // Setter metotları
    // set_name ve set_age matodlarına dikkat ediniz.
    student.set_name(String::from("Murat Ali"));
    student.set_age(25);

    // Yeni değerleri yazdıralım
    println!("Updated Name: {}", student.name());
    println!("Updated Age: {}", student.age());
    println!("Updated Email: {}", student.email());

    println!("------------------- öğretmen ------------------");

    let mut teacher = Person {
        name: String::from("Mehmet"),
        age: 18,
        email: String::from("mehmet@example.com"),
    };

    // Getter metotları
    println!("Name: {}", teacher.name());
    println!("Age: {}", teacher.age());
    println!("Email: {}", teacher.email());

    // Setter metotları

    teacher.set_age(48);

    // Yeni değerleri yazdıralım
    println!("Updated Name: {}", teacher.name());
    println!("Updated Age: {}", teacher.age());
    println!("Updated Email: {}", teacher.email());

}

Evet zanndiyorum declarative makrolara için bu kadar yeterli olacaktır. Şimdi birazda procedural makrolara bakalım.

Procedural Makrolar

Konuya dalmadan önce iletlyen başlıklarda işimize yarayacak bazı kütüphanelerden bahsetmek istiyorum. Bu iki crate makro yazarken daha doğrusu metaprogramming yazarken en cok kullanılan kütüphanaler.

  • syn: Rust kaynak kodunu soyut sözdizimi ağacı (AST) olarak ayrıştırır. Bu, kodu parçalarına ayırarak analiz etmeyi ve dönüştürmeyi kolaylaştırır.
  • quote: Rust kodunu stringler yerine Rust diline özgü bir biçimde oluşturmayı sağlar. Bu, kod üretmeyi ve dönüştürmeyi daha güvenilir ve okunabilir hale getirir.

syn için bir proje oluştutup main.rs dosyasını alttaki gibi düzenliyoruz.

use syn::{parse_str, ItemFn};

fn main() {
    // Ayrıştırılacak fonksiyon imzası
    let code = "fn my_function(param1: i32, param2: String) -> bool {}";

    // `syn` crate'i ile fonksiyon imzasını ayrıştır
    let ast: ItemFn = parse_str(code).expect("Failed to parse function");

    // Fonksiyon ismini al
    let fn_name = &ast.sig.ident;
    println!("Function name: {}", fn_name);

    // Parametreleri al
    for input in &ast.sig.inputs {
        if let syn::FnArg::Typed(pat_type) = input {
            if let syn::Pat::Ident(pat_ident) = &*pat_type.pat {
                println!("Parameter: {}", pat_ident.ident);
            }
        }
    }
}

Cargo.toml dosyamızı alattaki gibi düzenliyoruz.

[package]
name = "my_app"
version = "0.1.0"
edition = "2021"

[dependencies]
syn = { version = "2.0", features = ["full"] }

Kodumuzu çalıştırıığımızda altaki gibi bir sonuç görmeliyiz. Kodumuza dikkat edersek bilgiğimiz syring bir ifadeden bir AST oluştutup daha sonra analiz ederek koddaki fonksiyon ve parametre adlarını ekrana yazıyoruz.

Function name: my_function
Parameter: param1
Parameter: param2

Aynı projeyi kullanarak birde quote’yi inceleyelim.

Cargo.toml dosyasını alttaki gibi değiştirelim.

[package]
name = "my_app"
version = "0.1.0"
edition = "2021"

[dependencies]
syn = { version = "2.0", features = ["full"] }
quote = { version = "1.0", features = ["full"] }

main.rs doaysını da alttaki gibi değiştiriyoruz.

use quote::quote;

fn main() {
    // Fonksiyon adı ve parametreler
    let fn_name = "my_function";
    let param1 = 42;
    let param2 = "Hello".to_string();

    // `quote` crate'i ile kod oluşturma
    let generated_code = quote! {
        #fn_name(#param1, #param2);
    };

    // Oluşturulan kodu yazdır
    println!("{}", generated_code);
}

Kodu çalıştırdığımızda alttaki gibi bir sonuç görmeliyiz.

my_function(42, "Hello");

Eğer amacımız meta programlama yapmaksa bu iki aracın ne kadar işimize yarayacağını zannediyorum farketmişsinizdir. Bir kod içine derleme aşamasında kod eklemek aslında bir kod boluğunu yakalamak ,parametlerini, parametre tiplerini, ilgili tipin adını vb meta bilgilerini anlamak ve üzerindne değişiklik yapamk demek. En basit haliyle parse etmek ve parse etttikten sonra aldığımız kelimelerin eneenalama geldiğini anlamamız gerekiyor. Diğer tarafrtn ekleme veya değiştirme yaptığımız kodları da ilgili AST’de (Abstract Syntax Tree) doğru yere eklememiz gerekiyor. İşte bu iki crate bütün bu sürecte bize yardımcı oluyor.

Alttaki procedural makro örneklerinde de bu iki kütüphaneyi bolca kullanacağız.

Derive Macro

Bir trait’i bir tipe uygulamak için kullanılır. #[derive(traid1, traid2)] şeklinde ilgili tipin hemen üstüne yazılarak uygulanır.

Rust dilinin standart kütüphanelerinde bulunan ve sıklıkla kullanılan bazı derive makroları şunlardır. Herhalde debuk ve default makrolarını kullanmayan yoktur.

  • Debug
  • Clone
  • Copy
  • PartialEq
  • Eq
  • PartialOrd
  • Ord
  • Hash
  • Default

Mesela alttaki örnekte Debug derive makrosunu uygulayarak User Struct’ımızın printrln! makru ile doğrudan kendisinin print edilebilmesini sağlamış olduk.

#[derive(Debug)]
pub struct User {
    username: String,
    first_name: String,
    last_name: String,
}

fn main() {

    let my_user = User {
            username: String::from("muratcabuk"),
            first_name : String::from("Murat"),
            last_name: String::from("Cabuk")
    };

    println!("{:?}", my_user);
}

Eğer bu olmasaydı bütün yazdığımız struct’lar için alttakine benzer bir kod yazmak durumunda olacaktık.

impl ::core::fmt::Debug for User {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        ::core::fmt::Formatter::debug_struct_field3_finish(
            f,
            "User",
            "username",
            &self.username,
            "first_name",
            &self.first_name,
            "last_name",
            &&self.last_name,
        )
    }
}

E zaten makroların işi de kod tekrarını azaltmak değil miydi? Yani mevzu anlaşıldığına göre kendi derive makromuzu yazalım.

Bu projede amacımız herhangi bir struct üzerine #[derive(HelloMacro)] satırın eklediğimizde HelloMacro adlı trait'in eklenmesini sağlamak.

Proje yapımız alttaki gibi olacak.

my_app
 |-- Cargo.toml
 |-- src
     |-- main.rs
 |--my_macro (my_app dizini altında)
    |-- Cargo.toml
    |-- src
        |-- main.rs

my_app adında bir proje ekleyip src klasrüne main.rs adında bir dosya oluşturup içine alttaki kodları kopyalıyoruz.

Burada hemen altta yazdığımız HelloMacro adlı derive makromuzu uygulamış oluyoruz. Dikkat ederseniz MyStruct adlı struct’a HelloMacro uygulanıyor ve hello fonkiyonu cağldığında da ekrana Hello, MyStruct! yazması sağlanıyor. `

use my_macro::HelloMacro;

pub trait HelloMacro {
    fn hello();
}

#[derive(HelloMacro)]
struct MyStruct;

fn main() {
    MyStruct::hello();
}

my_app projesinin Cargo.toml dosyasını alttaki gibi düzenliyoruz.

[package]
name = "my_app"
version = "0.1.0"
edition = "2021"

[dependencies]
my_macro = { path = "my_macro" }

my_macro adında bir projeyi de my_app klasörü altına oluşturup src klasörüne lib.rs dosyası ekliyoruz. Alttaki kodları içine kopyalıyoruz.

Derive makro tanımlarken yazdığımız fonkiyonun üstüne #[proc_macro_derive(YazılanMakroAdı)] şeklinde attribute ekliyoruz. Fonkisyonun aldığı TokenStream ise trait uygulayacağımız struct veya enum'ın token tree'sini ifade ediyor. Geriye döndürdüğümüz TokenStren ise trait uygulanmış yeni versiyonunu ifade ediyor.

extern crate proc_macro;

use proc_macro::TokenStream;
use quote::quote;
use syn;

#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
    // Gelen TokenStream'i bir Rust yapısına dönüştür
    let ast = syn::parse(input).unwrap();

    // Yeni kodu oluştur
    impl_hello_macro(&ast)
}

fn impl_hello_macro(ast: &syn::DeriveInput) -> TokenStream {
    let name = &ast.ident;
    let gen = quote! {
        impl HelloMacro for #name {
            fn hello() {
                println!("Hello, {}!", stringify!(#name));
            }
        }
    };
    proc_macro::TokenStream::from(gen)
}

my_macro projesinin Cargo.toml dosyasını alttaki gibi düzenliyoruz. bir kütüphane olarak yazdığımza ve toml doyasında proc-macro özelliğini true yaptığımıza dikkat edin.

[package]
name = "my_macro"
version = "0.1.0"
edition = "2021"

[lib]
proc-macro = true

[dependencies]
syn = "1.0"
quote = "1.0"

my_app projemizi cargo expand komutu ile inceleyecek olursak makromuzun gcekte oluşturduğu kodu da görebiliriz.

use my_macro::HelloMacro;
pub trait HelloMacro {
    fn hello();
}
struct MyStruct;
impl HelloMacro for MyStruct {
    fn hello() {
        {
            ::std::io::_print(format_args!("Hello, {0}!\n", "MyStruct"));
        };
    }
}
fn main() {
    MyStruct::hello();
}

Attribute Like Macro

Kullanımı derive makrolara cok benziyor. Derive makrolar #[derive(CustomDerive)] şeklinde kullanılırken attribute-like makrolar #[CustomAttribute] şeklinde kullanılıyor.

Rust dilinde attribute (öznitelik) kavrammı hakkında bilgi almak ve standart olarak kullanılan attribute listesine ulaşamak içi şu linke bakabilirsiniz.

  • Aralarındaki en büyük fark ise derive makrolar sadece stuct ve enum’lara uygulanabilirken attribute-like makrolar ise onksiyonlar, modüller, yapılar (structs), enums gibi çeşitli öğelere uygulanabilir.
  • Diğer öenmli bir fark ise derive makrolar sadece trait implementasyonu için kullanılırken attribute makroları bir çok işlem için kullanılabilir örneğin cross cutting concerns’lari yönetebilirsiniz, bir trait uygulayabilirsiniz, koda eklemeler vaya değiştrimeler yabilirsiniz,

E madem bu kadar iyi birşey ozaman derive makrolara ne gerek var diyebiliriz?

  • Öncelikle derive makroları uygularken tek seferde birden fazla trait uuygulayabiliriz.
  • Diğer taraftan sistemde sadece trait’lerin implementasyonu için bir makro türü olması sebebiyle diğer Rust programcıları için hemen anlaşılır ve okunabilirdir. Bu sözdizimi, trait implementasyonunun otomatik olarak sağlandığını açıkça belirtir.
  • Kodun okunabilirliğini artırır ve diğer geliştiricilerin kodun amacını ve işlevselliğini hızlı bir şekilde kavramasını sağlar.
  • derive makrolarının kullanımı, kodun gelecekteki bakımını kolaylaştırır. Trait implementasyonları açıkça belirtilmiş olur ve gelecekteki değişiklikler veya genişletmeler daha kolay yapılabilir.

Attribute makroları proc_macro_attribute attribute’una sahip bir (TeokenStrem, TokenStream)->TokenStrem şekilinde iki parametreli ve bir TokenStream geri döndüren fonksiyon imzasına sahip bir public fonksiyonla tanımlanır. İlk TokenStream attribute'un kendi adını ve kendini takip eden token ağaçlarını ifade eder. İkinci parametre olan TokenStream ise attribute'un eklenmiş olduğu kod parçasını ifade eder. Burada diğer attribute'lar ve kodun geri kalanı da bulunur.Geri döndürülen TokenStrem ise değiştirlmiş yeni token ağacıdır. Yani eski kodun yerini alcak yeni koddur.

#[proc_macro_attribute]
pub fn my_attribute_macro(_attr: TokenStream, item: TokenStream) -> TokenStream {
    item
}

Eğer my_attribute_macro attributu’nu alttaki gibi kullanırsak üstteki kodda yazdığımız ilk TokenStream bu parametreleri içerecektir.

#[my_attribute_macro(parametre1, parametre2="value2")]
fn my_function() {
    // some code
}

İkinci TokenStrem ise aslında makronun uygulantdği item’ı yani onun TokenStream’ini ifade eder. Biz maktomuzun içinde bu kodu ayrıştırır ve istediğimiz değişiklikleri yapabiliriz. Burada my_function fonksiyonunun token ağacı ikinci TokenStream’in içerğini oluşturur. Örneklerle daha iyi anlayacağız.

Son tahlilde birinci Token ile makronun uygulandığı yerden parametreleri alıyoruz, ikinci Token ile makronun uygulanacağı treeYi alıyoruz ve geriye değiştirilmiş Token’ı donüyoruz.

Şimdi bir iki örnek yapalım.

Birinci örneğimizde attribute makronun çalışmasını anyalabilmek için basit bir uygulama ile başlayacağız.

Amacımız makromuzun kullanıldığı yerde makroya geçilen prametreleri ve kullanılan fonksiyornun içeriğini yazmak.

Proje yapımız alttaki gibi olacak.

my_app
 |-- Cargo.toml
 |-- src
     |-- main.rs
 |--my_macro (my_app dizini altında)
    |-- Cargo.toml
    |-- src
        |-- main.rs

Alttaki kodları my_macro altindaki lib.rs dosyasına kopyalıyoruz.


extern crate proc_macro;
use proc_macro::TokenStream;
#[proc_macro_attribute]
pub fn show_streams(attr: TokenStream, item: TokenStream) -> TokenStream {

    // makromuza gelen parametreleri yazdırıyoruz.
    println!("attr: \"{attr}\"");

    // değiştirilecek kodu ekrna yazdıyoruz
    println!("item: \"{item}\"");

    // ilgili fonkiyonda hiç bir değişiklik yapmadan TokenStrem i geri döndürüyoruz.
    item
}

my_macro projesnin Cargo.toml dosyası alttaki gibi olacak.

[package]
name = "my_macro"
version = "0.1.0"
edition = "2021"

[lib]
proc-macro = true

[dependencies]
syn = { version = "2.0", features = ["full"] }
quote = "1.0"

Alttaki kodları da ana dizindeki main.rs dosyasına kopyalıyoruz.

use my_macro::show_streams;

#[show_streams]
fn invoke1() {
    println!("test");
    }
//  attr: ""
//  item: "fn invoke1() {}"

#[show_streams(bar)]
fn invoke2() {}
//  attr: "bar"
//  item: "fn invoke2() {}"

// a => b şeklinde ilişkilerde gönderilebilir
#[show_streams(a => b)]
fn invoke3() {}
//  attr: "multiple => tokens"
//  item: "fn invoke3() {}"

// kod bloğunu da parametre olarak gönderilebilir
#[show_streams { def a = 20; def b = "merhaba"; my_func();}]
fn invoke4() {}
//  "def a = 20; def b = "merhaba"; my_func();"
//  "fn invoke4() {}"

fn main() {
    invoke1();
    invoke2();
    invoke3();
    invoke4();

    println!("bitti")
}

my_app projesinin yani ana dizinin Cargo.toml dosyası alttaki gibi olacak.

[package]
name = "my_app"
version = "0.1.0"
edition = "2021"

[dependencies]
my_macro = { path = "my_macro" }
syn = { version = "2.0", features = ["full"] }
quote = "1.0"

syn ve quote kütüphanelerini ileri de kullanacağız.

Kodumuzu build aldığımızda alttaki gibi bir sonuç göreceğiz.

attr: ""
item: "fn invoke1() { println!("test"); }"
attr: "bar"
item: "fn invoke2() {}"
attr: "a => b"
item: "fn invoke3() {}"
attr: "def a = 20; def b = "merhaba"; my_func();"
item: "fn invoke4() {}"

Görüleceği üzere çok farklı parametreler geçirmek mümkün. Biz burada sadece hangi token ne iş yapıyordu bunu bi de canlı görerek anlamaya çalıştık.

peki kodumuzu çalıştırırsak ne göreceğiz ona bakalım. Tam olarka alttaki gibi bir sonuç göreceğiz.

test
bitti

Çünkü makromuz hatırsanız geriye aynı fonkiyonu TokenStream olarka dödürüyor yani kodda hiç bir değişiklik yapmıyoruz. Bundan dolayı sadece invoke1 fonkiyonun body kısmında bir işlem olduğu için oranın sonucu olan test kelimesi basılıyor. Birde main fonksiyonumuz ekrana birri yazıyorsu onu görmüş olduk.

Şimdi mesela şöyle bir şey yapalım. Kodumuz çalıştırıldığında mekronun uygulandığı bütün fonksiyonlar çalıştırıldıklarında ekran “makro çalıştı” diye bir yazı yazsın.

bunun için makromuzu alttaki gibi değiştiriyoruz.

extern crate proc_macro;
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, ItemFn};

#[proc_macro_attribute]
pub fn show_streams(attr: TokenStream, item: TokenStream) -> TokenStream {
    // Makromuza gelen parametreleri yazdırıyoruz.
    println!("attr: \"{attr}\"");

    // Değiştirilecek kodu ekrana yazıyoruz
    println!("item: \"{item}\"");

    // TokenStream'i ItemFn'e dönüştürüyoruz
    let input = parse_macro_input!(item as ItemFn);

    // Fonksiyonun ismini alıyoruz
    let func_name = &input.sig.ident;

    // Fonksiyonun içeriğini alıyoruz
    let block = &input.block;

    // Fonksiyonun yeni kodunu oluşturuyoruz
    let expanded = quote! {
        fn #func_name() {
            // Fonksiyon çalışmadan önce mesajı yazdırıyoruz
            println!("{} çalışmadan öncesinden merhaba", stringify!(#func_name));

            // Orijinal fonksiyon kodunu çağırıyoruz
            #block

            // Fonksiyon çalıştıktan sonra mesajı yazdırıyoruz
            println!("{} çalıştıktan sonrasından merhaba", stringify!(#func_name));
        }
    };

    // Yeni kodu TokenStream olarak döndürüyoruz
    TokenStream::from(expanded)
}

Kodumuzu çalıştırdığımızda alattaki gibi sonuç göreceğiz.

invoke1 çalışmadan öncesinden merhaba
test
invoke1 çalıştıktan sonrasından merhaba
invoke2 çalışmadan öncesinden merhaba
invoke2 çalıştıktan sonrasından merhaba
invoke3 çalışmadan öncesinden merhaba
invoke3 çalıştıktan sonrasından merhaba
invoke4 çalışmadan öncesinden merhaba
invoke4 çalıştıktan sonrasından merhaba
bitti

Göreceğiniz üzere fonksiyonlarımızda hiç bir değişiklik yapmadan bir nda dört fonkiyonumuzn başına ve sonuna metinler ekelyebildik.

Şimdi kodumuzu biraz daha geliştirelim ve eğer age adında bir parametre eklendiyse ve değer de 18'den küçükse değeri 18 ile değiştirip fokiyonu değiştirelim.

bunun için main.rs doayamızı alttaki gibi değiştiriyoruz.


use my_macro::show_streams;

#[show_streams]
fn invoke1() {
    println!("test");
}

#[show_streams(bar)]
fn invoke2(age: i32) {
    println!("{}", age);
}

#[show_streams(a => b)]
fn invoke3(age: i32) {
    println!("{}", age);
}

#[show_streams { def a = 20; def b = "merhaba"; my_func();}]
fn invoke4() {}

fn main() {
    invoke1();
    invoke2(17);
    invoke3(25);
    invoke4();

    println!("bitti")
}

Eğer bu şekliyle build almayı ve kodu çalıştırmayı denerseniz age parametrelerinin olmadığına dair hatalar alacaksınız. Bunun sebebi ise makromuzdaki alttaki satırlada gizli.fn #func_name() satırında dikket ederseniz biz aldığımız fonksiyonu geriye parametresiz olarak döndürüyoruz. Makrolar derleme aşamasında çalıştığı için istediğimiz kadar parametre ekeleyelim sonuçta çalışma zamanına hep parametresiz bir fonksiyon elde ediyoruz.

// kısaltıldı
 // Fonksiyonun içeriğini alıyoruz
    let block = &input.block;

    // Fonksiyonun yeni kodunu oluşturuyoruz
    let expanded = quote! {
        fn #func_name() {
            // Fonksiyon çalışmadan önce mesajı yazdırıyoruz
            println!("{} çalışmadan öncesinden merhaba", stringify!(#func_name));

            // Orijinal fonksiyon kodunu çağırıyoruz
            #block

            // Fonksiyon çalıştıktan sonra mesajı yazdırıyoruz
            println!("{} çalıştıktan sonrasından merhaba", stringify!(#func_name));
        }
    };

// kısaltıldı

Bu nedenle makromuzu alttaki gibi değiştiriyoruz. İlgili satıra dikket ederseniz fn #func_name(#params) { artık fonkiyon parametrelerini alıp yeni oluşturdupumuz fonksiyonun TokenStream'ine ekliyoruz.

extern crate proc_macro;
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, ItemFn};

#[proc_macro_attribute]
pub fn show_streams(attr: TokenStream, item: TokenStream) -> TokenStream {
    // Makromuza gelen parametreleri yazdırıyoruz.
    println!("attr: \"{attr}\"");

    // Değiştirilecek kodu ekrana yazıyoruz
    println!("item: \"{item}\"");

    // TokenStream'i ItemFn'e dönüştürüyoruz
    let input = parse_macro_input!(item as ItemFn);

    // Fonksiyonun ismini alıyoruz
    let func_name = &input.sig.ident;

    // Fonksiyonun içeriğini alıyoruz
    let block = &input.block;

    // Fonksiyonun parametrelerini alıyoruz
    let params = &input.sig.inputs;

    // Fonksiyonun yeni kodunu oluşturuyoruz
    let expanded = quote! {
        fn #func_name(#params) {
            // Fonksiyon çalışmadan önce mesajı yazdırıyoruz
            println!("{} çalışmadan öncesinden merhaba", stringify!(#func_name));

            // Orijinal fonksiyon kodunu çağırıyoruz
            #block

            // Fonksiyon çalıştıktan sonra mesajı yazdırıyoruz
            println!("{} çalıştıktan sonrasından merhaba", stringify!(#func_name));
        }
    };

    // Yeni kodu TokenStream olarak döndürüyoruz
    TokenStream::from(expanded)
}

Kodu çalıştırdığımızda altak igibi bir sonuç alacağız.

invoke1 çalışmadan öncesinden merhaba
test
invoke1 çalıştıktan sonrasından merhaba
invoke2 çalışmadan öncesinden merhaba
17
invoke2 çalıştıktan sonrasından merhaba
invoke3 çalışmadan öncesinden merhaba
25
invoke3 çalıştıktan sonrasından merhaba
invoke4 çalışmadan öncesinden merhaba
invoke4 çalıştıktan sonrasından merhaba
bitti

Ancak bu hali istediğimiz bir şey değil biz 18 den küçük değerler için 18 yazmasını istiyorduk. İşte burası baya karışık bir konu. Çünkü birden fazla parametre verilmesi durumunda diğer parametrelere hiç dokunmadan sadece age barapametsini değiştirmemiz gerekiyor.

Tam burada bir kez daha düşünelim yapmak istediğimiz şey mantıklı mı? Yazımızın en başında makroların derleme zamanında çalıştığını söylemiştik hatırlarsanız. Yani çalışma zamanında örneğin veritabanından veya kullanıcıdan aldığımız bir değeri age parametresini değeri olak verirsek bu durumda makro o değeri değerlendiremeyecektir. O zaman bu durmu da dikkate alarak kodumuzu değiştirmeliyiz.

Bunun için şöyle bir yol izleyebiliriz. Eğer parametreler arasında age adında bir prametre varsa ilgili fonksiyona bu oaratreyi kontrol edecek bir fonksiyon ekleyebiliriz. lib.rs dosyasını alttaki gibi değiştiriyoruz.

extern crate proc_macro;
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, ItemFn, FnArg, PatType, Pat};

#[proc_macro_attribute]
pub fn show_streams(attr: TokenStream, item: TokenStream) -> TokenStream {
    // Makromuza gelen parametreleri yazdırıyoruz.
    println!("attr: \"{attr}\"");

    // Değiştirilecek kodu ekrana yazıyoruz
    println!("item: \"{item}\"");

    // TokenStream'i ItemFn'e dönüştürüyoruz
    let input = parse_macro_input!(item as ItemFn);

    // Fonksiyonun ismini alıyoruz
    let func_name = &input.sig.ident;

    // Fonksiyonun içeriğini alıyoruz
    let block = &input.block;

    // Fonksiyonun parametrelerini alıyoruz
    let params = &input.sig.inputs;

    // age parametresini kontrol eden kodu ekleyin
    let mut age_check_call = quote! {};

    for param in params.iter() {
        if let FnArg::Typed(PatType { pat, .. }) = param {
            if let Pat::Ident(ident) = pat.as_ref() {
                if ident.ident == "age" {
                    age_check_call = quote! {
                        age = age_check(age);
                    };
                    break;
                }
            }
        }
    }

    // Parametreler ve işleyişi destekleyen kodu oluşturuyoruz
    let expanded = quote! {
        fn #func_name(#params) {
            // age parametresini kontrol edip gerekirse ayarlıyoruz
            #age_check_call

            // Orijinal fonksiyon kodunu çağırıyoruz
            #block

            // Fonksiyon çalıştıktan sonra mesajı yazdırıyoruz
            println!("{} çalıştıktan sonrasından merhaba", stringify!(#func_name));
        }
    };

    // Yeni kodu TokenStream olarak döndürüyoruz
    TokenStream::from(expanded)
}

Koda dikkat ederseniz for döngüsü ile bütün parametreler taranıyor ve eğer age adında bir parametre varsa age = age_check(age); satırı expanded değişkeni için de #block satırında önce #age_check_call satırı ile ekleniyor.

Ancak age_check fonksiyonunun kullanılcağı yerde scope içinde tanımlanması gerekiyor. Bizim örneğimizde buun yeri main.rs dosyası. Ayrıca age parametrelerini de mutabel olması gerekiyor. Bu değşiklikleir de altta görebilirsiniz.

use my_macro::show_streams;

// age_check fonksiyonu, age parametresini kontrol edip gerekirse 18'e ayarlar
fn age_check(mut age: i32) -> i32 {
    if age < 18 {
        age = 18;
    }
    age
}

#[show_streams]
fn invoke1() {
    println!("test");
}

#[show_streams(bar)]
fn invoke2(mut age: i32) {
    println!("{}", age);
}

#[show_streams(a => b)]
fn invoke3(mut age: i32) {
    println!("{}", age);
}

#[show_streams { def a = 20; def b = "merhaba"; my_func();}]
fn invoke4() {}

fn main() {
    invoke1();
    invoke2(17);
    invoke3(25);
    invoke4();

    println!("bitti")
}

Kodu çalıştırdığımızda alttaki gibi bir sonuş göreceğiz.

test
invoke1 çalıştıktan sonrasından merhaba
18
invoke2 çalıştıktan sonrasından merhaba
25
invoke3 çalıştıktan sonrasından merhaba
invoke4 çalıştıktan sonrasından merhaba
bitti

Process finished with exit code 0

Dikkat ederseniz biz invode2 fonksiyonuna parametreyi 17 vermiştik ancak ekranımıza 18 olarak basıldı.

Peki aslında ne oldu onu inceleyelim. Bunun için cargo expand komutunu çalıştıralım ve sonuca bakalım.

main.rs dosyamız alttaki değişmiş.

fn age_check(mut age: i32) -> i32 {
    if age < 18 {
        age = 18;
    }
    age
}
fn invoke1() {
    {
        {
            ::std::io::_print(format_args!("test\n"));
        };
    }
    {
        ::std::io::_print(
            format_args!("{0} çalıştıktan sonrasından merhaba\n", "invoke1"),
        );
    };
}
fn invoke2(mut age: i32) {
    age = age_check(age);
    {
        {
            ::std::io::_print(format_args!("{0}\n", age));
        };
    }
    {
        ::std::io::_print(
            format_args!("{0} çalıştıktan sonrasından merhaba\n", "invoke2"),
        );
    };
}
fn invoke3(mut age: i32) {
    age = age_check(age);
    {
        {
            ::std::io::_print(format_args!("{0}\n", age));
        };
    }
    {
        ::std::io::_print(
            format_args!("{0} çalıştıktan sonrasından merhaba\n", "invoke3"),
        );
    };
}
fn invoke4() {
    {}
    {
        ::std::io::_print(
            format_args!("{0} çalıştıktan sonrasından merhaba\n", "invoke4"),
        );
    };
}
fn main() {
    invoke1();
    invoke2(17);
    invoke3(25);
    invoke4();
    {
        ::std::io::_print(format_args!("bitti\n"));
    }
}

Dikkat ederseniz sadece invoke2 ve invoke3 fonksiyonarına age_check fonksiyonu çağrısı eklenmiş.

Alında bakarsanız geröek hayatta iş akışınız değişitrecek bu tarz bir kodu doğrudan bu şekilde yazmamalısınız. Bu tarz işler için validation veya business specification pattern tarzi implementasyonlar yapmak daha mantıklı olacaktır. Tabiiki bu pattern’lerde de makrolar kullanılacak tır ancak bunlar belli bir düzende olacaktır.

Fakat burada verdiğim örnekle cash, loglama, güvenlik vb cross cutting concern tarzı işlemerinizi halldebilirsiniz. Zaten eğer web framework’lerini ncelerseniz bu tarz attribute makrolarının bu tarz işler için sıklıkla kullanıldığını da görebilirsiniz.

Zannediyorum attribute makrosunu anlamk için bu akdar yeterli oalcaktır.

Function-like Macro

İlk bakışta declarative (makro_rules!) makroyu andırır. Ancak bazı farklılıklar vardır.

  • Declarative makrolar belirli desenlerle çalışır. Desene uyan kural çalıştırılır. Kullanımı kolaydır belirli öğeleri hızlıca manipüle etmek ve kodu genişletmek için kullanılır.
  • Procedural (burada tabi özellikle function-like) makrolar ise daha gelişmiş özellikler sunarak kodu analiz etmemizi sağlarken doğrudan (syntax tree) kodu manipüle etmemize izin verir. Yeniden kod üretebilir, kod usilebilir veya nyeni kodlar ekleyebilir.

Şimdi bir iki örnek yapalım konu daja iyi anlaşılacaktır.

Basit bir örnekle başlayalım. Proje yapımız öncekilerle aynı artık tekrar yazmıyorum.

my_macro projesindeki lib.rs dosyasını alttaki kodlasrla değiştiriyoruz.

// lib.rs

use proc_macro::TokenStream;

#[proc_macro]
pub fn add_func(input: TokenStream) -> TokenStream {
    println!("Inputs: {}", input.to_string());
    "fn add(a:i32, b:i32) -> i32 { a + b }".parse().unwrap()
}

Tek bir TokenStream alıyor bu da üzerinde değişiklik yapılacak tree’yi fade ediyor. Geriye de TokenStream döndürüyor bu tahmin edebilceğiniz üzere kodun değişmiş halini ifade ediyor.

my_app dizini aldında yani ana disindek yer alan main.rs dosyamızı da alttaki gibi değiştiriyoruz.

// main.rs

use my_macro::add_func;

add_func!();

fn main() {
    // kmakroyu buraya da koyabilirdik.
    // sonuçta içiçe fonksiyon yazabiliyoruz.
    // add_func!();

    println!("The sum is: {}", add(2, 3));
}

Kodumuzu cargo expand komutu ile incelyecek olursak alttaki gibi kod üretildiğini göreceğiz. Normalde main.rs içinde add diye bir fonkiyon tanımlamamıştık. Ancak main fonksiyonu içinde sanki varmış gibi çağırmıştık. Böylece hata almadan kodumu derledik ve çalışatırabildik.

fn add(a: i32, b: i32) -> i32 {
    a + b
}
fn main() {
    fn add(a: i32, b: i32) -> i32 {
        a + b
    }
    {
        ::std::io::_print(format_args!("The sum is: {0}\n", add(2, 3)));
    };
}

String parametre alıp ekran abasacak bir örnek yapalım.

my_macro altındaki lib.rs doayamızı alttaki gibi değiştiriyoruz.

use proc_macro::TokenStream;
use quote::quote;
use syn::parse_macro_input;

#[proc_macro]
pub fn print_string(input: TokenStream) -> TokenStream {
    // Input token stream'ini parse et
    let input = parse_macro_input!(input as syn::LitStr);

    // String ifadesini al
    let string_value = input.value();

    // Code gen için quote kullanarak Rust kodu üret
    let expanded = quote! {
        println!("{}", #string_value);
    };

    // Üretilen kodu geri döndür
    TokenStream::from(expanded)
}

main.rs dosyamızı da alttaki gibi değiştiriyoruz.

use my_macro::print_string;

fn main() {
    print_string!("Merhaba, dünya!");
    print_string!("Rust procedural makroları ile çalışıyorum!");
}

kodu çalıştırdığımızda alttaki gibi bir çıktı alacağız.

Merhaba, dünya!
Rust procedural makroları ile çalışıyorum!

Şimdi daha önce yaptığımız bir örneği bir de function-like makro ile yapalım. Makromuza parametre olarak geçilen struct’a getter ve setter trait’ini implemet edecek bir function-like makro yazalım.

Bunun için lib.rs dosyamızı alttaki gibi değiştiriyoruz.

use proc_macro::TokenStream;
use quote::quote;
use syn::spanned::Spanned;
use syn::{parse_macro_input, ItemStruct, Fields};

#[proc_macro]
pub fn create_accessors(input: TokenStream) -> TokenStream {
    // Makro girişi olarak verilen TokenStream'i ItemStruct'a parse et
    let input = parse_macro_input!(input as ItemStruct);
    let struct_name = &input.ident;  // struct adını al

    // Struct tanımını oluştur
    let struct_definition = quote! {
        #input
    };

    // Struct'ın alanları üzerinden iterate et
    let gen = match &input.fields {
        Fields::Named(fields) => {
            // Getter ve setter metodları oluştur
            let getters_setters = fields.named.iter().map(|f| {
                let field_name = &f.ident;  // Alan adını al
                let field_type = &f.ty;  // Alan türünü al
                let setter_name = syn::Ident::new(&format!("set_{}", field_name.as_ref().unwrap()), field_name.span());  // Setter adı oluştur

                // Getter metodu tanımı
                let getter = quote! {
                    pub fn #field_name(&self) -> &#field_type {
                        &self.#field_name
                    }
                };

                // Setter metodu tanımı
                let setter = quote! {
                    pub fn #setter_name(&mut self, value: #field_type) {
                        self.#field_name = value;
                    }
                };

                // Getter ve setter metodlarını birleştir
                quote! {
                    #getter
                    #setter
                }
            });

            // Getter ve setter metodlarını struct için implement et
            quote! {
                impl #struct_name {
                    #(#getters_setters)*
                }
            }
        }
        _ => panic!("create_accessors only supports named fields"),  // Yalnızca named fields desteklenir
    };

    // Struct tanımı ve getter-setter implementasyonlarını birleştir
    let output = quote! {
        #struct_definition
        #gen
    };

    TokenStream::from(output)  // Sonuç olarak TokenStream döndür
}

main.rs dosyasını da alttaki gibi değiştiriyoruz.

use my_macro::create_accessors;

create_accessors!(struct Person {
    name: String,
    age: u32,
});

fn main() {
    let mut person = Person {
        name: "Alice".to_string(),
        age: 30,
    };

    // Getter'ları kullan
    println!("Name: {}", person.name());
    println!("Age: {}", person.age());

    // Setter'ları kullan
    person.set_name("Bob".to_string());
    person.set_age(25);

    println!("Updated Name: {}", person.name());
    println!("Updated Age: {}", person.age());
}

Kodun çıktısını artık yazmaya gerek yok zannediyorum. Ancak şunu hatorlatmak istiyorum burada yazdıklarımız sadece konuyu anlamak için yoksa sadece get ve set metrodlarını eklemek için sadece bu şekilde bir makro yazmak mantoıklı olamayabilir.

Mevzuyu sonlandırmadan önce bir konudan daha bahsetmek istiyorum.

Macro Hygiene

Macro Hygiene (makro hijyeni), Rust’ta makroların güvenli ve tahmin edilebilir bir şekilde çalışmasını sağlayan bir özelliktir. Bu kavram, makroların kod üretirken beklenmedik değişken isimleri çakışmalarını ve benzer sorunları önlemeyi amaçlar. Makro hijyeni sayesinde, makrolar tarafından tanımlanan veya kullanılan değişkenler, çağrıldıkları kodun bağlamından bağımsız olarak izole edilir.

Makro hijyenini anlamak için basit bir örnek üzerinde inceleyelim:

macro_rules! create_var {
    () => {
        let x = 42;
        println!("x = {}", x);
    };
}

fn main() {
    let x = 0;
    create_var!();
    println!("Main scope x = {}", x);
}

Yukarıdaki kodda, create_var! makrosu tanımlanıyor ve makro içinde x adlı bir değişken oluşturuluyor. Makro çağrıldığında x değişkeni tanımlanır ve değer olarak 42 atanır. Ancak, main fonksiyonunda da x adlı bir değişken tanımlanmış durumda ve değeri 0. Makro hijyeni sayesinde, makronun içinde tanımlanan x değişkeni, main fonksiyonundaki x değişkeni ile çakışmaz.

Çıktı şu şekilde olacaktır:

x = 42
Main scope x = 0

Bu, makro içinde tanımlanan x değişkeninin main fonksiyonundaki x ile karışmadığını gösterir. Makro hijyeni sayesinde, makro içindeki kod, çağrıldığı yerin yerel değişkenlerinden etkilenmez ve aynı isimli değişkenlerin karışmasına neden olmaz.

Alttaki örneği inceleyelim.

macro_rules! use_external_var {
    ($var:ident) => {
        println!("The value of {} is: {}", stringify!($var), $var);
    };
}

fn main() {
    let x = 100;
    use_external_var!(x); // "x" burada dışarıdan gelen bir değişken adı
}

Yukarıdaki örnekte, use_external_var! makrosu, kendisine verilen ident türündeki x parametresini kullanarak bir değişkeni dışarıdan getirir ve onunla işlem yapar. Bu, makro hijyenini atlatarak x değişkeninin değerine erişir. Makro hijyeni korunsaydı, x değişkeninin makro içinde tanımlanması veya makro tarafından tanımlanmış bir değişken olması gerekirdi.

Çıktı şu şekilde olacaktır:

The value of x is: 100

Makro hijyenini atlatan bu tür kullanımlar, özellikle büyük projelerde beklenmedik davranışlara ve çakışmalara neden olabilir. Örneğin, aşağıdaki senaryoda çakışma görülebilir:

macro_rules! use_external_var {
    ($var:ident) => {
        let $var = 200;
        println!("The value of {} is: {}", stringify!($var), $var);
    };
}

fn main() {
    let x = 100;
    use_external_var!(x); // Burada "x" dışarıdan gelen bir değişken adı
    println!("Main scope x = {}", x);
}

Öıktı şu şekilde olacak.`

The value of x is: 200
Main scope x = 100

Bu durumda, use_external_var! makrosu içindeki let $var = 200; satırı, x değişkenini yeniden tanımlar ve değerini 200 yapar. Ancak bu x, main fonksiyonundaki x ile aynı değildir çünkü makro hijyeni x’in global olarak yeniden tanımlanmasını engeller. Bu, karışıklığa neden olabilir ve kodun davranışını tahmin etmeyi zorlaştırabilir.

Özet olarak makro kullanırken bir yeri etkilememeye ve etkilenmemeye dikkat etmek gerekir. Yani kontrolsüz güç güç eğildir.

Kapanış

Detaya girmeden bir iki taviyede bulunup birkaç tane de faydalı olacağını düşündüğün kaynak vereceğim.

Tavsiyeler

  • Makroları uygularken sıraya dikkat etmelisiniz. Bir biriyle iliklili makrolarda sıra önemli olacaktır.
  • Macro Hygiene konusuna dikkat edin.
  • Aynı kod yapısını birçok yerde kullanma amacınız varsa declarative makro macro_rules! ile bu yapıyı bir defa tanımlayıp her yerde kullanın.
  • Trait uygulamak için derive makro kullanın.
  • Belirli bir yapı üzerinde analiz yapıp ona göre kod üretmek için attribue-like makro kullanın.
  • Belirli bir kod yapısını dönüştürmek veya genişletmek için function-like makro kullanın.
  • Test edilebilir makro yazmaya özen gösterin.
  • Makroların yazımı karışık olduğu gibi eğer iyi dokğmante edilmezse nasıl kulalnılacığını anlamk dakarışık olabilir Bu nedenle çok iyi dokümante edin.

Tavsiye Kaynaklar

  • earthly.dev sayfasında Practical Examples of Macros başlığında gerçek hayatta makro kullanımları hakkında güzel bilgiler var.
  • Rust resmi dokümanlarındaki Macro by Example başlığını kesinlikle inceleyin.
  • Şu Medium linkini de özellikle konuyu biraz daha ileri seviye daha detay konular için okursanız iyi olur.
  • Jetbrains resmi sayfalarında daha basit seviye de şekillerle şemalarla mkroların anlatıldığı bir blok serisi var şu linkten erişebilirsiniz.
  • Sadece rust kamroları için hazırlanmış bir github.io sayfası. Şu linkten ulaşabilirsiniz.
  • Solona Developmen için kaynaklar bulunan soldev.app sayfasında makrolar için güzel bir sayfa hazırlanmı. Adım adım bütün makroları detaylı bir şekilde anlatıyor. Şu linkten ulaşabilirsiniz.

Evet şimdilik Rust hakkındaki ilk yazımızı bu kadar umarım farydalı olmuştur. Bir süre daha vakit buldukça Ruat hakkında yazılarıma devam edeceğim.

Kendinize iyi bakın.

Originally published at https://github.com.

Kaynaklar


메타데이터
post_id
ad3aeeaaafe9
slug
rust-dilinde-makrolar-ve-meta-programlama-metaprogramming-ad3aeeaaafe9
url
https://medium.com/devopsturkiye/rust-dilinde-makrolar-ve-meta-programlama-metaprogramming-ad3aeeaaafe9
canonical_url
https://medium.com/devopsturkiye/rust-dilinde-makrolar-ve-meta-programlama-metaprogramming-ad3aeeaaafe9
author_url
https://medium.com/@mcabuk
status
ok
fetched_at
2026-06-27 18:20:27