Rust宏
Rust 的宏主要分为两类:声明式宏(Declarative Macros,使用 macro_rules! 定义)和 过程宏(Procedural Macros)
声明式宏(macro_rules!)的参数
在 macro_rules! 中,参数被称为片段说明符(Fragment Specifiers)。
基本语法结构
在宏定义中,参数通过 符号引入,格式为 标识符:类型。
rust
macro_rules! my_macro {
// matcher (匹配器): 定义参数模式
($x:expr, $y:ident) => {
// transcriber (转录器): 使用参数生成代码
println!("表达式结果: {}, 标识符名称: {}", $x, stringify!($y));
};
}
常用的参数类型(片段说明符)
以下是 Rust 宏中最常用的参数类型及其含义:
| 片段说明符 | 匹配内容(Fragment) | 示例 |
|---|---|---|
expr |
表达式(Expressions) | 2 + 3, my_function(), x |
ident |
标识符(Identifiers) | 变量名x, 函数名foo, 模块名 |
path |
路径(Paths) | std::vec::Vec, crate::MyMod::func |
ty |
类型(Types) | i32, String, Vec<T>, &str |
pat |
模式(Patterns) | Some(x), (a, b), ref y, _ |
stmt |
语句(Statements) | let x = 5, x += 1一个完整的语句(不含结尾分号) |
block |
代码块(Blocks) | { let x = 1; x + 2 } ,{...} |
item |
项(Items) | fn foo() {}, struct S;, mod m { } |
meta |
元信息(Meta-items) | derive(Debug), cfg(target_os = "linux") |
tt |
标记树(Token Trees) | 任意单个 token 或 ( ... ) / [ ... ] / { ... } 内容 |
重复模式(Repetition)
Rust 宏中的 重复模式(Repetition) 是 macro_rules! 声明式宏最强大的特性之一,它允许你匹配和生成任意数量的代码片段,是实现可变参数、列表处理、DSL 等高级功能的核心机制。
基本语法:
txt
重复模式使用 `$(...)*、$(...)+ 或 $(...)?` 语法:
... 是你要重复的模式或转录内容
*、+、? 是重复操作符
三种重复操作符详解
| 操作符 | 含义 | 最小次数 | 示例 |
|---|---|---|---|
| * | 零次或多次 | 0 | $(x),* → 可匹配空、a、a,b、a,b,c |
| + | 一次或多次 | 1 | $(x),+ → 必须至少有一个,如 a、a,b |
| ? | 零次或一次 | 0 或 1 | $(x)? → 要么没有,要么只有一个 |
分隔符(Separator)
在重复模式中,你可以指定分隔符(如逗号 ,、分号 ;、空格等),用于匹配或生成时插入。
rust
$( pattern ),* // 用逗号分隔,重复零次或多次。
$( pattern ),+ // 用逗号分隔,重复一次或多次。
$( pattern );+ // 用分号分隔,重复一次或多次。
$( pattern )? // 无分隔符(因为最多一个)
$( pattern );* // 用分号分隔,重复零次或多次。
$(...)*
它表示:捕获零个或多个任意 token tree,并将它们全部绑定到 $arg 上(在重复上下文中)。
但由于 * 表示"多个",而 $arg 是单数,实际使用时必须配合重复展开。
rust
$( $arg : tt ) *
│ │ │ └─ 重复:0 次或多次(* 表示"零次或多次")
│ │ └──────片段说明符(fragment specifier):tt = token tree
│ └─────────────绑定名(你可以用任何合法标识符,如 x、args 等)
└─────────────────宏参数绑定语法(必须用 $ 开头)
正确用法:在重复上下文中使用 $arg
macro_rules! forward {
($($arg:tt)*) => {
my_function($($arg)*); // ✅ 将所有捕获的 token 原样传给另一个函数/宏
};
}
// 使用
forward!(1, 2, "hello", { x + y });
// 展开为:
// my_function(1, 2, "hello", { x + y });
这就是为什么 println!、vec! 等宏能接受任意参数------它们内部通常使用了类似 ($($t:tt)*) 的模式。
典型应用场景
- 转发参数给其他宏或函数
rust
macro_rules! debug_log {
($($t:tt)*) => {
if cfg!(debug_assertions) {
eprintln!($($t)*);
}
};
}
- 实现可变参数宏
rust
macro_rules! count_args {
() => { 0 };
($($t:tt)*) => { <[()]>::len(&[$($t),*]) }; // 技巧:转为数组求长度
}
核心作用是在编译期计算出传递给宏的参数个数。
宏的含义与原理拆解:
这个宏包含两条规则,我们逐一来看看:
1,() => { 0 };
这是递归或匹配的终止条件(Base Case)。
当调用 count_args!() 且括号里什么都没有时,它直接返回数字 0。
2,($($t:tt)*) => { <[()]>::len(&[$($t),*]) };
($($t:tt)*):匹配任意数量(0个或多个)的 Token Tree(tt 是 Rust 宏中最通用的匹配符,可以匹配几乎任何代码片段)。
核心技巧:<[()]>::len(&[$($t),*])
[$($t),*]:宏展开时,会把捕获到的所有参数用逗号连接,生成一个数组。比如传入 1, "hello", true,这里就会展开成 [1, "hello", true]。
[()]:这是一个只包含单元类型 ()(即空元组)的数组类型。
3,为什么能求长度? 在 Rust 中,任何表达式都可以被转换为 ()(只要加上分号或者在特定上下文中)。这个技巧利用了数组初始化时会去计算每个元素的特性。无论 $($t) 是什么,Rust 都会为它们分配数组的位置。最终,<[()]>::len() 获取的是这个临时生成数组的长度,而这个长度正好等于参数的个数。
// 场景 1:传入 3 个不同类型的参数
let count1 = count_args!(1, "hello", true);
// 宏展开后相当于执行了:
// let count1 = <[()]>::len(&[(), (), ()]);
// 结果为 3
// 场景 2:传入 1 个参数
let count2 = count_args!("single_arg");
// 宏展开后相当于执行了:
// let count2 = <[()]>::len(&[()]);
// 结果为 1
// 场景 3:不传参数
let count3 = count_args!();
// 直接匹配第一条规则,结果为 0
- 递归宏的基础(配合 tt 模式匹配)
rust
这个recursive!宏是 Rust 声明式宏中递归解析(Recursive Parsing)的经典范例。它的核心作用是:将传入的一长串代码片段(Token Tree),拆解成一个一个的独立单元,并依次交给另一个宏(handle!)去处理。
macro_rules! recursive {
() => {};
($head:tt $($tail:tt)*) => {
handle!($head);
recursive!($($tail)*);
};
}
宏的含义与原理拆解:
这个宏包含两条规则,它们共同构成了递归的逻辑:
1,() => {};
这是递归的终止条件(Base Case)。
当传入的参数被一点点"蚕食"完,括号里什么都没有时,宏就会匹配到这条规则,直接返回一个空块 {},递归就此结束。如果没有这条规则,宏在参数耗尽时会报错。
($head:tt $($tail:tt)*) => { handle!($head); recursive!($($tail)*); };
参数拆解:
$head:tt:捕获传入代码片段的第一个标记树(Token Tree)。tt 是最灵活的捕获符,可以匹配单个符号、标识符,甚至是一对括号及其内部的所有内容。
$($tail:tt)*:捕获剩下所有的标记树(0个或多个)。
递归逻辑:
handle!($head);:把当前拆出来的第一个元素 $head 交给 handle! 宏去处理。
recursive!($($tail)*);:宏自己调用自己,把剩下的 $($tail)* 再次传进去。这样,下一轮递归时,原本的"第二个元素"就变成了新的 $head,如此循环往复。
举例说明
// 辅助宏:负责处理单个元素
macro_rules! handle {
($t:tt) => {
println!("正在处理元素: {}", stringify!($t));
};
}
// 你提供的递归宏
macro_rules! recursive {
() => {};
($head:tt $($tail:tt)*) => {
handle!($head);
recursive!($($tail)*);
};
}
fn main() {
// 传入一串毫无关联、甚至不符合 Rust 语法的符号
recursive!(hello + 123 * [world]);
}
执行流程与输出结果:
第一轮:$head 是 hello,$tail 是 + 123 * [world]。打印 hello,带着剩下的递归。
第二轮:$head 是 +,$tail 是 123 * [world]。打印 +,带着剩下的递归。
第三轮:$head 是 123,$tail 是 * [world]。打印 123,带着剩下的递归。
第四轮:$head 是 *,$tail 是 [world]。打印 *,带着剩下的递归。
第五轮:$head 是 [world](整个括号被视为一个 tt),$tail 为空。打印 [world],带着空参数递归。
第六轮:匹配到 () => {};,递归结束。
最终控制台输出:
正在处理元素: hello
正在处理元素: +
正在处理元素: 123
正在处理元素: *
正在处理元素: [world]
- 匹配函数参数
rust
macro_rules! my_fn {
($name:ident ( $($arg:ident : $ty:ty),* )) => {
fn $name( $($arg: $ty),* ) {
println!("Called function {}", stringify!($name));
}
};
}
// 使用
my_fn!(add (x: i32, y: i32));
// 展开为:
// fn add(x: i32, y: i32) { ... }
- 重复模式在"转录侧"(右侧)的使用:在宏的右侧(=> { ... } 部分),你必须以相同方式展开左侧捕获的重复变量。
rust
macro_rules! repeat_print {
($($x:expr),*) => {
$(println!("{}", $x);)*
};
}
repeat_print!(1, 2, "hello");
// 展开为:
// println!("{}", 1);
// println!("{}", 2);
// println!("{}", "hello");
- 在Rust调用C程序中的平台Log打印函数
rust
C宏函数:
define UEDOMAIN_DBG(fmt, ...) UE_DOMAIN_LOG_TEMPLATE(Dbg, fmt, ##__VA_ARGS__)
在C语言中封装FFI接口
VOID UeDomainDebug(CHAR *pStr, ...)
{
va_list args;
va_start(args, pStr);
UEDOMAIN_DBG(pStr, args);
va_end(args);
}
在Rust中unsafe模块中导入FFI
mod uemcm_unsafe{
unsafe extern "C" {
pub fn UeDomainDebug(fmt: *const c_char, ...);
}
}
在Rust内部代码封装宏
macro_rules! ue_domain_debug {
($($arg:tt)*) => {
unsafe{
use std::ffi::CString;
// 格式化字符串
let formatted = format!($($arg)*);
// 转换为C字符串
let c_str = CString::new(formatted).unwrap();
// 调用C函数
uemcm_unsafe::UeDomainDebug(c_str.as_ptr());
}
}
}
过程宏(Procedural Macros)的参数
过程宏(包括派生宏、属性宏、函数式宏)的参数处理方式与声明式宏完全不同。它们不使用 macro_rules! 的模式匹配,而是直接操作 Rust 的抽象语法树(AST)。
派生宏#derive(Debug)
是一个派生宏(Derive Macro),它用于自动为自定义的数据结构(如 struct、enum 或 union)实现 Debug trait。
- 核心作用:
- 它的主要作用是:让该类型能够被格式化打印,以便于调试和开发。当你在定义一个结构体或枚举时加上 #derive(Debug),Rust 编译器会自动生成实现 Debug trait 所需的代码。这使得你可以使用 {:?} 或 {:#?} 格式化符在 println! 宏中打印这个类型的实例。
- 如果没有 #derive(Debug),你尝试打印一个自定义结构体时会得到编译错误。
- 例:
rust
// 定义一个结构体,并派生 Debug trait
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect = Rectangle { width: 30, height: 50 };
// 现在可以使用 {:?} 打印 rect
println!("矩形信息: {:?}", rect);
// 或者使用 {:#?} 进行"美化"打印(格式更清晰,适合复杂结构)
println!("美化打印:\n{:#?}", rect);
}
txt
打印输出:
矩形信息: Rectangle { width: 30, height: 50 }
美化打印:
Rectangle {
width: 30,
height: 50,
}
详细说明:
{:?}:使用 Debug 格式打印,输出紧凑。
{:#?}:使用"美化"(pretty-print)的 Debug 格式,输出更易读,尤其对嵌套结构。
其他常用的 derive 宏
derive 不仅限于 Debug,还有很多其他常用的 trait 可以通过 derive 自动实现:
#derive(Clone):允许值被克隆。
#derive(Copy):使类型变为可复制的(通常与 Clone 一起使用)。
#derive(PartialEq):支持相等性比较 (==, !=)。
#derive(Eq):表示类型具有完全相等性。
#derive(PartialOrd, Ord):支持比较和排序。
#derive(Hash):使类型可用于 HashMap 等集合。
#derive(Default):为类型提供默认值。
Debug trait 的作用
目的:让类型能够被格式化打印,主要用于调试和开发。
使用方式:配合 println! 宏中的 {:?} 或 {:#?} 格式化符。
效果:你可以打印出结构体或枚举的字段值,方便查看程序状态。
rust
#[derive(Debug, Clone)]
struct Person {
name: String,
age: u32,
}
fn main() {
let p = Person { name: "Alice".to_string(), age: 30 };
println!("这个人是: {:?}", p); // 可以打印
}
Clone trait 的作用
目的:允许通过显式调用 .clone() 方法来创建一个值的完整副本(深拷贝)。
背景:Rust 默认是"移动语义"(move semantics),赋值或传参时会转移所有权。如果类型没有实现 Copy,就不能简单地"复制"。
效果:调用 .clone() 可以安全地复制数据,即使它包含 String、Vec 等堆上数据。
rust
let p1 = Person { name: "Alice".to_string(), age: 30 };
let p2 = p1.clone(); // 显式克隆,p1 和 p2 是两个独立的实例
println!("{:?}", p1); // p1 仍然可用!
组合效果:#derive(Debug, Clone)
当你同时使用这两个派生时,你的类型就具备了:
✅ 可打印性:方便调试,能用 {:?} 打印内部字段。
✅ 可复制性:能通过 .clone() 创建副本,避免所有权转移带来的麻烦。
这在实际开发中非常常见,尤其是在定义配置、数据模型、消息类型等场景。
rust
#[derive(Debug, Clone)]
struct Point {
x: i32,
y: i32,
}
fn main() {
let p1 = Point { x: 1, y: 2 };
// 1. Debug:打印
println!("点 p1: {:?}", p1);
// 2. Clone:复制
let p2 = p1.clone();
println!("点 p2: {:?}", p2);
// p1 仍然有效
println!("p1 仍然存在: {:?}", p1);
}
Clone 要求结构体的所有字段也必须实现了 Clone。例如,如果字段是 String、Vec、Rc 等,它们都实现了 Clone,所以没问题。
如果你希望类型是"可复制"的(即赋值时不移动,而是自动复制),可以额外加上 Copy:#derive(Debug, Clone, Copy),但前提是所有字段也都实现了 Copy。
#derive(Debug, Clone) 是 Rust 中最推荐的组合之一
Rust 五大核心格式化宏
Rust 提供了 5 个最常用的格式化宏,它们的使用语法完全一致,区别仅在于输出的目标和是否换行:
| 宏名称 | 作用 | 输出目标 | 自动换行 |
|---|---|---|---|
print! |
格式化输出文本 | 标准输出 (stdout) | ❌ |
println! |
格式化输出文本 | 标准输出 (stdout) | ✅ |
format! |
生成格式化字符串 | 内存中的 String 对象 |
--- |
eprint! |
格式化输出错误信息 | 标准错误 (stderr) | ❌ |
eprintln! |
格式化输出错误信息 | 标准错误 (stderr) | ✅ |
实例代码
fn main() {
// {[argument][:[fill][align][width][.precision][type]]}
// 填充与对齐
println!("[{}]", format!("{:->8}", "hi")); // 右对齐 -> [------hi]
println!("[{}]", format!("{:-<8}", "hi")); // 左对齐 -> [hi------]
println!("[{}]", format!("{:-^8}", "hi")); // 居中 -> [---hi---]
println!("[{}]", format!("{:0>5}", 42)); // 填充字符 '0', 右对齐 -> [00042]
// 动态宽度+精度(后面需要$)
println!("[{value:>width$.prec$}]", value=3.14159, width=10, prec=3); // [ 3.142]
// 带前缀的十六进制
println!("[{:#08x}]", 42); // [0x00002a]
println!("[{}]", format!("{:08x}", 42)); // [0000002a] 没有0x前缀
println!("[{}]", format!("{:#08x}", 42)); // [0x00002a]
println!("width: [{:*^5}]", 123); // width: [*123*]
// ### 精度控制(.precision)
println!("{:.3}", "hello"); // hel
println!("{:.2}", 3.1415); // 3.14
println!("{:.4}", 12); // 12
// 在 Rust 中,{:.*} 是一种动态精度(Dynamic Precision)的语法。它的工作原理是:从参数列表中按顺序"消耗"两个参数。第一个参数作为精度值,第二个参数才是要被格式化的实际值。
println!("{}-{:.*}","hello", 2, 3.1415); // hello-3.14
}
核心占位符:{} 与 {:?}
{:#?}:美化版的调试输出,会自动换行并缩进,打印复杂数据结构时非常清晰
进阶格式化语法
格式化语法说明:{argument:\[fillalignwidth.precisiontype]}
参数指定:
{argument}用于指定要格式化的参数,默认省略(按顺序)
| 形式 | 说明 | 示例 | 输出结果 |
|---|---|---|---|
| 省略 | 按参数顺序匹配(默认) | println!("{}, {}", "a", "b") | a, b |
| 位置索引{n} | 按索引n(从 0 开始)匹配 | println!("{1}, {0}", "a", "b") | b, a |
| 命名{name} | 按参数名匹配 | println!("{x}, {y}", x=1, y=2) | 1, 2 |
填充与对齐
语法片段:fillalignwidth
fill:任意单字符(默认空格);使用fill时需要同时指定align+width。
align:<(左对齐)、>(右对齐,默认)、^(居中)。
width:最小字段宽度(整数),不足时使用 fill 填充。
| 形式 | 说明 | 示例 | 输出结果 |
|---|---|---|---|
| 固定宽度{:w} | 指定最小宽度 | println!("width: {:\*\^5}", 123); | *123* |
| 动态宽度{:width$} | 宽度由参数指定($是必须的) | println!("{:w$}", 123, w=5); | 123 |
精度控制(.precision)
语法片段:.precision(写作 .N 或动态 .*)
| 适用类型 | 含义 | 示例 | 输出结果 |
|---|---|---|---|
| 字符串 | 最大长度(超过则截断) | println!("{:.3}", "hello") |
hel |
| 浮点数 | 小数位数(四舍五入) | println!("{:.2}", 3.1415) |
3.14 |
| 整数 | 最小位数(不足补 0,超过则原样输出) | println!("{:.4}", 12) |
12 |
动态精度 {:.*} |
精度由参数指定(前一个参数为精度值) | println!("{}-{:.*}","ab", 2, 3.1415) |
ab-3.14 |
在 Rust 中,{:.*} 是一种动态精度(Dynamic Precision)的语法。它的工作原理是:从参数列表中按顺序"消耗"两个参数。第一个参数作为精度值,第二个参数才是要被格式化的实际值。 |
类型格式
指定数据的格式化类型,将数据转换为特定格式的字符串。
整数类型(i8/i16/i32/i64/u8/u16/.../usize/isize)
| 说明符 | 作用 | 示例 | 输出结果 |
|---|---|---|---|
b |
二进制 | println!("{:b}", 10) |
1010 |
o |
八进制 | println!("{:o}", 10) |
12 |
d |
十进制(默认) | println!("{:d}", 10) |
10 |
x |
小写十六进制 | println!("{:x}", 255) |
ff |
X |
大写十六进制 | println!("{:X}", 255) |
FF |
浮点数类型(f32/f64)
| 说明符 | 作用 | 示例 | 输出结果 |
|---|---|---|---|
f |
小数形式(默认) | println!("{:f}", 123.456) |
123.456000(默认 6 位小数) |
e |
科学计数法(小写 e) | println!("{:e}", 123.456) |
1.234560e+02 |
E |
科学计数法(大写 E) | println!("{:E}", 123.456) |
1.234560E+02 |
g |
自动选择 f 或 e(取较短形式) | println!("{:g}", 123456789.0) |
1.23457e+08 |
G |
自动选择 f 或 E | println!("{:G}", 123456789.0) |
1.23457E+08 |
通用与特殊类型
| 说明符 | 作用 | 适用场景 | 示例 | 输出结果 |
|---|---|---|---|---|
? |
按 Debug trait 格式化(调试用) | 所有实现 Debug 的类型 | println!("{:?}", vec![1,2]) |
[1, 2] |
#? |
美化的 Debug 格式(换行 + 缩进) | 复杂结构(如结构体、嵌套集合) | println!("{:#?}", vec![1,2]) |
[\n 1,\n 2\n] |
p |
指针地址 | 仅用于引用类型 | println!("{:p}", &10) |
0x7ff72194e7b8 |
标志(Flags)
特殊修饰符,用于调整输出格式,可与其他说明符组合使用。
| 标志 | 作用 | 示例 | 输出结果 |
|---|---|---|---|
# |
为数值添加前缀(二进制 0b、八进制 0o、十六进制 0x) |
println!("{:#x}", 255) |
0xff |
+ |
强制显示正负号(正数显 +,负数显 -) |
println!("{:+}", 10); println!("{:+}", -10) |
+10 -10 |
0 |
用 0 填充(代替空格) |
println!("{:05}", 123) |
00123 |
_ |
为大数值添加千位分隔符(仅整数和浮点数) | println!("{:_}", 1000000) |
1_000_000 |
特殊格式(转义字符)
| 语法 | 示例 | 说明 |
|---|---|---|
{``{ |
{``{ |
输出左花括号 { |
}} |
}} |
输出右花括号 } |