在 Rust 中,你不能像 Python 的 eval() 那样在程序运行时“把字符串变代码执行”。quote! 生成的代码,必须在一个叫做“过程宏 (Procedural Macro)”的特殊函数里返回给编译器。
编译器在编译你的代码时,会执行这个宏,拿到 quote! 生成的代码,把它“粘贴”到你的源码里,然后再编译最终的程序。
为了演示如何真的生成代码并运行,我们需要建立一个完整的宏项目。这比单写 main.rs 稍微复杂一点点,但我会手把手带你做。
核心流程图解
解析 (Syn): 编译器把你的结构体代码给宏 -> 宏用
syn库读懂它。生成 (Quote): 宏用
quote!生成新的代码(比如impl块)。注入: 宏把
quote!的结果返回给编译器 -> 编译器把它合并到源码中。
实战:编写一个 #[derive(Hello)] 宏
我们要实现的效果是:只要给结构体加上 #[derive(Hello)],它就自动拥有一个 hello() 方法。
由于 Rust 的宏必须放在单独的库里,我们需要创建两个部分:
宏库 (
my_macros):用来生成代码的工具人。主程序 (
app):真正使用宏的程序。
第一步:创建宏库 (my_macros)
在你的终端里运行:
cargo new my_macros –lib
- 修改 my_macros/Cargo.toml
我们要告诉 Rust 这是一个宏库,并添加必要的依赖(syn 是用来解析代码的,quote 是用来生成代码的)。
Ini, TOML
[package]
name = "my_macros"
version = "0.1.0"
edition = "2021"
[lib]
proc-macro = true # 关键!声明这是一个过程宏库
[dependencies]
syn = "2.0" # 代码解析器(把代码变成数据结构)
quote = "1.0" # 代码生成器(把数据结构变回代码)
proc-macro2 = "1.0"
- 编写 my_macros/src/lib.rs
这是魔法发生的地方。
Rust
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};
// 这里的入口函数名 `hello_macro` 对应 #[derive(Hello)]
#[proc_macro_derive(Hello)]
pub fn hello_macro(input: TokenStream) -> TokenStream {
// 1. 解析:把输入的 Rust 代码(TokenStream)转换成我们可以操作的语法树
let ast = parse_macro_input!(input as DeriveInput);
// 2. 提取结构体的名字 (比如 "User")
let name = &ast.ident;
// 3. 生成:用 quote! 写代码模板
// 这里的 #name 会被替换成具体的结构体名字
let gen = quote! {
impl #name {
pub fn hello() {
println!("你好!我是自动生成的代码,我的名字是 {}。", stringify!(#name));
}
}
};
// 4. 返回:把生成的代码交回给编译器
gen.into()
}
第二步:创建主程序 (app)
在和 my_macros 同一级目录下,创建一个普通项目:
cargo new app
- 修改 app/Cargo.toml
我们需要引用刚才写的那个宏库。
Ini, TOML
[package]
name = "app"
version = "0.1.0"
edition = "2021"
[dependencies]
# 引用本地的宏库
my_macros = { path = "../my_macros" }
- 编写 app/src/main.rs
现在我们可以使用那个宏了!
Rust
use my_macros::Hello; // 导入我们的宏
// 使用宏!
// 编译器看到这就跑去执行 my_macros 里的代码,
// 并在背后生成了 impl User { pub fn hello() ... }
#[derive(Hello)]
struct User {
name: String,
}
#[derive(Hello)]
struct Order {
id: i32,
}
fn main() {
// 调用自动生成的方法
User::hello();
Order::hello();
}
第三步:运行见证奇迹
在 app 目录下运行:
cargo run
输出结果:
Plaintext
你好!我是自动生成的代码,我的名字是 User。
你好!我是自动生成的代码,我的名字是 Order。
总结
回到你的问题:如何生成代码?
Quote 只是工具:它只负责把“填空模板”变成
TokenStream(标记流)。过程宏是载体:必须通过
#[proc_macro_derive]这样的入口函数,把TokenStream真正返回给编译器。编译器是执行者:编译器收到返回值后,把它“粘贴”到你的源码里,然后才开始生成二进制文件。
现在的逻辑清楚了吗?
如果你想查看编译器到底偷偷帮你生成了什么代码(不用猜),有一个非常好用的工具叫做 cargo-expand。
你想让我教你如何安装和使用 cargo-expand 来看这些隐藏的代码吗?