提供用于生成Rust代码的宏。 | A Rust library that provides support for generating Rust code.
| 文件 | 最后提交记录 | 最后更新时间 |
|---|---|---|
| 4 个月前 | ||
| 4 个月前 | ||
| 4 个月前 | ||
| 4 个月前 | ||
| 4 个月前 | ||
| 4 个月前 | ||
| 4 个月前 | ||
| 2 年前 | ||
| 3 年前 | ||
| 3 个月前 | ||
| 4 个月前 | ||
| 4 个月前 | ||
| 3 个月前 | ||
| 4 年前 |
Rust 准引用
本 crate 提供了 quote! 宏,用于将 Rust 语法树数据结构转换为源代码令牌。
Rust 中的过程宏接收令牌流作为输入,执行任意 Rust 代码以确定如何处理这些令牌,并生成令牌流返回给编译器,以便编译到调用者的 crate 中。准引用就是解决其中一个环节的方案——生成返回给编译器的令牌。
准引用的理念是,我们编写的代码将被视为数据。在 quote! 宏内部,我们可以编写在文本编辑器或 IDE 中看起来像代码的内容。我们能充分利用编辑器的括号匹配、语法高亮、缩进,甚至可能还有自动补全功能。但这段内容不会作为代码编译到当前 crate 中,而是被当作数据,可以传递、修改,并最终作为令牌返回给编译器,编译到宏调用者的 crate 里。
本 crate 的开发初衷是服务于过程宏的使用场景,但它同时也是一个通用的 Rust 准引用库,并非专门针对过程宏。
[dependencies]
quote = "1.0"
版本要求:Quote 支持 rustc 1.68 及更高版本。
发布说明
语法
quote crate 提供了一个 quote! 宏,您可以在其中编写 Rust 代码,这些代码会被打包成一个 TokenStream,并可被视为数据。您可以将 TokenStream 理解为表示一段 Rust 源代码片段。
在 quote! 宏中,使用 #var 进行插值。任何实现了 quote::ToTokens trait 的类型都可以进行插值。这包括大多数 Rust 基本类型以及来自 syn 的大多数语法树类型。
let tokens = quote! {
struct SerializeWith #generics #where_clause {
value: &'a #field_ty,
phantom: core::marker::PhantomData<#item_ty>,
}
impl #generics serde::Serialize for SerializeWith #generics #where_clause {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
#path(self.value, serializer)
}
}
SerializeWith {
value: #value,
phantom: core::marker::PhantomData::<#item_ty>,
}
};
重复
重复功能通过 #(...)* 或 #(...),* 实现,类似于 macro_rules!。它会遍历重复块内任何插值变量的元素,并为每个元素插入一份重复体的副本。插值中的变量可以是 Vec、切片、BTreeSet 或任何 Iterator。
#(#var)*— 无分隔符#(#var),*— 星号前的字符用作分隔符#( struct #var; )*— 重复块中可包含其他内容#( #k => println!("{}", #v), )*— 甚至可以包含多个插值
请注意,#(#var ,)* 和 #(#var),* 之间存在区别——后者不会生成尾随逗号。这与 macro_rules! 中分隔符的行为一致。
向编译器返回令牌
quote! 宏计算后得到的表达式类型为 proc_macro2::TokenStream。而 Rust 过程宏期望返回 proc_macro::TokenStream 类型。
这两种类型的区别在于,proc_macro 类型完全特定于过程宏,绝不可能存在于过程宏之外的代码中,而 proc_macro2 类型可以存在于任何地方,包括测试以及 main.rs 和 build.rs 等非宏代码中。这就是为什么即使是过程宏生态系统也主要围绕 proc_macro2 构建,因为这样可以确保库是可单元测试的,并且能在非宏上下文中使用。
这两种类型之间存在双向的 From 转换,因此从过程宏返回 quote! 的输出通常写作 tokens.into() 或 proc_macro::TokenStream::from(tokens)。
示例
组合引用片段
通常,您不会一次性构建整个最终的 TokenStream。不同的部分可能来自不同的辅助函数。quote! 生成的令牌本身实现了 ToTokens,因此可以插值到后续的 quote! 调用中,以构建最终结果。
let type_definition = quote! {...};
let methods = quote! {...};
let tokens = quote! {
#type_definition
#methods
};
构造标识符
假设我们有一个标识符 ident,它来自宏输入中的某个位置,而我们需要以某种方式对其进行修改,以便用于宏输出。让我们考虑在标识符前添加下划线。
直接将标识符插入到下划线旁边并不会实现拼接效果。下划线和标识符仍会是两个独立的标记,就好像你写了 _ x 一样。
// incorrect
quote! {
let mut _#ident = 0;
}
解决方法是使用正确的值构建一个新的标识符令牌。由于这是一种常见情况,format_ident! 宏提供了一个便捷的工具来正确执行此操作。
let varname = format_ident!("_{}", ident);
quote! {
let mut #varname = 0;
}
或者,可以使用 Syn 和 proc-macro2 提供的 API 直接构建标识符。这与上述方法大致等效,但不会处理作为原始标识符的 ident。
let concatenated = format!("_{}", ident);
let varname = syn::Ident::new(&concatenated, ident.span());
quote! {
let mut #varname = 0;
}
进行方法调用
假设我们的宏要求宏输入中指定的某个类型具有名为 new 的构造函数。我们将该类型存储在一个类型为 syn::Type 的变量 field_type 中,并希望调用该构造函数。
// incorrect
quote! {
let value = #field_type::new();
}
这种情况并非总能奏效。如果 field_type 是 String,展开后的代码会包含 String::new(),这没有问题。但如果 field_type 是类似 Vec<i32> 这样的类型,展开后的代码就会变成 Vec<i32>::new(),这属于无效语法。通常在手写的 Rust 代码中,我们会写成 Vec::<i32>::new(),不过对于宏而言,以下方式往往更为便捷。
quote! {
let value = <#field_type>::new();
}
这会展开为 <Vec<i32>>::new(),其行为符合预期。
类似的模式也适用于 trait 方法。
quote! {
let value = <#field_type as core::default::Default>::default();
}
卫生性
任何插值标记都会保留其 ToTokens 实现所提供的 Span 信息。在 quote! 调用内部生成的标记,其跨度由 Span::call_site() 指定。
可以通过 quote_spanned! 宏显式提供不同的跨度。
非宏代码生成器
当在 build.rs 或 main.rs 中使用 quote 并将输出写入文件时,建议代码生成器在写入前通过 prettyplease 处理标记。这样,若生成的代码中出现错误,便于人工阅读和调试。
请注意,当标记写入文件时,不会保留任何卫生性或跨度信息;从标记到源代码的转换是有损的。
build.rs 中的使用示例:
let output = quote! { ... };
let syntax_tree = syn::parse2(output).unwrap();
let formatted = prettyplease::unparse(&syntax_tree);
let out_dir = env::var_os("OUT_DIR").unwrap();
let dest_path = Path::new(&out_dir).join("out.rs");
fs::write(dest_path, formatted).unwrap();
许可协议
根据您的选择,本项目基于 Apache 许可证 2.0 版 或 MIT 许可证 进行许可。除非您明确另有说明,否则您有意提交以纳入此 crate 的任何贡献(如 Apache-2.0 许可证中所定义)均应按上述方式双重许可,且不附加任何额外条款或条件。