macro_rules!
到 Derive Macro在深入 Zenoh 的如何應用 derive macro 之前,先來複習一下 Rust 的 macro 系統。Rust 的 macro 不只是「尋找與替換」,它可以讓你 自動生成程式碼、減少樣板程式碼,甚至擴充語言功能。
本次文章講涵蓋:
macro_rules!
– Rust 內建的宣告式 macro 系統。proc_macro
– 支援 #[derive(...)]
的 proc_macro
系統。macro_rules!
:宣告式 MacroRust 的第一個 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 實作。而且通常較難除錯與維護。
proc_macro
如果需要更多功能,Rust 提供 proc_macro
:在編譯時執行函數,生成 Rust 程式碼。
proc_macro
類型:
#[derive]
:生成 trait 實作。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"
修改 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()
}
建立一個 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" } # 本地路徑依賴
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
}
cargo run -p hello_app
輸出:
Hello, I am Robot
Hello, I am Drone
這裡完整地展示了 proc_macro
流程:
proc-macro
crate 定義 macro。#[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 |
易用性 | 較簡單、輕量 | 較複雜,但功能強大 |
最後我們來看看 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 worker
、rx = 2 workers
)直接嵌入 enum。ZENOH_RUNTIME='(rx: (worker_threads: 4))'
在我們具備了關於Rust macro的基礎知識後,下一篇文章,我們將深入 Zenoh 的ZRuntime
,敬請期待!