Rust 的过程宏(Procedural Macros)是一种强大的元编程工具,允许你在编译时对代码进行操作和生成。与属性宏和派生宏不同,过程宏可以接收并处理任意 Rust 代码,生成新的代码片段。这里有一个简单的例子来说明 Rust 的过程宏。
假设你想创建一个过程宏来生成一个函数,该函数返回一个固定的字符串。我们可以按如下步骤进行:
-
创建一个新的 Rust 库项目:
cargo new my_proc_macro --lib cd my_proc_macro
-
配置
Cargo.toml
:
在Cargo.toml
中,我们需要启用过程宏支持,并添加必要的依赖项:[package] name = "my_proc_macro" version = "0.1.0" edition = "2018" [dependencies] syn = { version = "1.0", features = ["full"] } quote = "1.0" proc-macro2 = "1.0" [lib] proc-macro = true
-
编写过程宏代码:
编辑lib.rs
文件,编写我们的过程宏:extern crate proc_macro; use proc_macro::TokenStream; use quote::quote; use syn::{parse_macro_input, ItemFn}; #[proc_macro_attribute] pub fn hello_fn(_attr: TokenStream, item: TokenStream) -> TokenStream { // 解析输入的函数 let input = parse_macro_input!(item as ItemFn); let name = &input.sig.ident; // 生成新的代码 let expanded = quote! { pub fn #name() -> &'static str { "Hello, world!" } }; // 把生成的代码返回给编译器 TokenStream::from(expanded) }
-
使用过程宏:
创建一个新的二进制项目来使用这个宏:cargo new my_app cd my_app
更新
Cargo.toml
以包含我们刚刚创建的过程宏库:[package] name = "my_app" version = "0.1.0" edition = "2018" [dependencies] my_proc_macro = { path = "../my_proc_macro" }
编写使用这个过程宏的代码:
use my_proc_macro::hello_fn; #[hello_fn] fn greet() {} fn main() { println!("{}", greet()); }
-
运行程序:
现在你可以编译并运行这个程序:cargo run
你将会看到输出:
Hello, world!
这个简单的例子展示了如何创建和使用一个过程宏。这个过程宏 hello_fn
接受一个函数,并生成一个返回固定字符串 "Hello, world!"
的函数。实际中,过程宏可以用来生成更复杂的代码,提供编译时的代码验证和生成功能。
过程宏
#[hello_fn]
。这像是你给greet
函数贴上了一个标签,告诉编译器:“嘿!我希望这个函数返回的是'Hello, world!'
,而不仅仅是一个普通的空函数。
-
当编译器遇到
#[hello_fn]
时,它会调用你定义的hello_fn
过程宏函数。 -
过程宏接收到
greet()
函数作为输入,它首先用syn
库解析了输入的函数(通过parse_macro_input!(item as ItemFn)
)。也就是说,它把原本的函数(一个空的greet
函数)转成了可以被 Rust 进一步处理的结构体。 -
然后,它通过
quote!
宏生成了一个新的函数代码。这个生成的新函数名字就是你传入的greet
(通过#name
),但是这个函数的实现已经变了,不再是空的了,它返回"Hello, world!"
。 -
最后,生成的代码(新的
greet
函数)被返回给编译器,替换原来的空函数代码。
_attr: TokenStream 与 item: TokenStream
接下来我们用一个更复杂的例子来解释
_attr: TokenStream
与item: TokenStream
1. item: TokenStream
item
是宏作用的目标代码,这部分代码可以是函数、结构体、枚举等。例如:
#[my_macro]
fn some_function() {
println!("Hello, world!");
}
在这个例子中,item
就是函数 some_function()
的代码。你可以解析 item
,修改它,或者在代码中做一些转换。
2. _attr: TokenStream
_attr
是宏的属性参数(即 #[my_macro(...)]
中的内容)。这部分通常是用户提供的配置,像是标志、字符串、数字或其他值。例如:
#[my_macro("hello")]
fn some_function() {
println!("Hello, world!");
}
在这个例子中,_attr
就是 "hello"
字符串。
现在假设我们要实现一个属性宏,它接收一个字符串属性并打印该字符串,然后将目标函数的功能替换为返回该字符串。
use proc_macro::TokenStream;
use syn::{parse_macro_input, LitStr, ItemFn};
use quote::quote;
#[proc_macro_attribute]
pub fn print_hello(_attr: TokenStream, item: TokenStream) -> TokenStream {
// 解析 _attr(属性参数),这里假设我们传入的是一个字符串
let attr_input = parse_macro_input!(_attr as LitStr); // LitStr 是字符串字面量
let greeting = attr_input.value(); // 获取字符串的值
// 解析 item(目标代码),这里我们假设目标代码是一个函数
let input_fn = parse_macro_input!(item as ItemFn);
let fn_name = &input_fn.sig.ident; // 获取目标函数的名称
// 创建新的代码:新的函数实现,打印字符串,并返回这个字符串
let expanded = quote! {
pub fn #fn_name() -> &'static str {
println!("{}", #greeting);
#greeting
}
};
// 返回生成的代码
TokenStream::from(expanded)
}
解释:
-
_attr: TokenStream
:我们使用parse_macro_input!
把传入的_attr
转换成一个LitStr
,即字符串字面量类型。在这个宏中,我们假设用户传入的是一个字符串,例如#[print_hello("Hello, World!")]
。通过attr_input.value()
获取到字符串的实际值。 -
item: TokenStream
:我们使用parse_macro_input!
将目标代码(item
)转换成ItemFn
,即目标函数的 AST 结构。在这里,我们提取了函数的名称(fn_name
),以便在生成代码时保持一致。
这段代码使用了
quote!
宏来生成 Rust 代码片段。quote!
是quote
库提供的一个宏,用于在 Rust 中进行代码生成,它将 Rust 代码转化为TokenStream
,可以用于宏生成、代码插入、以及模板代码的生成。
let expanded = quote! {
pub fn #fn_name() -> &'static str {
println!("{}", #greeting);
#greeting
}
};
-
quote! { ... }
宏:quote!
是quote
库提供的宏,它将其内部的代码块转换为TokenStream
,这可以用于后续的代码生成或宏扩展。- 你可以把
quote!
看作是一个 Rust 代码模板,允许在其中插入动态内容。
-
#fn_name
和#greeting
:- 在
quote!
宏内部,使用#
符号可以将 Rust 代码中定义的变量、表达式或者其他值插入到代码模板中。这样做可以在模板中动态替换变量。 #fn_name
是一个变量,它会被替换成你传递给quote!
宏的具体函数名。比如,如果fn_name
是一个字符串"hello"
, 那么#fn_name
会被替换成hello
。#greeting
是另一个变量,它会被替换成你传递的具体值,即#[my_macro(...)]
中的内容。
- 在
-
生成的代码:
- 该模板生成一个名为
fn_name
的函数,它返回一个'static str
类型的字符串,并且在执行时打印一个greeting
的值。 pub fn #fn_name() -> &'static str { ... }
生成一个公开的函数定义,其函数名是#fn_name
,返回类型是'static str
,即静态字符串。println!("{}", #greeting);
生成了一条打印语句,用于输出greeting
的内容。- 最后,
#greeting
表示函数的返回值,它返回的是传入的greeting
字符串。
- 该模板生成一个名为
使用:
假设我们在主程序中使用这个宏:
use my_proc_macro::print_hello;
#[print_hello("Hello, world!")]
fn greet() {}
fn main() {
println!("{}", greet()); // 应该打印 "Hello, world!" 并返回该字符串
}
输出:
Hello, world!
Hello, world!