【Rust】 Rust宏学习笔记

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
特殊格式(转义字符)
语法 示例 说明
{``{ {``{ 输出左花括号 {
}} }} 输出右花括号 }
相关推荐
tianxingjian20192 小时前
从欧盟电池法新规看QFD:如何将合规需求转化为技术特性?
笔记
网络与设备以及操作系统学习使用者2 小时前
路由器如何实现跨VLAN通信
运维·网络·学习·华为·智能路由器
喜樂的CC2 小时前
NestJS图解笔记
笔记
182******20832 小时前
2026年学C语言还有出路吗?学习需要报班吗?
c语言·开发语言·学习
智者知已应修善业3 小时前
【51单片机数码管驱动2位显示0-99按键3短按+1长按+10按键4短按-1长按清零,按键不影响数码管显示】2023-8-16
c++·经验分享·笔记·算法·51单片机
whyTeaFo3 小时前
MIT 6.1810: xv6 book Chapter5: Page faults 笔记
笔记
rime_neko3 小时前
开发部署笔记
笔记
东方佑3 小时前
可学习破坏策略:实现大语言模型二倍推理加速的统一自洽框架
人工智能·学习·语言模型
森林古猿13 小时前
论CDQ分治
c++·学习·算法·排序算法