macro_rules! menempa kode lewat pencocokan pola: arm, fragment, repetisi. Pendekatan declarative tersebut kuat untuk antarmuka token yang bentuknya teratur, tetapi lemah ketika hasil ekspansi harus bergantung pada isi item — misalnya #[derive(Debug)] perlu membaca nama field satu per satu untuk menghasilkan kode pencetakan, pekerjaan yang pola $(...),* tidak sanggup tuntaskan dengan nyaman. Untuk kebutuhan itulah Rust memiliki macro prosedural: macro yang ditulis sebagai fungsi Rust sungguhan, dikompilasi menjadi pustaka khusus, lalu dimuat dan dijalankan oleh kompilator pada saat kompilasi kode pengguna. Chapter penutup Section ini membahas tiga jenis macro prosedural — derive, attribute, dan function-like — beserta struktur crate yang diperlukan, dengan contoh nyata yang benar-benar kita bangun dan jalankan dengan cargo.

Dua Keluarga Macro di Rust #

Macro declarative dan macro prosedural membagi satu ide dasar — keduanya menghasilkan kode saat kompilasi — tetapi bekerja dengan cara yang berbeda total. Macro declarative (macro_rules!) tidak memiliki logika program: hasil ekspansi sepenuhnya ditentukan oleh pencocokan pola atas token pemanggilan. Macro prosedural berisi kode Rust arbitrer: input macro berupa TokenStream (aliran token), kode kita bebas memeriksa, memilah, dan mengubah token tersebut, lalu mengembalikan TokenStream baru sebagai hasil ekspansi. Karena berbentuk fungsi biasa, macro prosedural boleh memakai loop, struct, method — semua fasilitas bahasa yang telah kita pelajari sepanjang Course.

Konsekuensi arsitekturnya: macro prosedural harus tinggal di crate tersendiri dengan penanda khusus. Kompilator memuat crate tersebut layaknya plugin ketika menemukan pemakaianya, menjalankan fungsi macro dengan token kode pengguna sebagai input, lalu menyuntikkan output token ke kode yang sedang dikompilasi. Karena mekanisme penyuntingan kode se rumit ini, rustc satu file — alat uji andalan kita sepanjang Section — tidak dapat mendemokan macro prosedural; kita membutuhkan proyek cargo dengan dua crate.

Tiga Jenis Macro Prosedural #

Rust membedakan tiga posisi pemakaian macro, dan tiap posisi punya atribut serta bentuk fungsi sendiri. Ketiganya menerima TokenStream dan mengembalikan TokenStream; pembedanya adalah dari mana input berasal dan ke mana output disuntikkan.

Derive macro menempel pada struct, enum, atau union lewat #[derive(Nama)]. Input fungsi adalah token item yang diberi derive; output ditambahkan setelah item tersebut. Contoh nyata di ekosistem: #[derive(Clone)] dan #[derive(Debug)] dari pustaka standar, #[derive(Serialize)] dari crate serde. Kerangkanya:

rust
#[proc_macro_derive(Perkenalan)]
pub fn perkenalan_derive(input: TokenStream) -> TokenStream {
    // input: token struct yang diberi #[derive(Perkenalan)]
    // output: kode tambahan yang disuntikkan SETELAH struct
    todo!()
}

Attribute macro menempel pada item apa pun lewat #[nama_attribute(...)]. Fungsi menerima dua input: isi dalam kurung attribute dan item yang ditandai. Output menggantikan item tersebut seluruhnya. Contoh nyata: #[tokio::main] yang menuliskan ulang fn main menjadi runtime async, #[serde(rename_all = "camelCase")]. Kerangkanya:

rust
#[proc_macro_attribute]
pub fn tandai(attr: TokenStream, item: TokenStream) -> TokenStream {
    // attr: token di dalam tanda kurung #[tandai(...)]
    // item: token kode yang ditandai
    // output: pengganti item
    todo!()
}

Function-like macro dipanggil seperti macro_rules!: nama!(...). Input adalah token di dalam kurung pemanggilan; output menggantikan pemanggilan. Contoh nyata: sqlx::query! yang memeriksa query SQL terhadap skema basis data saat kompilasi. Kerangkanya:

rust
#[proc_macro]
pub fn teriak(input: TokenStream) -> TokenStream {
    // input: token di dalam teriak!(...)
    // output: pengganti pemanggilan
    todo!()
}

Struktur Crate proc-macro #

Ketiga macro di atas tinggal dalam satu crate dengan penanda proc-macro = true pada bagian [lib]:

toml
[package]
name = "macro_kit"
version = "0.1.0"
edition = "2021"

[lib]
proc-macro = true

Penanda tersebut mengubah sifat crate: crate hanya boleh mengekspor macro prosedural, tidak boleh mengekspor fungsi atau tipe biasa, dan hasil kompilasinya berupa pustaka yang dimuat kompilator — bukan library yang ditautkan ke program pengguna. Kode pengguna tinggal di crate kedua yang menggantungkan diri pada macro_kit lewat dependensi path:

text
proyek-makro/
├── macro_kit/
│   ├── Cargo.toml
│   └── src/
│       └── lib.rs
└── aplikasi/
    ├── Cargo.toml
    └── src/
        └── main.rs
toml
[package]
name = "aplikasi"
version = "0.1.0"
edition = "2021"

[dependencies]
macro_kit = { path = "../macro_kit" }

Alur kerjanya dapat dipetakan sebagai berikut:

Diagram tersebut menegaskan posisi macro prosedural di pipeline: seluruh pekerjaan plugin terjadi sebelum type check, dan kode hasil penyuntikan tetap wajib lulus pemeriksaan tipe.

Kit Macro Minimal yang Teruji #

Ketiga kerangka di atas memang todo!(), tetapi kit minimal yang benar-benar bekerja tidak harus rumit — kita tidak butuh pustaka parsing eksternal untuk kasus sederhana. Isi penuh macro_kit/src/lib.rs berikut menerapkan ketiga jenis macro dengan pemeriksaan token manual:

rust
use proc_macro::{TokenStream, TokenTree};

#[proc_macro_derive(Perkenalan)]
pub fn perkenalan_derive(input: TokenStream) -> TokenStream {
    // Cari ident pertama setelah keyword `struct`: itulah nama struct-nya
    let mut nama_struct = String::new();
    let mut setelah_struct = false;
    for token in input {
        if let TokenTree::Ident(ident) = token {
            if setelah_struct {
                nama_struct = ident.to_string();
                break;
            }
            if ident.to_string() == "struct" {
                setelah_struct = true;
            }
        }
    }

    let kode = format!(
        "impl {} {{ pub fn nama_struct() -> &'static str {{ \"{}\" }} }}",
        nama_struct, nama_struct
    );
    kode.parse().unwrap()
}

#[proc_macro_attribute]
pub fn tandai(_attr: TokenStream, item: TokenStream) -> TokenStream {
    // attr diabaikan; item diteruskan utuh plus satu const penanda
    let gabungan = format!("pub const SUDAH_DITANDAI: bool = true;\n{}", item);
    gabungan.parse().unwrap()
}

#[proc_macro]
pub fn teriak(input: TokenStream) -> TokenStream {
    // input berupa satu string literal, contoh: "promo"
    let isi = input.to_string().trim_matches('"').to_uppercase();
    let kode = format!("String::from(\"{}!!!\")", isi);
    kode.parse().unwrap()
}

Kode pengguna di aplikasi/src/main.rs:

rust
use macro_kit::{Perkenalan, tandai, teriak};

#[derive(Perkenalan)]
struct Menu {
    nama: String,
    harga: u32,
}

#[tandai]
fn sambutan() -> &'static str {
    "Selamat datang di Toko Kopi Nusantara"
}

fn main() {
    let menu = Menu {
        nama: String::from("Kopi Gayo 250g"),
        harga: 85000,
    };
    println!("{}", sambutan());
    println!("{} Rp{}", menu.nama, menu.harga);
    println!("Struct hasil derive: {}", Menu::nama_struct());
    println!("Seruan: {}", teriak!("promo"));
    println!("Penanda: {}", SUDAH_DITANDAI);
}

Jalankan dari direktori aplikasi:

bash
cargo run

Output yang Dihasilkan:

text
Selamat datang di Toko Kopi Nusantara
Kopi Gayo 250g Rp85000
Struct hasil derive: Menu
Seruan: PROMO!!!
Penanda: true

Mari kita bedah bagian-bagian kuncinya:

  1. Baris 8 (for token in input): TokenStream dapat diiterasi; tiap elemen adalah TokenTree — satu token berbentuk pohon. Derive kita memindai token hingga menemukan identifier struct, lalu mengambil identifier berikutnya sebagai nama struct — cara manual yang cukup untuk struct tanpa generik.
  2. Baris 20-24 (format! lalu kode.parse().unwrap()): kode hasil dibangun sebagai teks lewat format!, lalu parse() mengubah teks menjadi TokenStream — fungsi macro wajib mengembalikan TokenStream, bukan String.
  3. Baris 30 (format!("pub const SUDAH_DITANDAI ...")): attribute macro menggabungkan satu const penanda dengan item asli. Karena output attribute macro menggantikan item, const tersebut menjadi bagian kode pengguna — itulah kenapa main dapat memanggil SUDAH_DITANDAI di Baris 23 main.rs.
  4. Baris 37 (input.to_string().trim_matches('"')): function-like macro menerima "promo" sebagai token literal; representasi teksnya memuat tanda kutip, sehingga trim_matches('"') membuang kutip pembuka dan penutup sebelum to_uppercase() menghasilkan PROMO.
  5. Baris 3-4 main.rs (#[derive(Perkenalan)]): derive menyuntikkan impl Menu { pub fn nama_struct() ... } setelah struct — pemanggilan Menu::nama_struct() pada Baris 21 hanyalah pemanggilan fungsi biasa hasil suntikan.

Ringkasan #

Macro prosedural menutup jurang yang tidak dijangkau macro_rules!: logika ekspansi berupa fungsi Rust sungguhan yang menerima TokenStream dan mengembalikan TokenStream, dijalankan kompilator sebagai plugin sebelum type check. Tiga jenisnya dibedakan oleh posisi pemakaian — derive menyuntik kode setelah item, attribute menggantikan item, function-like menggantikan pemanggilan — dan semuanya wajib tinggal di crate terpisah dengan proc-macro = true. Kit minimal macro_kit membuktikan ketiganya bekerja dalam proyek cargo nyata tanpa dependensi eksternal, sekaligus menunjukkan titik di mana syn dan quote sebaiknya mengambil alih. Dengan Section Macros tuntas, kita memegang dua keluarga macro Rust: yang declarative untuk antarmuka token ringkas, dan yang prosedural untuk kode yang menulis kode secara sungguhan.