本文 AI 產出,尚未審核

Rust 宏與進階功能 — 屬性宏(Attribute Macros)

簡介

在 Rust 的編譯階段,屬性宏(attribute macros)提供了一種強大的方式,讓開發者可以在程式碼上貼上自訂的屬性(#[...]),讓編譯器在展開(expand)時自動產生或改寫程式碼。與傳統的函式宏(macro_rules!)不同,屬性宏的作用範圍更廣,能夠直接操作整個語法樹(AST),因此在 程式碼生成、重構與跨 crate 的 API 設計 上扮演關鍵角色。

本單元將帶你從概念到實作,逐步了解屬性宏的工作原理、常見使用方式,以及在真實專案中如何安全、有效地運用它們。


核心概念

1. 什麼是屬性宏?

屬性宏是一種 proc‑macro(程序宏),以 #[proc_macro_attribute] 標記的函式實作。它接受兩個參數:

  1. 屬性參數(attribute arguments):位於方括號內的內容,例如 #[my_macro(foo = "bar")] 中的 foo = "bar"
  2. 被標記的項目(item token stream):整個被套用的程式碼片段,例如一個 structenumfn 等。

宏函式會接收這兩段 TokenStream,進行分析、修改或重新產生,最後回傳新的 TokenStream 給編譯器。

#[proc_macro_attribute]
pub fn my_macro(attr: TokenStream, item: TokenStream) -> TokenStream {
    // 1. 解析 attr
    // 2. 解析 item(可能是 struct、fn …)
    // 3. 產生新的程式碼
    // 4. 回傳 TokenStream
}

2. 為什麼需要屬性宏?

  • 自動產生樣板程式碼:例如 #[derive(Debug)] 會自動為 struct 實作 Debug
  • 跨模組/跨 crate 的語意擴充:在第三方 crate 中加入自訂屬性,讓使用者只需要貼標籤即可得到完整功能。
  • 編譯期驗證:可以在編譯階段檢查程式碼是否符合特定規則,減少 runtime 錯誤。

3. 建立屬性宏的基本步驟

步驟 說明
1. 新增 proc‑macro crate cargo new my_macros --lib,在 Cargo.toml 加入 proc-macro = true
2. 引入必要套件 常用 syn(語法解析)與 quote(產生程式碼)兩個 crate。
3. 實作宏函式 使用 #[proc_macro_attribute] 標記,並以 TokenStream 為參數與回傳值。
4. 發布或在同一工作區使用 在主 crate 的 Cargo.toml 加入 my_macros = { path = "../my_macros" }
5. 在程式碼中使用 use my_macros::my_macro; 然後 #[my_macro] 套用。

程式碼範例

以下提供 五個實用範例,從最簡單的「自動加上 Debug」到較進階的「產生 REST API 路由」示範。

範例 1:自動為 struct 加上 Debug(簡化版 derive(Debug)

// my_macros/src/lib.rs
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};

#[proc_macro_attribute]
pub fn auto_debug(_attr: TokenStream, item: TokenStream) -> TokenStream {
    // 解析被標記的 struct
    let input = parse_macro_input!(item as DeriveInput);
    let name = &input.ident;

    // 產生 impl Debug
    let expanded = quote! {
        #input

        impl std::fmt::Debug for #name {
            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
                write!(f, concat!(stringify!(#name), " {{ .. }}"))
            }
        }
    };
    TokenStream::from(expanded)
}
// main.rs
use my_macros::auto_debug;

#[auto_debug]
struct User {
    id: u32,
    name: String,
}

fn main() {
    let u = User { id: 1, name: "Alice".into() };
    println!("{:?}", u); // 會印出 "User { .. }"
}

重點:只要在 struct 前加上 #[auto_debug],即可自動得到 Debug 實作,免除手動撰寫樣板。


範例 2:為函式加入執行時間測量

// my_macros/src/lib.rs
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, ItemFn};

#[proc_macro_attribute]
pub fn time_it(_attr: TokenStream, item: TokenStream) -> TokenStream {
    let input = parse_macro_input!(item as ItemFn);
    let fn_name = &input.sig.ident;
    let block = &input.block;
    let vis = &input.vis;
    let attrs = &input.attrs;
    let sig = &input.sig;

    let expanded = quote! {
        #(#attrs)*
        #vis #sig {
            let _start = std::time::Instant::now();
            let __result = (|| #block)();
            let _elapsed = _start.elapsed();
            println!("⚡️ 函式 `{}` 執行時間: {:?}", stringify!(#fn_name), _elapsed);
            __result
        }
    };
    TokenStream::from(expanded)
}
// main.rs
use my_macros::time_it;

#[time_it]
fn heavy_computation(n: u64) -> u64 {
    (0..n).fold(0, |acc, x| acc + x)
}

fn main() {
    heavy_computation(1_000_000);
}

說明:宏會把原本的函式內容包在閉包中,先記錄開始時間,最後印出執行時長。開發者只需要貼上 #[time_it] 即可取得 profiling 功能。


範例 3:自訂屬性參數 – 產生帶預設值的建構子

// my_macros/src/lib.rs
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, AttributeArgs, DeriveInput, Lit, Meta, NestedMeta};

#[proc_macro_attribute]
pub fn builder(attr: TokenStream, item: TokenStream) -> TokenStream {
    // 解析屬性參數,例如 #[builder(default = "42")]
    let args = parse_macro_input!(attr as AttributeArgs);
    let mut default_val = quote! { Default::default() };

    for arg in args {
        if let NestedMeta::Meta(Meta::NameValue(kv)) = arg {
            if kv.path.is_ident("default") {
                if let Lit::Str(lit_str) = kv.lit {
                    let expr: syn::Expr = syn::parse_str(&lit_str.value()).unwrap();
                    default_val = quote! { #expr };
                }
            }
        }
    }

    // 解析 struct
    let input = parse_macro_input!(item as DeriveInput);
    let name = &input.ident;
    let builder_name = syn::Ident::new(&format!("{}Builder", name), name.span());

    // 產生 builder struct 與 impl
    let expanded = quote! {
        #input

        pub struct #builder_name {
            // 所有欄位皆使用 Option 包起來
            // 這裡簡化,只示範單一欄位
            value: Option<i32>,
        }

        impl #builder_name {
            pub fn new() -> Self {
                Self { value: None }
            }

            pub fn value(mut self, v: i32) -> Self {
                self.value = Some(v);
                self
            }

            pub fn build(self) -> #name {
                #name {
                    value: self.value.unwrap_or_else(|| #default_val),
                }
            }
        }
    };
    TokenStream::from(expanded)
}
// main.rs
use my_macros::builder;

#[builder(default = "100")]
struct Config {
    value: i32,
}

fn main() {
    let cfg = ConfigBuilder::new().build(); // value 會是 100
    println!("config.value = {}", cfg.value);
}

技巧:屬性宏可以接受自訂參數,讓使用者在使用時自行指定行為(如預設值、路徑等),提升彈性。


範例 4:自動產生 REST API 路由(結合 actix-web

// my_macros/src/lib.rs
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, AttributeArgs, ItemFn, Lit, Meta, NestedMeta};

#[proc_macro_attribute]
pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream {
    // 解析屬性參數,例如 #[route(GET, "/users")]
    let args = parse_macro_input!(attr as AttributeArgs);
    let mut method = quote! { get };
    let mut path = quote! { "/" };

    if let [NestedMeta::Meta(Meta::Path(m)), NestedMeta::Lit(Lit::Str(lit))] = &args[..] {
        let method_str = m.segments.last().unwrap().ident.to_string().to_lowercase();
        method = syn::Ident::new(&method_str, m.span()).into_token_stream();
        path = lit.value().into_token_stream();
    }

    let input = parse_macro_input!(item as ItemFn);
    let fn_name = &input.sig.ident;
    let vis = &input.vis;
    let block = &input.block;
    let attrs = &input.attrs;
    let sig = &input.sig;

    let expanded = quote! {
        #(#attrs)*
        #vis #sig

        // 產生 actix-web 的路由註冊
        pub fn register(cfg: &mut actix_web::web::ServiceConfig) {
            cfg.route(#path, actix_web::web::#method().to(#fn_name));
        }
    };
    TokenStream::from(expanded)
}
// main.rs
use actix_web::{App, HttpServer};
use my_macros::route;

#[route(GET, "/hello")]
async fn hello() -> &'static str {
    "Hello, world!"
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new().configure(|cfg| {
            // 只要呼叫 register,即可自動掛上路由
            register(cfg);
        })
    })
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}

實務價值:開發者只要在函式上貼上 #[route(...)],就能自動完成路由註冊,減少重複的 cfg.route(...).to(...) 程式碼。


範例 5:編譯期檢查欄位命名規則(避免使用保留字)

// my_macros/src/lib.rs
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput, Error};

#[proc_macro_attribute]
pub fn forbid_reserved(_attr: TokenStream, item: TokenStream) -> TokenStream {
    let input = parse_macro_input!(item as DeriveInput);
    let name = &input.ident;

    // 只檢查 struct 欄位
    if let syn::Data::Struct(data) = &input.data {
        for field in &data.fields {
            if let Some(ident) = &field.ident {
                let field_name = ident.to_string();
                if ["type", "match", "async"].contains(&field_name.as_str()) {
                    return Error::new_spanned(
                        ident,
                        format!("欄位名稱 `{}` 為 Rust 保留字,請改用其他名稱", field_name)
                    )
                    .to_compile_error()
                    .into();
                }
            }
        }
    }

    // 若無問題,直接回傳原始程式碼
    TokenStream::from(quote! { #input })
}
// main.rs
use my_macros::forbid_reserved;

#[forbid_reserved]
struct Bad {
    // 編譯時會產生錯誤:欄位名稱 `type` 為保留字
    // type: i32,
    value: i32,
}

優點:透過屬性宏在編譯期即捕捉錯誤,提升 API 的安全性與可讀性。


常見陷阱與最佳實踐

陷阱 說明 解決方案
TokenStream 直接拼接 直接使用 format! 產生程式碼會失去語法資訊,容易產生錯誤。 使用 quote!syn 解析 AST,確保產生的程式碼符合語法。
遺失原始屬性 宏在產生新程式碼時,常會把原本的屬性(如 #[derive])遺失。 quote! 中使用 #(#attrs)* 重新插入所有原始屬性。
過度抽象 把太多邏輯塞進宏,會讓錯誤訊息變得難以理解。 保持宏的職責單一,複雜邏輯可放在普通函式或 trait 中。
相依性衝突 多個 crate 同時提供相似的屬性宏,可能產生名稱衝突。 為宏加上命名空間(例如 mycrate::my_macro),或使用 cargo rename
編譯時間激增 大量使用宏會讓編譯器在解析 AST 時變慢。 僅在需要重複生成樣板的地方使用,避免在每個小模組都大量使用。

最佳實踐

  1. 先寫測試:在 tests/ 中加入 trybuild 測試,確保宏在各種情況下都能正確展開。
  2. 提供清晰的錯誤訊息:使用 syn::Error::new_spanned 產生定位精準的編譯錯誤,提升使用者體驗。
  3. 保持文件同步:宏的行為往往隱蔽,務必在 README 或 crate 文檔中說明屬性參數、限制與範例。
  4. 避免在宏內使用 unsafe:若必須使用,務必在文件中說明安全前提,避免不必要的 UB 風險。
  5. 利用 proc-macro-error:此 crate 能讓宏在錯誤時不中斷編譯,並提供多個錯誤訊息,對開發者非常友好。

實際應用場景

場景 為什麼適合使用屬性宏
資料庫模型自動實作 CRUD 只要在 struct 上加 #[orm(model)],宏即可產生 insert, update, delete 等方法,減少手寫重複程式。
Web 框架的路由註冊 如上範例所示,使用 #[route(...)] 可把路由資訊與處理函式緊密結合,提升可維護性。
跨平台 FFI 介面 透過 #[ffi_export] 自動產生 extern "C" 包裝與安全檢查,降低手寫錯誤。
測試生成器 #[auto_test] 可根據函式簽名自動產生單元測試骨架,幫助測試驅動開發(TDD)。
編譯期配置驗證 #[config(validate)] 能在編譯時檢查設定檔格式與必填欄位,避免程式在執行時崩潰。

總結

屬性宏是 Rust 進階開發者不可或缺的工具,它讓我們能在編譯期自動產生、改寫或驗證程式碼,從而大幅提升開發效率與程式品質。本文從概念、建立步驟、實作範例、常見陷阱與最佳實踐,最後列出多種實務應用,提供了一條從 入門 → 熟練 → 專業 的完整學習路徑。

掌握屬性宏的關鍵

  • 熟悉 proc_macro, syn, quote 三大核心 crate;
  • 清晰的錯誤訊息完整的文件 為目標;
  • 僅在必要時使用,避免過度抽象造成維護困難。

透過本文的範例與建議,你現在已具備在自己的專案中 安全、有效地使用屬性宏 的能力,快把它套用在實際的程式碼裡,讓 Rust 的表達力更上一層樓吧! 🚀