Rust 宏
声明宏 `macro_rules!`、过程宏三类、以及什么时候该写宏。
Key Points声明宏按模式匹配生成代码,过程宏在编译期接收并返回 TokenStream。
两种宏
| Kind | Declared by | Example |
|---|---|---|
| 声明宏 | macro_rules! | vec! println! |
| 派生宏 | #[proc_macro_derive] | #[derive(Debug)] |
| 属性宏 | #[proc_macro_attribute] | #[tokio::main] |
| 函数宏 | #[proc_macro] | sql!("...") |
macro_rules!
Rust
macro_rules! square {
($x:expr) => {
$x * $x
};
}
macro_rules! my_vec {
() => { Vec::new() };
($($x:expr),+ $(,)?) => {
vec![$($x),+]
};
}
fn main() {
println!("{}", square!(5));
let v: Vec<i32> = my_vec![1, 2, 3];
println!("{:?}", v);
let empty: Vec<i32> = my_vec![];
println!("{:?}", empty);
}Output
25
[1, 2, 3]
[]| Fragment | Matches |
|---|---|
expr | 表达式 |
ident | 标识符 |
ty | 类型 |
pat | 模式 |
stmt | 语句 |
block | 代码块 |
tt | 单个 token 树 |
item | 项(fn、struct 等) |
卫生性
Rust
macro_rules! make_var {
() => {
let x = 1; // 宏里的 x 和外面的 x 不是同一个
};
}
fn main() {
let x = 100;
make_var!();
println!("{}", x); // 100,宏没覆盖它
}Output
100This is hygiene: names introduced inside a macro cannot clash with the call site. C's #define lacks it — #define SWAP(a,b) int tmp = a; ... collides with a caller's tmp.
过程宏
Rust
// 在一个单独的 crate 里(proc-macro = true)
use proc_macro::TokenStream;
#[proc_macro_derive(Hello)]
pub fn hello_derive(input: TokenStream) -> TokenStream {
let ast = syn::parse(input).unwrap();
impl_hello(&ast)
}TOML
[lib]
proc-macro = true
[dependencies]
syn = { version = "2", features = ["full"] }
quote = "1"
proc-macro2 = "1"Proc macros need three crates: syn parses TokenStream into an AST, quote generates code from templates, and proc-macro2 gives a testable layer. Hand-building TokenStream is unmaintainable.
常用内置宏
Rust
fn main() {
// 断言
assert!(1 + 1 == 2);
assert_eq!(2 + 2, 4);
assert_ne!(1, 2);
// 格式化
println!("{}", format!("a{}", 1));
// 编译期
println!("{}", env!("CARGO_PKG_NAME"));
println!("{}", file!());
println!("{}", line!());
// 条件编译
#[cfg(debug_assertions)]
println!("debug 构建");
// 提前返回
fn f(x: Option<i32>) -> i32 {
let Some(v) = x else { return 0 };
v
}
println!("{}", f(Some(1)));
// 编译期生成 panic
// compile_error!("这个分支不应该被编译");
}Output
a1
hello
src/main.rs
8
debug 构建
1该不该写宏
- 该写 —— 消除大量重复的样板(
#[derive]类)、DSL、编译期校验 - 不该写 —— 用泛型或 trait 能解决的,别用宏
- 不该写 —— 只在一处用一次的代码
Macro errors are usually poor — they point into expanded code, not your source. Using
$x:expr rather than $x:tt in macro_rules! markedly improves diagnostics.两种宏
| 类型 | 声明方式 | 例子 |
|---|---|---|
| 声明宏 | macro_rules! | vec! println! |
| 派生宏 | #[proc_macro_derive] | #[derive(Debug)] |
| 属性宏 | #[proc_macro_attribute] | #[tokio::main] |
| 函数宏 | #[proc_macro] | sql!("...") |
macro_rules!
Rust
macro_rules! square {
($x:expr) => {
$x * $x
};
}
macro_rules! my_vec {
() => { Vec::new() };
($($x:expr),+ $(,)?) => {
vec![$($x),+]
};
}
fn main() {
println!("{}", square!(5));
let v: Vec<i32> = my_vec![1, 2, 3];
println!("{:?}", v);
let empty: Vec<i32> = my_vec![];
println!("{:?}", empty);
}Output
25
[1, 2, 3]
[]| 片段 | 匹配 |
|---|---|
expr | 表达式 |
ident | 标识符 |
ty | 类型 |
pat | 模式 |
stmt | 语句 |
block | 代码块 |
tt | 单个 token 树 |
item | 项(fn、struct 等) |
卫生性
Rust
macro_rules! make_var {
() => {
let x = 1; // 宏里的 x 和外面的 x 不是同一个
};
}
fn main() {
let x = 100;
make_var!();
println!("{}", x); // 100,宏没覆盖它
}Output
100这叫「卫生性」:宏内部引入的变量不会污染调用处。C 的 #define 没有这个特性,#define SWAP(a,b) int tmp = a; ... 会和调用处的 tmp 撞名。
过程宏
Rust
// 在一个单独的 crate 里(proc-macro = true)
use proc_macro::TokenStream;
#[proc_macro_derive(Hello)]
pub fn hello_derive(input: TokenStream) -> TokenStream {
let ast = syn::parse(input).unwrap();
impl_hello(&ast)
}TOML
[lib]
proc-macro = true
[dependencies]
syn = { version = "2", features = ["full"] }
quote = "1"
proc-macro2 = "1"过程宏要三个 crate:syn 把 TokenStream 解析成语法树,quote 用模板生成代码,proc-macro2 提供可测试的中间层。手写 TokenStream 拼接几乎不可维护。
常用内置宏
Rust
fn main() {
// 断言
assert!(1 + 1 == 2);
assert_eq!(2 + 2, 4);
assert_ne!(1, 2);
// 格式化
println!("{}", format!("a{}", 1));
// 编译期
println!("{}", env!("CARGO_PKG_NAME"));
println!("{}", file!());
println!("{}", line!());
// 条件编译
#[cfg(debug_assertions)]
println!("debug 构建");
// 提前返回
fn f(x: Option<i32>) -> i32 {
let Some(v) = x else { return 0 };
v
}
println!("{}", f(Some(1)));
// 编译期生成 panic
// compile_error!("这个分支不应该被编译");
}Output
a1
hello
src/main.rs
8
debug 构建
1该不该写宏
- 该写 —— 消除大量重复的样板(
#[derive]类)、DSL、编译期校验 - 不该写 —— 用泛型或 trait 能解决的,别用宏
- 不该写 —— 只在一处用一次的代码
宏的错误信息通常很差——报错指向宏展开后的代码,和你的源码对不上。
macro_rules! 里用 $x:expr 而不是 $x:tt 能显著改善报错质量。