【Rust宏编程】Rust有关宏编程底层原理解析与应用实战
✨✨ 欢迎大家来到景天科技苑✨✨
🎈🎈 养成好习惯,先赞后看哦~🎈🎈
🏆 作者简介:景天科技苑
🏆《头衔》:大厂架构师,华为云开发者社区专家博主,阿里云开发者社区专家博主,CSDN全栈领域优质创作者,掘金优秀博主,51CTO博客专家等。
🏆《博客》:Rust开发,Python全栈,Golang开发,云原生开发,PyQt5和Tkinter桌面开发,小程序开发,人工智能,js逆向,App逆向,网络系统安全,数据分析,Django,fastapi,flask等框架,云原生K8S,linux,shell脚本等实操经验,网站搭建,数据库等分享。所属的专栏:Rust语言通关之路
景天的主页:景天科技苑
文章目录
- Rust宏编程
- 1. Rust宏系统概述
- 2. 宏和函数的区别
- 3. 声明式宏:macro_rules!
- 3.1 基础语法与模式匹配
- 3.2 宏模式详解
- 3.3 重复模式与递归宏
- 4. 过程式宏:更强大的代码生成
- 4.1 设置过程式宏项目
- 4.2 类属性宏
- 4.2.1 基本概念
- 4.2.2 定义类属性宏
- 4.2.3 基本用法示例
- 4.2.4 实际应用示例
- 4.2.5 注意事项
- 4.3 类函数宏
- 4.3.1 基本概念
- 4.3.2 定义类函数宏
- 4.3.3 基本用法示例
Rust宏编程
1. Rust宏系统概述
Rust的宏系统是其语言中最强大但也最复杂的特性之一。
与C/C++的简单文本替换宏不同,Rust提供了两种完全集成到语言中的宏系统:声明式宏(declarative macros)和过程式宏(procedural macros)。
宏在Rust中扮演着至关重要的角色,它们允许开发者:
- 消除重复代码
- 创建领域特定语言(DSL)
- 在编译时生成代码
- 实现编译期计算
许多流行的Rust库如serde、tokio和rocket都大量使用了宏来提供优雅的API。本文将通过实际案例带你深入理解Rust宏的方方面面。
2. 宏和函数的区别
从根本上来说,宏是一种为写其他代码而写代码的方式,即所谓的 元编程(metaprogramming)。
我们之前使用过 println! 宏和 vec! 宏。所有的这些宏以 展开 的方式来生成比你所手写出的更多的代码。
元编程对于减少大量编写和维护的代码是非常有用的,它也扮演了函数扮演的角色。但宏有一些函数所没有的附加能力。
一个函数标签必须声明函数参数个数和类型。
相比之下,宏能够接受不同数量的参数:用一个参数调用 println!(“hello”) 或用两个参数调用 println!(“hello {}”, name) 。
而且,宏可以在编译器翻译代码前展开,例如,宏可以在一个给定类型上实现 trait 。而函数则不行,因为函数是在运行时被调用,同时 trait 需要在编译时实现。
实现一个宏而不是一个函数的缺点是宏定义要比函数定义更复杂,因为你正在编写生成 Rust 代码的 Rust 代码。由于这样的间接性,宏定义通常要比函数定义更难阅读、理解以及维护。
宏和函数的最后一个重要的区别是:在一个文件里调用宏 之前 必须定义它,或将其引入作用域,而函数则可以在任何地方定义和调用。
3. 声明式宏:macro_rules!
Rust 最常用的宏形式是 声明宏(declarative macros)。它们有时也被称为 “macros by example”、“macro_rules! 宏” 或者就是 “macros”。
其核心概念是,声明宏允许我们编写一些类似 Rust match 表达式的代码。
match 表达式是控制结构,其接收一个表达式,与表达式的结果进行模式匹配,然后根据模式匹配执行相关代码。
宏也将一个值和包含相关代码的模式进行比较;此种情况下,该值是传递给宏的 Rust 源代码字面量,模式用于和传递给宏的源代码进行比较,同时每个模式的相关代码则用于替换传递给宏的代码。所有这一切都发生于编译时。
可以使用 macro_rules! 来定义宏。
3.1 基础语法与模式匹配
声明式宏使用macro_rules!定义,其基本结构如下:
macro_rules! macro_name {(pattern) => { expansion };// 更多匹配规则...
}
让我们从一个简单的例子开始:
macro_rules! say_hello {() => {println!("Hello, world!");};
}fn main() {//注意:调用的时候,类似系统自带的宏println!一样,后面加个感叹号!say_hello!(); // 输出: Hello, world!
}
宏的强大之处在于模式匹配。我们可以定义接受不同输入的宏:
示例一:
macro_rules! greet { // 定义一个名为 greet 的宏// 宏的匹配模式,匹配一个参数($name:expr) => {println!("Hello, {}!", $name);};//匹配两个参数($name:expr, $time:expr) => {println!("Good {}, {}!", $time, $name);};
}fn main() {greet!("Alice"); // 输出: Hello, Alice!greet!("Bob", "evening"); // 输出: Good evening, Bob!
}
示例二:匹配多种模式
//计算两个数相加,相乘的宏
macro_rules! calculate {(add, $x:expr, $y:expr) => {$x + $y};(multiply, $x:expr, $y:expr) => {$x * $y};
}fn main() {let sum = calculate!(add, 1, 2);let product = calculate!(multiply, 3, 4);println!("Sum: {}, Product: {}", sum, product);
}
3.2 宏模式详解
Rust宏中的$符号用于捕获元变量,后面跟着的类型是片段分类符(fragment specifier),常见的有:
宏可以匹配以下形式:
$var:expr:表达式
$var:ident:标识符(函数名、变量名等)
$var:tt:token tree(单个标记或括号内的标记)
$var:block:代码块
$var:pat:匹配模式
$var:ty:类型
$var:stmt:语句
$var:item:项(如函数、struct)
$var:meta:元属性(如 #[derive(Debug)])
macro_rules! create_function {// `ident`表示标识符,此处用于匹配函数名($func_name:ident) => {//根据传参,生成一个函数fn $func_name() {println!("You called {}", stringify!($func_name));}};
}create_function!(foo);
create_function!(bar);fn main() {foo(); // 输出: You called foobar(); // 输出: You called bar
}
宏可以生成rust代码
3.3 重复模式与递归宏
宏可以处理可变数量的参数,使用 ( . . . ) ∗ 、 (...)*、 (...)∗、(…)+或$(…),*等重复模式:
//通过宏来创建一个向量
macro_rules! vector {//匹配一个或多个参数($($x:expr),*) => {{let mut temp_vec = Vec::new();//将参数展开为一个个表达式$(temp_vec.push($x);)*temp_vec}};
}fn main() {//使用宏来创建一个向量let v = vector![1, 2, 3];println!("{:?}", v); // 输出: [1, 2, 3]
}
案例讲解:
如果是在库里面创建宏,需要导出,使用#[macro_export] 标注
#[macro_export] 标注说明,只要将定义了宏的 crate 引入作用域,宏就应当是可用的。如果没有该标注,这个宏就不能被引入作用域。
使用 macro_rules! 和宏名称开始宏定义,且所定义的宏并 不带 感叹号。名字后跟大括号表示宏定义体,在该例中宏名称是 vec 。
vec! 宏的结构和 match 表达式的结构类似。此处有一个单边模式 ( $( $x:expr ),* ) ,后跟 => 以及和模式相关的代码块。
如果模式匹配,该相关代码块将被执行。假设这是这个宏中唯一的模式,则只有这一种有效匹配,其他任何匹配都是错误的。更复杂的宏会有多个单边模式。
宏定义中有效模式语法和模式语法是不同的,因为宏模式所匹配的是 Rust 代码结构而不是值。
首先,一对括号包含了整个模式。
接下来是美元符号( $ ),后跟一对括号,捕获了符合括号内模式的值以用于替换后的代码。$() 内则是 $x:expr ,其匹配 Rust 的任意表达式,并将该表达式记作 $x。
$() 之后的逗号说明一个可有可无的逗号分隔符可以出现在 $() 所匹配的代码之后。紧随逗号之后的 * 说明该模式匹配零个或多个 * 之前的任何模式。
当以 vec![1, 2, 3]; 调用宏时,$x 模式与三个表达式 1、2 和 3 进行了三次匹配。
现在让我们来看看与此单边模式相关联的代码块中的模式:对于每个(在 => 前面)匹配模式中的 $() 的部分,
生成零个或多个(在 => 后面)位于 $()* 内的 temp_vec.push() ,生成的个数取决于该模式被匹配的次数。
$x 由每个与之相匹配的表达式所替换。当以 vec![1, 2, 3]; 调用该宏时,替换该宏调用所生成的代码会是下面这样:
let mut temp_vec = Vec::new();
temp_vec.push(1);
temp_vec.push(2);
temp_vec.push(3);
temp_vec
我们已经定义了一个宏,其可以接收任意数量和类型的参数,同时可以生成能够创建包含指定元素的 vector 的代码。
macro_rules! 中有一些奇怪的地方。在将来,会有第二种采用 macro 关键字的声明宏,其工作方式类似但修复了这些极端情况。
在此之后,macro_rules! 实际上就过时(deprecated)了。
宏还支持递归调用,这允许实现复杂的代码生成:
macro_rules! count_exprs {() => (0);($head:expr) => (1);($head:expr, $($tail:expr),*) => (1 + count_exprs!($($tail),*)); //递归调用计算参数个数
}fn main() {println!("Count: {}", count_exprs!(1, 2, 3)); // 输出: Count: 3
}
4. 过程式宏:更强大的代码生成
过程式宏比声明式宏更强大,过程宏更像函数(一种过程类型)。
过程宏接收 Rust 代码作为输入,在这些代码上进行操作,然后产生另一些代码作为输出,而非像声明式宏那样匹配对应模式然后以另一部分代码替换当前代码。
它们可以接受任意TokenStream并返回新的TokenStream。这也是宏的核心:宏所处理的源代码组成了输入 TokenStream,同时宏生成的代码是输出 TokenStream。
过程式宏分为三种:
- 自定义派生宏(derive macros):为#[derive]属性实现自定义trait
- 属性宏(attribute-like macros):自定义属性
- 函数式宏(function-like macros):类似macro_rules!但更灵活
4.1 设置过程式宏项目
创建过程宏时,其定义必须驻留在它们自己的具有特殊 crate 类型的 crate 中。这么做出于复杂的技术原因,将来我们希望能够消除这些限制。
1)创建crate
mkdir guochenghong
创建个库项目
cd guochenghong
cargo new hello_macro --lib
然后在hello_macro这个crate里面的src/lib.rs中创建trait
pub trait HelloMacro {fn hello_macro();
}
在这个库项目里面,再创建个库项目
cd hello_mcaro
cargo new hello_macro_derive --lib
2)要使用过程宏,必须在一个独立的 crate 中定义,并启用如下依赖
此时,我们在hello_macro_derive 这个crate的Cargo.toml中添加:
依赖可以通过cargo add添加
cargo add quote
cargo add syn
[dependencies]
quote = "1.0.40"
syn = "2.0.101"[lib]
proc-macro = true
然后编写实现trait的代码
src/lib.rs
extern crate proc_macro; //导入外部包
use quote::quote;
use syn;
use crate::proc_macro::TokenStream;fn impl_hello_macro(ast: &syn::DeriveInput) -> TokenStream {// 获取结构体名称let name = &ast.ident;//新版本中,gen作为关键字了,不能用let gen1 =quote! {impl HelloMacro for #name {fn hello_macro() {println!("Hello, Macro! My name is {}!", stringify!(#name));}}};gen1.into()
}#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {// 将 Rust 代码解析为语法树以便进行操作let ast = syn::parse(input).unwrap();// 构建 trait 实现impl_hello_macro(&ast)
}
新版也可以这样导入
use proc_macro; //导入外部包
use quote::quote;
use syn;
use proc_macro::TokenStream;fn impl_hello_macro(ast: &syn::DeriveInput) -> TokenStream {// 获取结构体名称let name = &ast.ident;//新版本中,gen作为关键字了,不能用let gen1 =quote! {impl HelloMacro for #name {fn hello_macro() {println!("Hello, Macro! My name is {}!", stringify!(#name));}}};gen1.into()
}#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {// 将 Rust 代码解析为语法树以便进行操作let ast = syn::parse(input).unwrap();// 构建 trait 实现impl_hello_macro(&ast)
}
3)使用过程宏
回到guochenghong目录,与hello_macro平行目录下创建main项目
cargo new main
然后修改Cargo.toml文件,设置依赖
在main.rs中调用过程宏
use hello_macro::HelloMacro;
use hello_macro_derive::HelloMacro;#[derive(HelloMacro)]
struct Pancakes;fn main() {Pancakes::hello_macro();
}
4.2 类属性宏
类属性宏与自定义派生宏相似,不同于为 derive 属性生成代码,它们允许你创建新的属性。它们也更为灵活;derive 只能用于结构体和枚举;属性还可以用于其它的项,比如函数。
类属性宏(Attribute-like macros)是Rust宏系统中的一种强大功能,允许你创建自定义属性,这些属性可以附加到Rust的各种项(item)上,如结构体、枚举、函数等。
4.2.1 基本概念
类属性宏类似于Rust内置的属性,如#[derive(…)]、#[test]等,但是由开发者自定义的。它们使用#[…]语法,可以接收输入并生成代码。
4.2.2 定义类属性宏
要定义一个类属性宏,你需要:
创建一个proc-macro类型的crate
使用proc_macro_attribute属性标记函数
// 在Cargo.toml中
[lib]
proc-macro = true
// 在lib.rs中
use proc_macro::TokenStream;#[proc_macro_attribute]
pub fn my_attribute(attr: TokenStream, item: TokenStream) -> TokenStream {// attr是属性括号内的内容// item是属性所附加的项// 返回处理后的TokenStreamitem
}
4.2.3 基本用法示例
- 简单属性宏
#[proc_macro_attribute]
pub fn hello(_attr: TokenStream, item: TokenStream) -> TokenStream {println!("Hello from attribute macro!");item
}// 使用
#[hello]
fn some_function() {}
- 带参数的属性宏
#[proc_macro_attribute]
pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream {let route_path = attr.to_string();let input = syn::parse_macro_input!(item as syn::ItemFn);let fn_name = &input.sig.ident;let block = &input.block;let output = quote::quote! {fn #fn_name() {println!("Routing to: {}", #route_path);#block}};output.into()
}// 使用
#[route("/home")]
fn home() {println!("Home page");
}
4.2.4 实际应用示例
- Web框架路由
#[proc_macro_attribute]
pub fn get(attr: TokenStream, item: TokenStream) -> TokenStream {let route_path = attr.to_string();let input = syn::parse_macro_input!(item as syn::ItemFn);let fn_name = &input.sig.ident;let fn_block = &input.block;let output = quote::quote! {fn #fn_name() {println!("GET request to: {}", #route_path);#fn_block}inventory::submit! {RouteInfo {method: "GET",path: #route_path,handler: #fn_name,}}};output.into()
}// 使用
#[get("/users")]
fn get_users() {// 获取用户逻辑
}
- ORM模型定义
#[proc_macro_attribute]
pub fn model(attr: TokenStream, item: TokenStream) -> TokenStream {let args = syn::parse_macro_input!(attr as syn::AttributeArgs);let input = syn::parse_macro_input!(item as syn::ItemStruct);let struct_name = &input.ident;let fields = if let syn::Fields::Named(fields) = &input.fields {fields} else {panic!("Model must have named fields");};// 生成字段信息、表名等let output = quote::quote! {#inputimpl Model for #struct_name {fn table_name() -> &'static str {stringify!(#struct_name)}fn fields() -> Vec<Field> {vec![#(Field {name: stringify!(#fields),// 其他字段属性}),*]}}};output.into()
}// 使用
#[model]
struct User {id: i32,name: String,email: String,
}
4.2.5 注意事项
性能影响:过程宏在编译时执行,复杂的宏可能会增加编译时间
错误信息:确保提供清晰的错误信息,使用syn::Error来生成友好的编译错误
卫生性:注意宏卫生性,使用quote!宏时生成的标识符需要正确处理
测试:为你的宏编写充分的测试,可以使用trybuild库测试宏的错误情况
4.3 类函数宏
类函数宏定义看起来像函数调用的宏。类似于 macro_rules!,它们比函数更灵活;例如,可以接受未知数量的参数。
然而 macro_rules! 宏只能使用之前 “使用 macro_rules! 的声明宏用于通用元编程” 介绍的类匹配的语法定义。
类函数宏获取 TokenStream 参数,其定义使用 Rust 代码操纵 TokenStream,就像另两种过程宏一样。
类函数宏(Function-like macros)是Rust中一种强大的元编程工具,它们看起来像函数调用,但在编译时执行代码生成。
这类宏使用macro_name!(…)语法,可以接收任意Token流并生成新的Rust代码。
4.3.1 基本概念
类函数宏:
使用macro_name!(…)语法调用
在编译期执行代码转换
可以接受任意复杂的输入
必须定义在proc-macro类型的crate中
4.3.2 定义类函数宏
要定义一个类函数宏,你需要:
创建一个proc-macro类型的crate
使用proc_macro属性标记函数
// Cargo.toml
[lib]
proc-macro = true
// lib.rs
use proc_macro::TokenStream;#[proc_macro]
pub fn my_macro(input: TokenStream) -> TokenStream {// 处理输入并生成输出input
}
4.3.3 基本用法示例
- 简单类函数宏
#[proc_macro]
pub fn say_hello(input: TokenStream) -> TokenStream {println!("Macro received: {}", input);"println!(\"Hello, world!\")".parse().unwrap()
}// 使用
say_hello!(); // 展开为 println!("Hello, world!");
- 带参数的宏
#[proc_macro]
pub fn create_struct(input: TokenStream) -> TokenStream {let name = input.to_string();let output = format!("struct {} {{ x: i32, y: i32 }}", name);output.parse().unwrap()
}// 使用
create_struct!(Point); // 展开为 struct Point { x: i32, y: i32 }
更多宏的使用,可以参考官方推荐的书:https://danielkeep.github.io/tlborm/book/mbe-macro-rules.html