iT邦幫忙

2025 iThome 鐵人賽

DAY 10
0
Rust

30 天玩轉 Zenoh:Rust 助力物聯網、機器人與自駕的高速通訊系列 第 10

Day 10: Rust Macro 熱身:從 `macro_rules!` 到 Derive Macro

  • 分享至 

  • xImage
  •  

Rust Macro 熱身:從 macro_rules! 到 Derive Macro

在深入 Zenoh 的如何應用 derive macro 之前,先來複習一下 Rust 的 macro 系統。Rust 的 macro 不只是「尋找與替換」,它可以讓你 自動生成程式碼、減少樣板程式碼,甚至擴充語言功能。

本次文章講涵蓋:

  1. macro_rules! – Rust 內建的宣告式 macro 系統。
  2. proc_macro – 支援 #[derive(...)]proc_macro 系統。
  3. 預覽Zenoh 透過derive macro 來幫助配置 Zenoh 的執行緒。

1. macro_rules!:宣告式 Macro

Rust 的第一個 macro 系統是 macro_rules!。可以把它想成 程式碼的模式匹配。你描述輸入模式,編譯器會將其轉換為 Rust 程式碼。

範例:自訂 vec!

標準庫的 vec! macro 可以輕鬆建立向量:

let nums = vec![1, 2, 3];

這是一個簡化版的自訂實作:

// 定義一個名為 `my_vec` 的宣告式 macro
macro_rules! my_vec {
    // 模式:接受逗號分隔的表達式列表 (`$x:expr`)
    // `*` 表示「零個或多個」重複
    ( $( $x:expr ),* ) => {
        {
            // 建立一個新的空向量
            let mut v = Vec::new();

            // 對於每個傳入的表達式 `$x`,
            // 重複執行這個區塊:將它推入向量
            $(
                v.push($x);
            )*

            // 回傳建立好的向量
            v
        }
    };
}

fn main() {
    // 使用 macro 建立包含元素 10、20、30 的向量
    let v = my_vec![10, 20, 30];

    // 印出向量:[10, 20, 30]
    println!("{:?}", v);
}
  • $( ... ),* 意思是「對每個逗號分隔的表達式重複這個模式」。
  • $()* 的重複會展開成一系列 v.push(...) 語句。

宣告式 macro 非常適合 小型、可重用的語法糖,但有限制:無法檢查型別,也不能生成 trait 實作。而且通常較難除錯與維護。


2. proc_macro

如果需要更多功能,Rust 提供 proc_macro:在編譯時執行函數,生成 Rust 程式碼。

proc_macro 類型:

  • 自訂 #[derive]:生成 trait 實作。
  • 屬性 macro (Attribute macros):為項目附加額外行為。
  • 函數樣式 macro:像函數呼叫一樣使用 macro。

步驟 1:建立新的 macro crate

proc_macro 必須 放在 proc-macro 類型的 crate 中:

cargo new hello_derive --lib

編輯 Cargo.toml

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

[lib]
proc-macro = true

[dependencies]
syn = "2"
quote = "1"

步驟 2:實作 macro

修改 src/lib.rs

// 匯入所需的 crate
use proc_macro::TokenStream; // 編譯器將 macro 輸入提供為 TokenStream
use quote::quote;            // 將 Rust 語法樹轉換成程式碼
use syn;                     // 將 Rust 程式碼解析成語法樹

// 定義 proc_macro:#[derive(Hello)]
#[proc_macro_derive(Hello)]
pub fn hello_derive(input: TokenStream) -> TokenStream {
    // 將輸入的 TokenStream(帶 #[derive(Hello)] 的程式碼)
    // 解析成語法樹 (DeriveInput)
    let ast = syn::parse(input).unwrap();

    // 為指定型別生成實作
    impl_hello(&ast)
}

// 輔助函數,生成 Hello trait 的程式碼
fn impl_hello(ast: &syn::DeriveInput) -> TokenStream {
    // 取得識別子(struct 名稱,例如 Robot 或 Drone)
    let name = &ast.ident;

    // 使用 quote! macro 生成程式碼
    let gen = quote! {
        impl Hello for #name {
            fn hello() {
                // stringify!(#name) 將 struct 名稱轉成字串
                println!("Hello, I am {}", stringify!(#name));
            }
        }
    };

    // 將生成的程式碼轉回 TokenStream 交給編譯器
    gen.into()
}

步驟 3:在另一個 crate 使用

建立一個 binary crate:

cargo new hello_app

修改 hello_app/Cargo.toml

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

[dependencies]
hello_derive = { path = "../hello_derive" } # 本地路徑依賴

步驟 4:撰寫 main.rs

use hello_derive::Hello;

// 定義 trait,讓 macro 生成實作
pub trait Hello {
    fn hello();
}

// 套用自訂 derive
#[derive(Hello)]
struct Robot;

#[derive(Hello)]
struct Drone;

fn main() {
    Robot::hello(); // 印出: Hello, I am Robot
    Drone::hello(); // 印出: Hello, I am Drone
}

步驟 5:執行

cargo run -p hello_app

輸出:

Hello, I am Robot
Hello, I am Drone

這裡完整地展示了 proc_macro 流程

  • proc-macro crate 定義 macro。
  • 在另一個 crate 引入。
  • 套用 #[derive(...)] 自動生成程式碼。

proc_macro 可謂是相當好用。氣功能遠超過 macro_rules! 的能力:它 解析 Rust 語法(使用 syn)並生成整個 trait 實作(使用 quote)! 但也很考驗駕駛員的技術就是XD


比較:macro_rules! vs proc_macro

功能 macro_rules! (宣告式) proc_macro
風格 模式匹配 透過函數生成程式碼
能力 只能改寫語法 可以檢查 AST,生成 impl
使用場景 小型 helper (vec!, DSL, 語法糖) #[derive], 設定解析器, 消除樣板程式碼
依賴 內建 需要 syn + quote
易用性 較簡單、輕量 較複雜,但功能強大

3. 預覽:Zenoh 的 Derive Macro 應用

最後我們來看看 Zenoh 如何使用 derive macro 管理內部的執行緒配置。

Zenoh 使用兩個 proc_macro

  • GenericRuntimeParam – 生成解析與預設程式碼。
  • RegisterParam – 將 enum 變體連結到設定。

簡化版範例:

use zenoh_macros::{GenericRuntimeParam, RegisterParam};
use serde::Deserialize;

/// Zenoh 非同步執行引擎的執行緒設定
#[derive(Deserialize, Debug, GenericRuntimeParam)]
#[serde(default)]
pub struct RuntimeParam {
    /// 非同步工作執行緒數量
    pub worker_threads: usize,
    /// 阻塞任務的最大執行緒數量
    pub max_blocking_threads: usize,
}

impl Default for RuntimeParam {
    fn default() -> Self {
        Self {
            worker_threads: 1,
            max_blocking_threads: 50,
        }
    }
}

/// Zenoh 的不同執行緒角色
#[derive(Debug, RegisterParam, Deserialize)]
#[param(RuntimeParam)]
pub enum ZRuntime {
    #[serde(rename = "app")]
    #[param(worker_threads = 1)]
    Application,

    #[serde(rename = "rx")]
    #[param(worker_threads = 2)]
    RX,

    #[serde(rename = "tx")]
    TX,
}
  • 預設值(app = 1 workerrx = 2 workers)直接嵌入 enum。
  • 可從環境變數讀取參數:
ZENOH_RUNTIME='(rx: (worker_threads: 4))'
  • 大幅度減少了重複的程式碼,一切由macro 自動生成。

參考資料


在我們具備了關於Rust macro的基礎知識後,下一篇文章,我們將深入 Zenoh 的ZRuntime,敬請期待!


上一篇
Day 09: 🦀 打造各種網路拓撲的Zenoh 微服務
系列文
30 天玩轉 Zenoh:Rust 助力物聯網、機器人與自駕的高速通訊10
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言