Lewati ke konten

Makro Prosedural di Rust: Membuat #[derive] Sendiri

Setiap kali kamu menulis #[derive(Debug)] atau #[derive(Serialize)], ada program kecil yang berjalan di dalam compiler. Program itu melihat struct kamu lalu menuliskan blok impl untukmu. Program kecil itu adalah procedural macro (makro prosedural), dan kamu juga bisa membuatnya sendiri.

Bayangkan seorang petugas administrasi yang sangat membantu. Kamu menyerahkan formulir (struct kamu), dia membaca setiap kolomnya, lalu mengembalikan semua dokumen membosankan yang sudah terisi. Di halaman Makro kamu sudah berkenalan dengan macro_rules!, yang bekerja dengan mencocokkan pola. Procedural macro berbeda: ia adalah fungsi Rust biasa yang menerima kode dan mengembalikan kode. Di halaman ini kamu akan membuat satu yang sungguhan, langkah demi langkah, dan melihat output aslinya.

Ada tiga jenis. Di dalamnya cara kerjanya sama, tapi bagi orang yang memakainya bentuknya berbeda:

Jenis Yang ditulis pengguna Contoh terkenal
Derive macro #[derive(Serialize)] di atas struct atau enum Serialize dari serde, Error dari thiserror, Parser dari clap
Attribute macro #[route(GET, "/")] di atas fungsi atau item lain #[tokio::main], #[get("/")] dari actix-web
Function-like macro sql!(SELECT * FROM users) query! dari sqlx, html! dari yew
  • Derive macro hanya menambahkan kode di samping tipe kamu. Struct kamu tetap persis seperti yang kamu tulis.
  • Attribute macro mengganti item tempat ia dipasang. #[tokio::main] mengambil async fn main kamu dan mengubahnya menjadi fn main biasa yang menyalakan runtime tokio.
  • Function-like macro terlihat seperti pemanggilan macro_rules!, tapi sebuah fungsi Rust yang memutuskan arti input-nya. Begitulah cara sqlx::query! bisa memeriksa SQL kamu saat kompilasi.

Sebenarnya kamu sudah memakai derive macro dari crate lain. Ini dua di antaranya yang sedang bekerja sungguhan. Serialize menulis kode JSON-nya (lihat JSON dengan Serde), dan Error menulis kode Display dari teks #[error(...)] (lihat anyhow dan thiserror):

use serde::Serialize;
use thiserror::Error;
#[derive(Serialize)]
struct Order {
id: u32,
item: String,
quantity: u8,
}
#[derive(Debug, Error)]
enum ShopError {
#[error("stok barang {0} sedang habis")]
OutOfStock(String),
}
fn main() {
let order = Order { id: 42, item: "Teko".to_string(), quantity: 2 };
println!("{}", serde_json::to_string(&order).unwrap());
let err = ShopError::OutOfStock("Teko".to_string());
println!("{err}");
}

Output:

{"id":42,"item":"Teko","quantity":2}
stok barang Teko sedang habis
▶ Coba sendiri

Kamu sama sekali tidak menulis impl Display atau kode JSON apa pun. Derive-lah yang menuliskannya saat program kamu dikompilasi.

Pertama-tama, compiler memotong kode sumber kamu menjadi token: nama, tanda baca, literal, dan tanda kurung. Procedural macro adalah fungsi yang menerima token-token itu sebagai TokenStream dan mengembalikan TokenStream baru. Setelah itu compiler mengompilasi apa pun yang kamu kembalikan, seolah-olah kamu sendiri yang mengetiknya.

Ini tiga bentuk fungsinya. Masing-masing diberi atribut supaya compiler tahu jenisnya:

use proc_macro::TokenStream;
// #[derive(HelloMacro)]: menerima struct atau enum, mengembalikan kode untuk DITAMBAHKAN.
#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream { /* ... */ }
// #[route(GET, "/")]: menerima token atribut itu sendiri dan item-nya, mengembalikan PENGGANTI.
#[proc_macro_attribute]
pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream { /* ... */ }
// sql!(...): menerima semua yang ada di dalam kurung, mengembalikan penggantinya.
#[proc_macro]
pub fn sql(input: TokenStream) -> TokenStream { /* ... */ }

TokenStream mentah hanyalah daftar token, dan itu merepotkan untuk diolah. Karena itu hampir semua procedural macro memakai tiga crate pembantu:

  • syn mengurai (parse) token menjadi pohon sintaks. Alih-alih token, kamu mendapat DeriveInput yang berisi nama tipe, generics-nya, dan field-field-nya.
  • quote mengubah kode yang berbentuk Rust kembali menjadi token. Di dalam quote! { ... } kamu menulis Rust biasa, dan #name menyisipkan isi sebuah variabel.
  • proc-macro2 adalah pembungkus untuk tipe-tipe proc_macro milik compiler, dan inilah yang dipakai syn dan quote. Crate ini juga bisa dipakai di luar compiler, jadi fungsi pembantu dan testing jadi lebih mudah.

Satu hal penting: makro kamu berjalan saat kompilasi. Cargo mengompilasi crate makro lebih dulu, untuk komputermu sendiri, lalu compiler menjalankannya ketika membangun crate yang memakainya. Saat program akhirnya dijalankan, makronya sudah lama tidak ada. Yang tersisa hanya kode yang ditulisnya.

Procedural macro harus tinggal di crate-nya sendiri, dengan tipe crate khusus. Crate itu hanya boleh mengekspor makro, bukan fungsi atau trait biasa. Jadi susunan yang umum adalah dua crate berdampingan, seperti serde dan serde_derive: satu untuk trait, satu untuk derive. Kita akan menambahkan crate ketiga, program kecil bernama shop yang memakai keduanya. (Perlu mengingat lagi soal crate dan workspace? Lihat Cargo dan crate.)

Terminal window
mkdir procmacro-demo && cd procmacro-demo
cargo new --lib hello_macro
cargo new --lib hello_macro_derive
cargo new shop

Ini struktur file akhirnya:

procmacro-demo/
├── Cargo.toml (workspace-nya)
├── hello_macro/
│ ├── Cargo.toml
│ └── src/lib.rs (trait, dan re-export derive-nya)
├── hello_macro_derive/
│ ├── Cargo.toml (proc-macro = true)
│ └── src/lib.rs (makronya sendiri)
└── shop/
├── Cargo.toml
└── src/main.rs (program yang memakai #[derive(...)])

Cargo.toml di folder paling atas menyatukan ketiga crate menjadi satu workspace:

[workspace]
members = ["hello_macro", "hello_macro_derive", "shop"]
resolver = "2"

Crate makro adalah satu-satunya yang punya pengaturan khusus: proc-macro = true. Crate ini juga bergantung pada syn, quote, dan proc-macro2:

hello_macro_derive/Cargo.toml
[package]
name = "hello_macro_derive"
version = "0.1.0"
edition = "2024"
[lib]
proc-macro = true
[dependencies]
proc-macro2 = "1"
quote = "1"
syn = "2"

Dua crate lainnya biasa saja. hello_macro bergantung pada crate derive, dan shop hanya butuh hello_macro:

hello_macro/Cargo.toml
[dependencies]
hello_macro_derive = { version = "0.1.0", path = "../hello_macro_derive" }
shop/Cargo.toml
[dependencies]
hello_macro = { version = "0.1.0", path = "../hello_macro" }

Kita mulai dari yang sangat kecil. Kita ingin setiap tipe yang memakai #[derive(HelloMacro)] mendapat fungsi hello_macro() yang mencetak nama tipenya. Pertama trait-nya, di hello_macro/src/lib.rs. Baris terakhir me-re-export derive macro-nya, jadi pengguna cukup memakai satu crate ini saja. Persis seperti yang dilakukan serde.

hello_macro/src/lib.rs
pub trait HelloMacro {
fn hello_macro();
}
pub trait Describe {
fn type_name() -> &'static str;
fn field_names() -> &'static [&'static str];
}
// Re-export derive macro-nya, jadi pengguna cukup memakai crate ini saja.
pub use hello_macro_derive::{Describe, HelloMacro};

(Describe untuk langkah 2.) Sekarang makronya sendiri, di hello_macro_derive/src/lib.rs:

hello_macro_derive/src/lib.rs
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, Data, DeriveInput, Fields}; // Data dan Fields: langkah 2
#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
// 1. Urai token menjadi pohon sintaks yang bisa kita periksa.
let ast = parse_macro_input!(input as DeriveInput);
let name = &ast.ident;
// 2. Susun kode baru dengan quote!.
let generated = quote! {
impl hello_macro::HelloMacro for #name {
fn hello_macro() {
println!("Hello, Macro! My name is {}!", stringify!(#name));
}
}
};
// 3. Serahkan token baru itu kembali ke compiler.
generated.into()
}

Tiga langkah, dan kamu akan melihatnya di hampir semua derive macro:

  1. parse_macro_input! mengubah token menjadi DeriveInput. ast.ident adalah nama tipenya, misalnya Product. Kalau parsing gagal, makro ini otomatis mengembalikan compile error yang rapi untukmu.
  2. quote! menyusun blok impl. #name menyisipkan namanya. stringify!(#name) mengubah nama itu menjadi teks "Product".
  3. .into() mengubah proc_macro2::TokenStream milik quote menjadi proc_macro::TokenStream milik compiler.

Perhatikan path lengkap hello_macro::HelloMacro. Kode yang dihasilkan ditempel ke file milik pengguna, jadi kode itu tidak bisa mengandalkan baris use milik pengguna.

Ketika seseorang menulis #[derive(HelloMacro)] struct Product, compiler menambahkan kode yang bekerja sama persis seperti versi tulisan tangan di bawah ini. Yang ini bisa dijalankan di Playground, karena tidak ada crate makro yang terlibat (teksnya sengaja dibiarkan dalam bahasa Inggris supaya sama dengan proyek contoh):

trait HelloMacro {
fn hello_macro();
}
struct Product;
// Inilah yang ditulis #[derive(HelloMacro)] untukmu:
impl HelloMacro for Product {
fn hello_macro() {
println!("Hello, Macro! My name is {}!", "Product");
}
}
fn main() {
Product::hello_macro();
}

Output:

Hello, Macro! My name is Product!
▶ Coba sendiri

Menulis ini dengan tangan sekali saja tidak masalah. Menulisnya untuk 40 tipe, nah, di situlah derive mulai terasa manfaatnya.

Langkah 2: #[derive(Describe)] membaca field

Section titled “Langkah 2: #[derive(Describe)] membaca field”

HelloMacro hanya memakai nama tipe. Derive sungguhan, seperti milik serde, melihat field-nya. Ayo buat Describe: derive ini mendaftar nama-nama field sebuah struct, berguna untuk header file CSV. syn memberimu field-field itu lewat ast.data:

  • Data::Struct, Data::Enum, atau Data::Union: jenis tipenya.
  • Untuk struct, Fields::Named (struct Product { id: u32 }), Fields::Unnamed (struct Meters(f64)), atau Fields::Unit (struct Marker;).

Tambahkan ini di bawah makro pertama di hello_macro_derive/src/lib.rs:

#[proc_macro_derive(Describe)]
pub fn describe_derive(input: TokenStream) -> TokenStream {
let ast = parse_macro_input!(input as DeriveInput);
expand_describe(&ast)
.unwrap_or_else(|err| err.to_compile_error())
.into()
}
fn expand_describe(ast: &DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
let name = &ast.ident;
let Data::Struct(data) = &ast.data else {
return Err(syn::Error::new_spanned(
name,
"Describe can only be derived for structs",
));
};
let field_names: Vec<String> = match &data.fields {
Fields::Named(fields) => fields
.named
.iter()
.map(|field| field.ident.as_ref().unwrap().to_string())
.collect(),
Fields::Unit => Vec::new(),
Fields::Unnamed(_) => {
return Err(syn::Error::new_spanned(
name,
"Describe needs named fields, like `struct Point { x: i32 }`",
));
}
};
Ok(quote! {
impl hello_macro::Describe for #name {
fn type_name() -> &'static str {
stringify!(#name)
}
fn field_names() -> &'static [&'static str] {
&[#(#field_names),*]
}
}
})
}

Bagian-bagian barunya:

  • Kerja utamanya terjadi di fungsi pembantu biasa yang mengembalikan syn::Result. Dengan begitu kamu bisa memakai ? dan return Err(...) seperti di kode penanganan error lainnya. Fungsi makro di bagian atas hanya mengubah error menjadi compile error.
  • let ... else langsung menangani kasus “bukan struct” dan membuat jalur normalnya tetap rata, tanpa banyak indentasi.
  • #(#field_names),* adalah pengulangan (repetition) milik quote. Ia menyisipkan setiap string di dalam Vec, dipisahkan koma. Idenya sama dengan $(...),* di macro_rules!. Setiap String menjadi literal string, jadi hasilnya &["id", "name", "price_cents"].
  • field.ident.as_ref().unwrap() aman di sini: di Fields::Named, setiap field pasti punya nama.

Sekarang pakai kedua derive itu di shop/src/main.rs. Perhatikan bahwa use hello_macro::{Describe, HelloMacro} membawa masuk trait-nya sekaligus derive macro-nya. Namanya sama, tapi keduanya tinggal di namespace yang berbeda, persis seperti cara kerja serde::Serialize.

shop/src/main.rs
use hello_macro::{Describe, HelloMacro};
#[derive(HelloMacro, Describe)]
struct Product {
id: u32,
name: String,
price_cents: u64,
}
#[derive(HelloMacro, Describe)]
struct Customer {
email: String,
city: String,
}
fn main() {
Product::hello_macro();
Customer::hello_macro();
println!("{} has fields {:?}", Product::type_name(), Product::field_names());
// File CSV: header-nya dari derive, baris-barisnya dari nilai sungguhan.
let ana = Customer { email: "[email protected]".to_string(), city: "Lisbon".to_string() };
println!("{}", Customer::field_names().join(","));
println!("{},{}", ana.email, ana.city);
let teapot = Product { id: 7, name: "Teapot".to_string(), price_cents: 1999 };
println!("{} #{}: {} costs {} cents", Product::type_name(), teapot.id, teapot.name, teapot.price_cents);
}

Jalankan dari folder workspace:

Terminal window
cargo run -p shop

Output (pada build pertama dari nol):

Compiling proc-macro2 v1.0.107
Compiling unicode-ident v1.0.27
Compiling quote v1.0.47
Compiling syn v2.0.119
Compiling hello_macro_derive v0.1.0 (/home/you/procmacro-demo/hello_macro_derive)
Compiling hello_macro v0.1.0 (/home/you/procmacro-demo/hello_macro)
Compiling shop v0.1.0 (/home/you/procmacro-demo/shop)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 2.45s
Running `target/debug/shop`
Hello, Macro! My name is Product!
Hello, Macro! My name is Customer!
Product has fields ["id", "name", "price_cents"]
email,city
Product #7: Teapot costs 1999 cents

Baris Compiling, Finished, dan Running adalah pesan dari Cargo sendiri, dan selalu dalam bahasa Inggris. Lihat urutannya: Cargo membangun syn dan hello_macro_derive sebelum shop, karena compiler perlu menjalankan makro kamu saat mengompilasi shop. Kalau kamu menambahkan field ke Customer, header CSV-nya ikut berubah sendiri pada build berikutnya.

Apa yang terjadi kalau seseorang memasang #[derive(Describe)] di sebuah enum? Karena expand_describe mengembalikan syn::Error, pengguna mendapat compiler error yang terlihat normal. (Contoh-contoh yang rusak di halaman ini kami taruh di shop/src/bin/broken.rs, itulah sebabnya path itu muncul.)

use hello_macro::Describe;
#[derive(Describe)]
enum OrderStatus {
Pending,
Shipped,
}
error: Describe can only be derived for structs
--> shop/src/bin/broken.rs:4:6
|
4 | enum OrderStatus {
| ^^^^^^^^^^^

Pesannya adalah teks yang kita tulis sendiri di makro (“Describe hanya bisa di-derive untuk struct”). Ada dua hal yang membuat error ini bagus:

  • syn::Error::new_spanned(name, ...) mengingat di mana nama itu berada di file pengguna (disebut span, yaitu posisinya di kode). Karena itulah ^^^^^ menunjuk ke OrderStatus, bukan ke suatu baris di dalam makro kamu.
  • to_compile_error() mengubah error itu menjadi pemanggilan compile_error!("..."). Compiler lalu melaporkannya seperti error lainnya. Kamu juga bisa memakai makro compile_error! yang sama di kode biasa.

Tuple struct mendapat pesan yang satunya lagi (“Describe butuh field yang punya nama”), dan juga menunjuk ke tempat yang tepat:

error: Describe needs named fields, like `struct Point { x: i32 }`
--> shop/src/bin/broken.rs:4:8
|
4 | struct Meters(f64);
| ^^^^^^

Tulis pesanmu untuk orang yang memakai makro itu. Katakan apa yang salah dan apa yang seharusnya.

Melihat kode yang dihasilkan dengan cargo expand

Section titled “Melihat kode yang dihasilkan dengan cargo expand”

Makro bisa terasa seperti sulap, dan sulap itu sulit di-debug. cargo-expand menunjukkan kode setelah semua makro selesai berjalan. Instal sekali saja (lihat Tools dan setup Rust untuk tools berguna lainnya):

Terminal window
cargo install cargo-expand
cargo expand -p shop

Sebagian dari yang dicetaknya untuk proyek kita:

impl hello_macro::Describe for Product {
fn type_name() -> &'static str {
"Product"
}
fn field_names() -> &'static [&'static str] {
&["id", "name", "price_cents"]
}
}
impl hello_macro::Describe for Customer {
fn type_name() -> &'static str {
"Customer"
}
fn field_names() -> &'static [&'static str] {
&["email", "city"]
}
}

Itu dia blok impl kamu, dengan nama-nama field sudah terisi dan stringify!(#name) sudah berubah menjadi "Product". Kalau sebuah derive tidak bekerja seperti yang kamu harapkan, jalankan cargo expand dulu. Tool ini juga mengembangkan println! dan makro lainnya, jadi carilah blok impl milikmu sendiri.

Crate makro tidak mudah menguji dirinya sendiri, karena ia hanya boleh mengekspor makro. Solusi sederhananya: tulis test biasa di crate yang memakai makro itu. Taruh ini di hello_macro/tests/describe.rs lalu jalankan cargo test -p hello_macro:

use hello_macro::Describe;
#[allow(dead_code)]
#[derive(Describe)]
struct Product {
id: u32,
name: String,
}
#[test]
fn lists_field_names_in_order() {
assert_eq!(Product::type_name(), "Product");
assert_eq!(Product::field_names(), ["id", "name"]);
}
running 1 test
test lists_field_names_in_order ... ok

Tapi bagaimana cara menguji bahwa kasus enum menghasilkan error yang bagus? Test biasa tidak bisa, karena file test-nya sendiri tidak akan lolos kompilasi. Untuk itulah crate trybuild ada. Kamu menaruh file-file kecil yang “harus gagal” di sebuah folder, lalu trybuild mengompilasi masing-masing dan membandingkan error dari compiler dengan file .stderr yang sudah disimpan:

#[test]
fn ui() {
let t = trybuild::TestCases::new();
t.compile_fail("tests/ui/*.rs");
}

Sebagian besar crate makro populer, termasuk serde dan thiserror, menguji pesan error mereka dengan cara ini.

Procedural macro itu ampuh, tapi ada harganya. Jujurlah pada dirimu sendiri sebelum membuatnya:

  • Waktu kompilasi. syn adalah crate yang besar. Build pertama setiap proyek yang memakai makro kamu harus mengompilasinya. (Fitur bawaan syn sudah cukup untuk derive. Kamu baru butuh features = ["full"] untuk mengurai hal seperti isi fungsi, dan itu membuatnya lebih lambat.)
  • Kerumitan. Sekarang kamu punya crate tambahan, kode yang menulis kode, dan error yang lebih sulit diikuti.
  • Tooling. Editor dan rust-analyzer lebih memahami kode biasa daripada kode yang berasal dari makro.

Urutan yang baik untuk dicoba:

  1. Sebuah fungsi, trait dengan method bawaan (default), atau generics. Kebanyakan masalah “kode berulang” selesai di sini.
  2. Makro macro_rules! (lihat Makro). Tidak butuh crate tambahan.
  3. Procedural macro, ketika kodenya bergantung pada struktur sebuah tipe (nama, field, atau variannya), banyak tipe yang membutuhkannya, dan orang lain akan memakainya. Itulah alasan serde, thiserror, dan clap memakai derive.

Sebagai perbandingan, ini Describe yang dibuat dengan macro_rules!. Ia berfungsi dan tidak butuh crate tambahan, tapi struct-nya harus ditulis di dalam pemanggilan makro. Itu cepat jadi canggung, misalnya kalau kamu juga ingin doc comment, field pub, atau derive lain.

trait Describe {
fn type_name() -> &'static str;
fn field_names() -> &'static [&'static str];
}
macro_rules! describe_struct {
(struct $name:ident { $($field:ident : $ty:ty),* $(,)? }) => {
struct $name {
$($field: $ty),*
}
impl Describe for $name {
fn type_name() -> &'static str {
stringify!($name)
}
fn field_names() -> &'static [&'static str] {
&[$(stringify!($field)),*]
}
}
};
}
describe_struct!(struct Customer { email: String, city: String });
fn main() {
let ana = Customer { email: "[email protected]".to_string(), city: "Lisbon".to_string() };
println!("{} has fields {:?}", Customer::type_name(), Customer::field_names());
println!("{},{}", ana.email, ana.city);
}

Output:

Customer has fields ["email", "city"]
▶ Coba sendiri

Untuk satu atau dua tipe di programmu sendiri, cara ini (atau sekadar menulis impl dengan tangan) sering jadi pilihan yang lebih baik. Derive lebih nyaman bagi orang yang memakainya: mereka tetap menulis struct biasa dan cukup menambah satu baris.

Lupa proc-macro = true. Tanpa bagian [lib] di Cargo.toml crate makro, crate itu hanyalah library biasa, dan compiler menolaknya:

error: the `#[proc_macro_derive]` attribute is only usable with crates of the `proc-macro` crate type
--> hello_macro_derive/src/lib.rs:5:1
|
5 | #[proc_macro_derive(HelloMacro)]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
error[E0432]: unresolved import `proc_macro`

Artinya: atribut #[proc_macro_derive] hanya bisa dipakai di crate bertipe proc-macro, dan karena itu crate proc_macro juga tidak ditemukan.

Memakai panic! atau unwrap() untuk kesalahan pengguna. Memang “jalan”, tapi error-nya kabur dan menunjuk ke seluruh #[derive], bukan ke sumber masalahnya:

error: proc-macro derive panicked
--> shop/src/bin/broken.rs:3:10
|
3 | #[derive(Describe)]
| ^^^^^^^^
|
= help: message: Describe can only be derived for structs

Di sini compiler hanya bilang bahwa derive macro-nya panic, lalu menampilkan pesannya sebagai bantuan. Lebih baik kembalikan syn::Error dengan new_spanned, seperti di langkah 2.

Path pendek di kode yang dihasilkan. Kalau kamu menulis impl Describe for #name dan bukan impl hello_macro::Describe for #name, kode itu hanya jalan kalau pengguna kebetulan sudah meng-import Describe. Orang yang menulis #[derive(hello_macro::Describe)] akan mendapat ini:

error[E0405]: cannot find trait `Describe` in this scope
--> shop/src/bin/broken.rs:1:10
|
1 | #[derive(hello_macro::Describe)]
| ^^^^^^^^^^^^^^^^^^^^^ not found in this scope
|
= note: this error originates in the derive macro `hello_macro::Describe`

Artinya: trait Describe tidak ditemukan di cakupan (scope) itu. Selalu pakai path lengkap di dalam quote!: hello_macro::Describe, ::std::fmt::Display, ::core::option::Option.

Latihan ini mengubah proyek yang sungguhan, jadi tidak ada tombol “Coba sendiri”. Buat proyek dari halaman ini di komputermu sendiri lalu jalankan cargo run -p shop.

Tambahkan method ketiga, fn field_count() -> usize, ke trait Describe dan ke derive-nya. Product::field_count() harus mengembalikan 3. Petunjuk: kamu sudah punya vektor field_names. Simpan field_names.len() ke sebuah variabel sebelum quote!, lalu sisipkan dengan #.

Lihat solusi

Di hello_macro/src/lib.rs, tambahkan method-nya ke trait:

pub trait Describe {
fn type_name() -> &'static str;
fn field_names() -> &'static [&'static str];
fn field_count() -> usize;
}

Di expand_describe, hitung field-nya dan tambahkan method itu ke impl yang dihasilkan:

let count = field_names.len();
Ok(quote! {
impl hello_macro::Describe for #name {
fn type_name() -> &'static str {
stringify!(#name)
}
fn field_names() -> &'static [&'static str] {
&[#(#field_names),*]
}
fn field_count() -> usize {
#count
}
}
})

Lalu println!("{} has {} fields", Product::type_name(), Product::field_count()); di main mencetak:

Product has 3 fields

Penghitungannya terjadi di makro kamu, saat kompilasi. cargo expand menunjukkan hasilnya hanya fn field_count() -> usize { 3usize }, sebuah konstanta tanpa sisa pekerjaan saat program berjalan.

Coba pasang #[derive(Describe)] di struct generik seperti struct Page<T> { items: Vec<T>, number: u32 }. Hasilnya gagal dengan error[E0107]: missing generics for struct Page (generics-nya hilang), karena kode yang dihasilkan berbunyi impl hello_macro::Describe for Page dan melupakan <T>. Perbaiki derive-nya supaya struct generik bisa dipakai. Petunjuk: ast.generics.split_for_impl() memberimu tiga bagian untuk disisipkan di baris impl. Lihat Generics untuk mengingat lagi bentuk impl<T>.

Lihat solusi

Di expand_describe, pecah generics-nya dan pakai ketiga bagiannya di baris impl:

let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl();
Ok(quote! {
impl #impl_generics hello_macro::Describe for #name #ty_generics #where_clause {
fn type_name() -> &'static str {
stringify!(#name)
}
fn field_names() -> &'static [&'static str] {
&[#(#field_names),*]
}
}
})

Sekarang ini berfungsi di shop:

#[derive(Describe)]
struct Page<T> {
items: Vec<T>,
number: u32,
}
fn main() {
let page = Page { items: vec!["a", "b"], number: 1 };
println!("{} {} {:?}", page.items.len(), page.number, Page::<String>::field_names());
}
2 1 ["items", "number"]

impl_generics adalah <T> (beserta bound-nya, kalau ada), ty_generics adalah <T>, dan where_clause adalah bagian where ..., kalau ada. Setiap derive yang serius melakukan ini, jadi ingat-ingat split_for_impl.

  • Procedural macro adalah fungsi Rust yang berjalan saat kompilasi: TokenStream masuk, TokenStream keluar.
  • Ada tiga jenis: derive (#[derive(Serialize)]), attribute (#[tokio::main]), dan function-like (sql!(...)).
  • Makro ini tinggal di crate-nya sendiri dengan proc-macro = true. Biasanya sebuah crate utama me-re-export-nya di samping trait-nya.
  • syn mengurai input (DeriveInput, Data, Fields), dan quote menulis output-nya (#name, #(...),*).
  • Laporkan kesalahan pengguna dengan syn::Error::new_spanned(...).to_compile_error(), periksa hasilnya dengan cargo expand, dan uji pesan error dengan trybuild.
  • Coba dulu fungsi, trait, atau macro_rules!. Procedural macro layak dibuat ketika banyak tipe butuh kode yang didasarkan pada strukturnya.

Kuis singkat

1. Kapan kode sebuah procedural macro berjalan?

2. #[tokio::main] termasuk jenis procedural macro yang mana?

3. Apa yang dilakukan syn dan quote?

4. Derive kamu harus menolak enum. Apa cara terbaiknya?

Skor: 0 / 4

  • Procedural Macros — The Rust Book (bahasa Inggris): panduan resmi membuat derive HelloMacro, ditambah attribute-like dan function-like macro.
  • Procedural Macros — The Rust Reference (bahasa Inggris): aturan pasti untuk ketiga jenisnya, tipe crate, span, dan hygiene.
  • Dokumentasi syn (bahasa Inggris): DeriveInput, Data, Fields, Error, dan fungsi-fungsi parsing yang dipakai di halaman ini.
  • Dokumentasi quote (bahasa Inggris): cara kerja interpolasi #var dan pengulangan #(...),* di dalam quote!.
  • proc-macro-workshop (bahasa Inggris): proyek latihan gratis dari David Tolnay (derive builder, derive debug, dan lainnya), lengkap dengan test yang memandumu langkah demi langkah.