Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Macros

Kita udah sering memakai macros (makro) kayak println! di sepanjang buku ini, tapi kita belum sepenuhnya mengeksplorasi apa itu macro dan gimana cara kerjanya. Istilah macro mengacu ke sekumpulan fitur di Rust: macros declarative (deklaratif) yang memakai macro_rules! dan tiga macam macros procedural (prosedural):

  • Macros #[derive] kustom yang menentukan kode yang bakal ditambahin pakai atribut derive yang dipakai pada structs dan enums
  • Macros mirip atribut (attribute-like) yang mendefinisikan atribut kustom yang bisa dipakai pada item apa pun
  • Macros mirip fungsi (function-like) yang kelihatannya kayak pemanggilan fungsi tapi beroperasi pada tokens yang ditentukan sebagai argumen mereka

Kita bakal ngebahas masing-masing dari ini secara bergantian, tapi pertama- tama, mari kita bahas kenapa kita butuh macros padahal kita udah punya fungsi biasa.

Perbedaan Antara Macros dan Fungsi

Secara fundamental, macros itu adalah sebuah cara buat nulis kode yang nulisin kode lain, yang mana dikenal dengan istilah metaprogramming (metapemrograman). Di Lampiran C, kita ngebahas soal atribut derive, yang menghasilkan (generates) sebuah implementasi dari berbagai traits buat kita. Kita juga udah memakai macro println! dan vec! di sepanjang buku ini. Semua macros ini melebar (expand) buat menghasilkan lebih banyak kode ketimbang kode yang secara manual kita tulis.

Metaprogramming itu sangat berguna buat ngurangin seberapa banyak kode yang harus kita tulis dan pelihara, yang mana juga merupakan salah satu dari peran fungsi biasa. Namun, macros punya beberapa kekuatan tambahan yang tidak dipunyai sama fungsi biasa.

Sebuah signature fungsi wajib mendeklarasikan jumlah dan tipe dari parameter-parameter yang dipunyai fungsi tersebut. Macros, di sisi lain, bisa menerima jumlah parameter yang bervariasi (variable number of parameters): kita bisa memanggil println!("hello") dengan satu argumen atau println!("hello {}", name) dengan dua argumen. Selain itu, macros itu dijabarkan (expanded) sebelum compiler menginterpretasi (menafsirkan) makna dari kode tersebut, jadi sebuah macro bisa, contohnya, mengimplementasikan sebuah trait pada suatu tipe tertentu. Sebuah fungsi tidak bisa ngelakuin ini, karena dia dipanggil pas runtime dan sebuah trait wajib diimplementasikan pas compile time.

Kelemahan dari mengimplementasikan sebuah macro ketimbang sebuah fungsi adalah kalau definisi macro itu lebih kompleks daripada definisi fungsi karena kita lagi nulis kode Rust yang bertugas buat nulis kode Rust. Gara-gara ketidaklangsungan (indirection) ini, definisi macro itu umumnya lebih susah buat dibaca, dipahami, dan dipelihara ketimbang definisi fungsi.

Perbedaan penting lainnya antara macros dan fungsi adalah kita wajib mendefinisikan macros atau membawa mereka ke dalam scope (ruang lingkup) sebelum kita manggil mereka di dalam sebuah file, berlawanan dengan fungsi biasa yang bisa kita definisikan di mana aja dan panggil dari mana aja.

Macros Declarative dengan macro_rules! buat Metaprogramming Umum

Bentuk macros yang paling sering dipakai di Rust adalah declarative macro (makro deklaratif). Ini kadang-kadang juga disebut sebagai “macros by example” (makro lewat contoh), “macros macro_rules!”, atau sekadar “macros” doang. Pada intinya, macros deklaratif membiarkan kita nulis sesuatu yang mirip sama ekspresi match di Rust. Seperti yang udah dibahas di Bab 6, ekspresi match adalah struktur kontrol yang mengambil sebuah ekspresi, ngebandingin nilai hasil dari ekspresi tersebut dengan serangkaian patterns (pola), lalu menjalankan kode yang berasosiasi sama pattern yang cocok tersebut. Macros juga membandingkan sebuah nilai terhadap patterns yang diasosiasikan dengan kode tertentu: di situasi ini, nilainya itu adalah kode sumber (source code) Rust literal yang dioper ke dalam macro tersebut; lalu patterns tersebut dibandingkan dengan struktur dari kode sumber tadi; dan kode yang terkait dengan setiap pattern itu, pas dia cocok, bakal menggantikan kode yang dioper ke dalam macro tersebut. Ini semua terjadi pas masa kompilasi.

Buat mendefinisikan sebuah macro, kita memakai konstruk macro_rules!. Mari kita telusuri gimana cara memakai macro_rules! dengan melihat gimana si macro vec! itu didefinisikan. Bab 8 mencakup gimana kita bisa memakai macro vec! buat ngebikin sebuah vector baru dengan nilai-nilai tertentu. Misalnya, macro berikut ini ngebikin sebuah vector baru yang berisi tiga buah integer:

#![allow(unused)]
fn main() {
let v: Vec<u32> = vec![1, 2, 3];
}

Kita juga bisa memakai macro vec! buat ngebikin sebuah vector yang berisi dua integer atau sebuah vector yang berisi lima string slices. Kita tidak bakal bisa memakai fungsi biasa buat ngelakuin hal yang sama karena kita tidak bakal tahu jumlah atau tipe nilai-nilainya secara pasti dari awal (up front).

Listing 20-35 nunjukin definisi yang sedikit disederhanakan dari macro vec!.

Filename: src/lib.rs
#[macro_export]
macro_rules! vec {
    ( $( $x:expr ),* ) => {
        {
            let mut temp_vec = Vec::new();
            $(
                temp_vec.push($x);
            )*
            temp_vec
        }
    };
}
Listing 20-35: Sebuah versi yang disederhanakan dari definisi macro vec!

Catatan: Definisi asli dari macro vec! yang ada di standard library juga mengandung kode buat mengalokasikan (pre-allocate) jumlah memori yang tepat dari awal. Kode tersebut adalah sebuah optimasi (optimization) yang tidak kita sertakan di sini supaya contohnya lebih simpel.

Anotasi #[macro_export] mengindikasikan kalau macro ini seharusnya dibikin tersedia di mana pun crate di mana macro ini didefinisikan dibawa ke dalam scope. Tanpa anotasi ini, si macro tersebut tidak bisa dibawa masuk ke dalam scope.

Terus kita memulai definisi macro-nya dengan macro_rules! dan nama dari macro yang lagi kita definisikan ini tanpa pake tanda seru. Nama itu, di kasus ini yakni vec, lalu diikuti dengan kurung kurawal yang menandakan isi (body) dari definisi macro tersebut.

Struktur yang ada di dalam isi dari vec! ini mirip sekali sama struktur dari ekspresi match. Di sini kita punya satu arm (lengan) dengan pattern ( $( $x:expr ),* ), lalu diikuti dengan => dan blok kode yang terkait sama pattern ini. Kalau pattern ini cocok, blok kode yang terkait itu bakal dipancarkan (emitted). Mengingat bahwa ini adalah satu-satunya pattern yang ada di dalam macro ini, berarti cuma ada satu cara valid buat mencocokkan nilainya; pattern lain apa pun bakal nyebabin error. Macros yang lebih kompleks bakal punya lebih dari satu arm.

Sintaks pattern yang valid di dalam definisi macro itu berbeda dengan sintaks pattern yang udah kita bahas di Bab 19 karena patterns pada macro itu dicocokkan terhadap struktur dari kode Rust ketimbang terhadap nilai. Mari kita telusuri apa arti dari potongan-potongan pattern yang ada di Listing 20-29; buat ngelihat sintaks pattern macro yang seutuhnya, silakan lihat Rust Reference.

Pertama-tama kita memakai sepasang tanda kurung biasa (parentheses) buat ngebungkus keseluruhan pattern tersebut. Kita memakai tanda dolar ($) buat mendeklarasikan sebuah variabel di dalam sistem macro tersebut yang bakal menampung kode Rust yang cocok dengan pattern-nya. Tanda dolar ini ngejelasin (makes it clear) kalau ini adalah sebuah variabel macro bukannya variabel Rust biasa. Berikutnya ada sepasang tanda kurung lagi yang menangkap nilai-nilai yang cocok sama pattern yang ada di dalam tanda kurung tersebut buat dipakai di dalam kode penggantinya. Di dalam $() ada $x:expr, yang mana bakal cocok sama sembarang ekspresi Rust dan lalu ngasih nama $x ke ekspresi tersebut.

Koma yang ada di belakang $() mengindikasikan kalau sebuah karakter pemisah (separator) berupa koma secara literal itu wajib muncul di antara setiap instance kode yang cocok sama kode yang ada di dalam $(). Tanda bintang * menentukan kalau pattern tersebut cocok nol atau sekian kali (zero or more) dari apa pun yang ngeduluin (precedes) tanda * tersebut.

Saat kita memanggil macro ini pakai vec![1, 2, 3];, pattern $x bakal cocok sebanyak tiga kali dengan tiga ekspresi 1, 2, dan 3.

Sekarang mari kita lihat pada pola (pattern) yang ada di dalam isi blok kode yang terkait sama arm ini: temp_vec.push() yang ada di dalam $()* bakal dihasilkan buat setiap bagian yang cocok sama $() di dalam pattern sebelumnya sebanyak nol atau lebih kali (tergantung dari seberapa banyak pattern itu cocok). Si $x bakal diganti dengan masing-masing ekspresi yang cocok tadi. Saat kita manggil macro ini pakai vec![1, 2, 3];, kode yang dihasilkan yang bakal menggantikan pemanggilan macro ini bakal jadi kayak gini:

{
    let mut temp_vec = Vec::new();
    temp_vec.push(1);
    temp_vec.push(2);
    temp_vec.push(3);
    temp_vec
}

Kita udah mendefinisikan sebuah macro yang bisa menerima jumlah argumen sebanyak apa pun yang bertipe apa pun dan bisa menghasilkan kode buat ngebikin sebuah vector yang berisi elemen-elemen yang udah kita tentukan.

Buat belajar lebih jauh soal gimana cara nulis macros, silakan konsultasi ke dokumentasi online atau sumber referensi lainnya, kayak misalnya “The Little Book of Rust Macros” yang dimulai sama Daniel Keep dan terus dilanjutin sama Lukas Wirth.

Macros Procedural Buat Menghasilkan Kode dari Atribut

Bentuk kedua dari macros adalah procedural macro (makro prosedural), yang mana bekerjanya lebih mirip kayak sebuah fungsi (dan emang merupakan sebuah tipe prosedur). Procedural macros nerima beberapa kode sebagai input, lalu beroperasi pada kode tersebut, dan akhirnya memproduksi (menghasilkan) beberapa kode sebagai output, bukannya mencocokkan patterns lalu nggantiin kode tersebut dengan kode lain kayak yang dilakuin sama macros deklaratif. Tiga macam macros prosedural ini adalah derive kustom, mirip atribut (attribute-like), dan mirip fungsi (function-like), dan mereka semua bekerja pakai cara yang mirip-mirip.

Pas lagi membikin macros prosedural, definisi-definisi ini wajib berada di dalam crate mereka sendiri (tersendiri) dengan sebuah tipe crate spesial. Ini terjadi karena alasan-alasan teknis yang rumit yang mana kita harap bisa kita hilangkan di masa depan. Di Listing 20-36, kita nunjukin gimana cara mendefinisikan sebuah macro prosedural, di mana some_attribute itu adalah placeholder buat memakai salah satu variasi spesifik dari macro tersebut.

Filename: src/lib.rs
use proc_macro;

#[some_attribute]
pub fn some_name(input: TokenStream) -> TokenStream {
}
Listing 20-36: Sebuah contoh definisi macro prosedural

Fungsi yang mendefinisikan sebuah macro prosedural menerima sebuah TokenStream sebagai input dan memproduksi sebuah TokenStream sebagai output. Tipe TokenStream ini didefinisikan sama crate proc_macro yang mana emang disertakan bareng Rust dan merepresentasikan sekumpulan dari tokens. Inilah inti (core) dari macro tersebut: kode sumber (source code) yang lagi dioperasikan sama si macro itu ngebentuk input TokenStream tersebut, dan kode yang dihasilkan sama si macro itu adalah output TokenStream hasilnya. Fungsi ini juga punya atribut yang nempel ke dirinya yang menentukan jenis macro prosedural apa yang lagi kita bikin. Kita bisa punya berbagai jenis macros prosedural di dalam satu crate yang sama.

Mari kita ngelihat jenis-jenis dari macros prosedural tersebut. Kita bakal mulai dengan macro derive kustom dan terus ngejelasin sedikit perbedaan kecil (small dissimilarities) yang ngebikin bentuk-bentuk lainnya jadi beda.

Gimana Cara Menulis Sebuah Macro derive Kustom

Mari kita bikin sebuah crate bernama hello_macro yang mendefinisikan sebuah trait bernama HelloMacro dengan satu fungsi associated bernama hello_macro. Ketimbang harus maksa supaya user kita mengimplementasikan trait HelloMacro secara manual buat setiap tipe mereka, kita bakal nyediain sebuah macro prosedural sehingga para pengguna bisa menganotasi tipe mereka dengan #[derive(HelloMacro)] buat dapetin implementasi default (bawaan) dari fungsi hello_macro ini. Implementasi default ini bakal mencetak Hello, Macro! My name is TypeName! di mana TypeName itu adalah nama dari tipe di mana trait ini baru aja didefinisikan. Dengan kata lain, kita bakal nulis sebuah crate yang memungkinkan programmer lain buat nulis kode kayak di Listing 20-37 dengan memakai crate kita.

Filename: src/main.rs
use hello_macro::HelloMacro;
use hello_macro_derive::HelloMacro;

#[derive(HelloMacro)]
struct Pancakes;

fn main() {
    Pancakes::hello_macro();
}
Listing 20-37: Kode yang bisa ditulis sama user crate kita pas lagi memakai macro prosedural kita

Kode ini bakal mencetak Hello, Macro! My name is Pancakes! pas kita udah selesai. Langkah pertamanya adalah membikin library crate baru, kayak gini:

$ cargo new hello_macro --lib

Berikutnya, di Listing 20-38, kita bakal mendefinisikan trait HelloMacro dan fungsi associated-nya.

Filename: src/lib.rs
pub trait HelloMacro {
    fn hello_macro();
}
Listing 20-38: Sebuah trait sederhana yang bakal kita pakai bareng macro derive

Kita udah punya trait dan fungsinya. Pada titik ini, user dari crate kita udah bisa mengimplementasikan trait ini buat dapetin fungsionalitas yang mereka inginkan, kayak di Listing 20-39.

Filename: src/main.rs
use hello_macro::HelloMacro;

struct Pancakes;

impl HelloMacro for Pancakes {
    fn hello_macro() {
        println!("Hello, Macro! My name is Pancakes!");
    }
}

fn main() {
    Pancakes::hello_macro();
}
Listing 20-39: Gimana kelihatannya kalau users menulis implementasi manual dari trait HelloMacro

Namun, mereka masih butuh buat nulis blok implementasi ini buat setiap tipe yang pengen mereka pakai dengan hello_macro; kita mau nyelametin mereka dari keharusan (having to do) ngelakuin kerjaan ini.

Selain itu, kita belum bisa nyediain fungsi hello_macro dengan implementasi default yang mana bakal bisa nyetak nama dari tipe di mana trait itu diimplementasikan: Rust tidak punya kapabilitas reflection (kemampuan buat meneliti tipe-tipe saat jalan), jadi dia tidak bisa nyari tahu nama dari sebuah tipe pas runtime dateng. Kita butuh macro buat bisa menghasilkan (generate) kode tersebut saat compile time.

Langkah selanjutnya adalah mendefinisikan macro prosedural tersebut. Saat tulisan ini dibikin, macros prosedural wajib berada di dalam crate mereka sendiri (terpisah). Suatu hari nanti, pembatasan ini mungkin bakal diangkat (lifted). Konvensi buat menata struktur (structuring) crates dan crates macro itu kayak gini: buat sebuah crate yang namanya foo, crate macro prosedural derive kustomnya itu dikasih nama foo_derive. Mari kita mulai crate baru bernama hello_macro_derive di dalam project hello_macro kita:

$ cargo new hello_macro_derive --lib

Dua crates kita ini itu saling berkaitan erat (tightly related), jadi kita ngebikin crate macro prosedural ini di dalam directory dari crate hello_macro kita. Kalau kita ngubah definisi trait yang ada di dalam hello_macro, kita juga bakal harus mengubah implementasi macro prosedural di dalam hello_macro_derive. Kedua crates ini harus dipublikasikan (published) secara terpisah, dan programmer-programmer yang memakai crates ini harus menambahkan dua-duanya sebagai dependensi lalu membawa keduanya masuk ke dalam scope. Sebagai gantinya, kita juga bisa sih bikin supaya crate hello_macro itu memakai hello_macro_derive sebagai dependensi terus dia yang mengekspor ulang (re-export) kode macro prosedural tersebut. Namun, cara kita menata struktur project kita ini membiarkan programmer-programmer buat memakai hello_macro bahkan kalau mereka sebenernya tidak butuh sama fungsionalitas derive-nya.

Kita perlu mendeklarasikan crate hello_macro_derive ini sebagai sebuah crate macro prosedural. Kita juga bakal butuh fungsionalitas dari crates syn dan quote, kayak yang bakal kita lihat sebentar lagi, jadi kita perlu nambahin mereka sebagai dependensi. Tambahkan yang berikut ini ke dalam file Cargo.toml untuk hello_macro_derive:

Filename: hello_macro_derive/Cargo.toml
[lib]
proc-macro = true

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

Buat mulai mendefinisikan macro prosedural ini, tempatin kode dari Listing 20-40 ke dalam file src/lib.rs kita buat crate hello_macro_derive. Perhatikan bahwa kode ini belum bakal bisa di-compile sampai kita udah nambahin definisi buat fungsi impl_hello_macro.

Filename: hello_macro_derive/src/lib.rs
use proc_macro::TokenStream;
use quote::quote;

#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
    // Construct a representation of Rust code as a syntax tree
    // that we can manipulate.
    let ast = syn::parse(input).unwrap();

    // Build the trait implementation.
    impl_hello_macro(&ast)
}
Listing 20-40: Kode yang mana sebagian besar crates macro prosedural butuhkan buat memproses kode Rust

Coba perhatikan kalau kita udah membelah (split) kodenya ke dalam fungsi hello_macro_derive, yang mana bertanggung jawab buat nge-parse (mengurai/menguraikan) si TokenStream, dan fungsi impl_hello_macro, yang mana bertanggung jawab buat mengubah (transforming) struktur pohon sintaksnya (syntax tree): ini ngebikin penulisan sebuah macro prosedural jadi lebih nyaman (convenient). Kode di dalam fungsi yang luar (hello_macro_derive di kasus ini) itu bakal sama aja bunyinya buat hampir sebagian besar dari crates macro prosedural yang pernah kita lihat atau bikin. Kode yang kita tentuin di dalam isi fungsi dalamnya (impl_hello_macro di kasus ini) itu bakal berbeda-beda tergantung dari apa tujuan macro prosedural kita itu sebenernya.

Kita udah memperkenalkan tiga crates baru di sini: proc_macro, syn, dan quote. Crate proc_macro itu emang udah dibawa bareng sama Rust, jadi kita tidak perlu nambahin dia ke dalam dependensi di Cargo.toml kita. Crate proc_macro ini adalah API dari compiler yang memungkinkan kita buat membaca dan memanipulasi kode Rust dari dalam kode kita.

Crate syn itu mem-parse kode Rust yang asalnya dari string menjadi sebuah struktur data yang mana bisa kita operasikan lebih lanjut. Crate quote kemudian mengubah si struktur data syn tersebut kembali (turns back) menjadi kode Rust biasa. Crates ini ngebikin gampang sekali buat mem-parse segala macam kode Rust apa pun yang mungkin pengen kita tangani (handle): nulis parser yang sempurna (full parser) buat bahasa pemrograman Rust bukanlah tugas yang gampang lho.

Fungsi hello_macro_derive bakal dipanggil pas ada user library kita yang mencantumkan #[derive(HelloMacro)] pada sebuah tipe. Hal ini dimungkinkan karena kita udah ngasih anotasi ke fungsi hello_macro_derive di sini pakai proc_macro_derive dan nentuin nama HelloMacro, yang mana nama ini emang cocok sama nama dari trait kita; ini adalah konvensi yang paling banyak diikuti sama macros prosedural.

Fungsi hello_macro_derive pertama-tama mengkonversi input yang asalnya dari sebuah TokenStream menjadi sebuah struktur data yang lalu bisa kita interpretasikan dan kita operasikan lebih lanjut. Di sinilah si syn ikut campur. Fungsi parse di dalam syn mengambil sebuah TokenStream lalu mengembalikan sebuah struct DeriveInput yang merepresentasikan kode Rust yang udah selesai di-parse. Listing 20-41 nunjukin bagian-bagian yang relevan dari struct DeriveInput yang kita dapetin sebagai hasil dari menge-parse string struct Pancakes;.

DeriveInput {
    // --snip--

    ident: Ident {
        ident: "Pancakes",
        span: #0 bytes(95..103)
    },
    data: Struct(
        DataStruct {
            struct_token: Struct,
            fields: Unit,
            semi_token: Some(
                Semi
            )
        }
    )
}
Listing 20-41: Instance DeriveInput yang kita dapetin pas kita mem-parse kode yang punya atribut macro tersebut di Listing 20-37

Bidang (fields) dari struct ini nunjukin kalau kode Rust yang udah kita parse ini adalah sebuah unit struct dengan ident (identifier/pengidentifikasi, yang artinya nama) Pancakes. Ada lebih banyak bidang lagi pada struct ini buat mendeskripsikan segala macam variasi kode Rust; silakan cek dokumentasi syn untuk DeriveInput buat informasi lebih lengkap.

Bentar lagi kita bakal mendefinisikan fungsi impl_hello_macro, yang mana ini bakal jadi tempat di mana kita ngebangun kode Rust baru yang pengen kita masukkan (include) ke dalem programnya. Tapi sebelum kita melakukan itu, perhatikan kalau output dari macro derive kita ini juga merupakan sebuah TokenStream. Si TokenStream kembalian (returned) ini bakal ditambahin ke kode yang dibikin sama user crate kita, jadi pas mereka mengompilasi (compile) crate mereka, mereka bakal dapetin fungsi tambahan yang udah kita sediain di dalam TokenStream yang udah dimodifikasi tadi.

Kita mungkin sempat merhatiin kalau kita tadi memanggil unwrap yang mana bakal ngebikin fungsi hello_macro_derive jadi panic kalau pemanggilan ke fungsi syn::parse tersebut ternyata gagal (fails) di sini. Sangat diwajibkan buat macro prosedural kita supaya jadi panic pas ada error karena fungsi-fungsi proc_macro_derive itu wajib ngembaliin tipe TokenStream ketimbang Result supaya dia bisa patuh (conform) sama API macro prosedural tersebut. Kita udah menyederhanakan contoh ini dengan memakai unwrap; kalau di dalem kode produksi sungguhan (production code), kita harusnya menyediakan pesan error yang jauh lebih spesifik soal apa yang sebenernya salah dengan memakai panic! atau expect.

Nah, sekarang karena kita udah punya kode buat ngubah kode Rust yang udah dianotasikan (annotated Rust code) dari sebuah TokenStream menjadi sebuah instance DeriveInput, mari kita hasilkan kode (generate the code) yang mengimplementasikan trait HelloMacro pada tipe yang dianotasi tersebut, kayak yang ditunjukin di Listing 20-42.

Filename: hello_macro_derive/src/lib.rs
use proc_macro::TokenStream;
use quote::quote;

#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
    // Construct a representation of Rust code as a syntax tree
    // that we can manipulate
    let ast = syn::parse(input).unwrap();

    // Build the trait implementation
    impl_hello_macro(&ast)
}

fn impl_hello_macro(ast: &syn::DeriveInput) -> TokenStream {
    let name = &ast.ident;
    let generated = quote! {
        impl HelloMacro for #name {
            fn hello_macro() {
                println!("Hello, Macro! My name is {}!", stringify!(#name));
            }
        }
    };
    generated.into()
}
Listing 20-42: Mengimplementasikan trait HelloMacro memakai kode Rust yang udah diparse

Kita mendapatkan sebuah instance dari struct Ident yang mengandung nama (identifier) dari tipe yang dianotasi tadi memakai ast.ident. Struct yang ada di Listing 20-41 tadi nunjukin kalau pas kita ngejalanin fungsi impl_hello_macro pada kode yang ada di Listing 20-37, si ident yang bakal kita dapet ini bakal punya field ident dengan nilai "Pancakes". Oleh karenanya variabel name di dalam Listing 20-42 bakal berisi instance struct Ident yang mana, pas dicetak, dia bakal ngasih string "Pancakes", nama dari struct di dalam Listing 20-37.

Macro quote! membiarkan kita mendefinisikan kode Rust yang mau kita kembalikan (return). Compiler mengharapkan sesuatu yang agak beda dengan hasil eksekusi langsung dari macro quote!, jadi kita perlu buat ngubahnya (convert it) menjadi sebuah TokenStream. Kita melakukan hal ini dengan cara memanggil method into, yang mana memakan (consumes) si representasi menengah (intermediate representation) ini lalu mengembalikan sebuah nilai dengan tipe TokenStream yang diwajibkan tersebut.

Macro quote! ini juga nyediain mekanisme templat (templating) yang keren sekali lho: kita bisa memasukkan #name, dan quote! bakal nggantiin itu dengan nilai yang ada di dalam variabel name. Kita bahkan bisa ngelakuin pengulangan yang mirip sama cara kerja macros yang biasa. Silakan cek dokumentasi crate quote buat perkenalan yang komprehensif (thorough).

Kita pengen macro prosedural kita ini buat menghasilkan sebuah implementasi dari trait HelloMacro kita buat tipe yang dianotasi sama si user, yang mana nama tipenya itu bisa kita dapetin dengan memakai #name. Implementasi trait-nya ini punya satu fungsi doang yaitu hello_macro, di mana isinya (body) mengandung fungsionalitas yang pengen kita berikan: yaitu mencetak tulisan Hello, Macro! My name is lalu diikuti dengan nama dari tipe yang dianotasi tersebut.

Macro stringify! yang dipakai di sini emang udah tertanam (built into) di dalam Rust. Dia mengambil (takes) sebuah ekspresi Rust, kayak misalnya 1 + 2, lalu pada saat compile time dia bakal ngerubah ekspresi tersebut jadi string literal (string harfiah), kayak misalnya "1 + 2". Ini beda sama format! atau println!, dua macro ini kan mengevaluasi ekspresinya dan baru kemudian ngubah hasilnya jadi sebuah String. Ada kemungkinan input #name ini bisa jadi adalah sebuah ekspresi yang mana harus dicetak apa adanya secara harfiah (literally), jadi makanya kita memakai stringify!. Memakai stringify! juga ngirit alokasi (saves an allocation) karena dia ngubah #name jadi string literal saat compile time (saat kompilasi).

Pada titik ini, jalanin cargo build seharusnya udah bisa kelar tanpa masalah di dalam hello_macro sekaligus hello_macro_derive. Mari kita pasangkan (hook up) crates ini dengan kode yang ada di dalam Listing 20-37 tadi buat ngelihat gimana macro prosedural kita ini beraksi! Bikin sebuah project binary baru di dalam directory projects kita memakai cargo new pancakes. Kita perlu menambahkan hello_macro dan hello_macro_derive sebagai dependensi (dependencies) di dalam Cargo.toml milik crate pancakes ini. Kalau seandainya kita mempublikasikan versi dari hello_macro dan hello_macro_derive punya kita ke crates.io, mereka bakal jadi kayak dependensi biasa; kalau belum dipublikasikan, kita bisa menspesifikasikan mereka sebagai dependensi bertipe path (jalur ke folder) kayak gini:

[dependencies]
hello_macro = { path = "../hello_macro" }
hello_macro_derive = { path = "../hello_macro/hello_macro_derive" }

Masukin kode dari Listing 20-37 tadi ke dalam file src/main.rs, terus coba jalanin cargo run: dia seharusnya mencetak Hello, Macro! My name is Pancakes! Implementasi dari trait HelloMacro dari macro prosedural tersebut udah dimasukkan (included) tanpa crate pancakes ini harus mengimplementasikannya sendiri; si atribut #[derive(HelloMacro)] itulah yang udah nambahin implementasi trait tersebut secara otomatis.

Selanjutnya, mari kita telusuri di mana letak perbedaan dari jenis-jenis macro prosedural lain kalau dibandingin dengan macros derive kustom.

Macros Mirip Atribut (Attribute-Like Macros)

Macros yang mirip atribut itu serupa sama macros derive kustom, tapi ketimbang menghasilkan (generating) kode buat atribut derive, macros ini ngebolehin kita buat membikin atribut baru (new attributes). Mereka juga lebih fleksibel: derive itu kan cuma bisa kerja buat structs dan enums doang; sedangkan atribut itu bisa aja diterapkan ke item-item lain, kayak fungsi misalnya. Berikut ini adalah sebuah contoh penggunaan macro yang mirip atribut. Katakanlah kita punya sebuah atribut bernama route yang mana nganotasi fungsi-fungsi pas lagi makek framework aplikasi web (web application framework):

#[route(GET, "/")]
fn index() {

Atribut #[route] ini idealnya bakal didefinisikan sama framework tersebut sebagai sebuah macro prosedural. Signature dari fungsi pendefinisi macro-nya (macro definition function) mungkin bakal kelihatan kayak gini:

#[proc_macro_attribute]
pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream {

Di sini, kita punya dua parameter bertipe TokenStream. Yang pertama itu adalah buat menampung konten dari atributnya: yaitu di bagian GET, "/". Terus yang kedua itu adalah buat isinya (body) si item di mana atribut itu ditempelkan (attached to): di contoh kasus ini, buat si fn index() {} beserta sisa isi dari fungsi tersebut.

Selain perbedaan tadi, macros mirip atribut ini bekerja dengan cara yang sama persis dengan macros derive kustom: kita membikin sebuah crate dengan tipe crate proc-macro lalu kita mengimplementasikan fungsi yang tugasnya buat ngehasilin kode (generates the code) yang pengen kita bikin!

Macros Mirip Fungsi (Function-Like Macros)

Macros mirip fungsi itu mendefinisikan macros yang kelihatannya kayak pemanggilan fungsi. Sama kayak macros macro_rules!, macros ini itu lebih luwes (fleksibel) daripada fungsi biasa; misalnya, mereka bisa nerima jumlah argumen yang bervariasi (unknown number of arguments). Namun, macros macro_rules! itu cuma bisa didefinisikan memakai sintaks yang mirip match (match-like syntax) yang udah kita obrolin di “Macros Declarative dengan macro_rules! buat Metaprogramming Umum” tadi sebelumnya. Macros yang mirip fungsi ini mengambil satu parameter TokenStream dan kemudian di definisinya dia memanipulasi TokenStream tersebut memakai kode Rust sama persis kayak apa yang dilakuin sama kedua tipe macros prosedural sebelumnya. Sebuah contoh dari macro mirip fungsi adalah macro sql! yang mana mungkin bakal dipanggil kayak gini:

let sql = sql!(SELECT * FROM posts WHERE id=1);

Macro ini bakal mem-parse pernyataan (statement) SQL yang ada di dalamnya dan ngecek apakah secara sintaks itu (syntactically) benar atau tidak, yang mana itu merupakan pemrosesan yang jauh lebih ribet (complex processing) ketimbang apa yang sanggup dilakukan sama macro tipe macro_rules!. Definisi macro sql! ini kelihatannya bakal kayak gini:

#[proc_macro]
pub fn sql(input: TokenStream) -> TokenStream {

Definisi ini mirip sekali kan sama signature dari macro derive kustom tadi: kita menerima tokens yang ada di dalam tanda kurung tersebut (parentheses) dan terus kita ngembaliin (return) kode yang emang pengen kita hasilin (generate).

Ringkasan

Fiuh! (Whew!) Nah sekarang kita punya segelintir fitur Rust baru di sabuk perkakas (toolbox) kita yang mana kemungkinan besar kita tidak bakal sering memakainya, tapi seenggaknya kita bakal tahu kalau mereka itu tersedia di situasi-situasi tertentu yang amat spesifik. Kita udah memperkenalkan beberapa topik yang kompleks (complex topics) sehingga saat kita kebetulan menjumpainya di dalam saran-saran pesan error (error message suggestions) atau di dalam kode orang lain, kita bakal sanggup buat mengenali berbagai macam konsep dan sintaks ini. Gunakan bab ini sebagai sebuah pedoman referensi (reference guide) buat ngebimbing kita nemuin solusinya.

Berikutnya, kita bakal mempraktikkan (put into practice) segala macam hal yang udah kita bicarain di sepanjang buku ini dengan cara mengerjakan satu project lagi!