1 个不稳定版本
| 0.1.0 | 2020年4月14日 |
|---|
#1298 in 过程宏
每月21次下载
在 syn-impersonated 中使用
69KB
931 行
Rust近似引用
改编自
quote如果你不知道自己在做什么,不要在proc-macro上下文中使用
此crate提供quote!宏,将Rust语法树数据结构转换为源代码的标记。
Rust的过程宏接收一个标记流作为输入,执行任意Rust代码来确定如何操作这些标记,并产生一个标记流返回给编译器以编译成调用者的crate。近似引用是解决这一问题的方法之一——向编译器返回标记。
近似引用的想法是编写我们将其视为数据的代码。在quote!宏中,我们可以编写看起来像代码的内容,这些内容看起来像我们的文本编辑器或IDE中的代码。我们获得编辑器括号匹配、语法高亮、缩进和可能自动补全的所有好处。但我们可以将其作为数据,传递它,修改它,最终将其作为标记返回给编译器,以编译成宏调用者的crate。
此crate受过程宏用例的启发,但它是一个通用Rust近似引用库,并不特定于过程宏。
[dependencies]
quote-impersonated = "0.1"
语法
quote crate提供了一个quote!宏,在其中你可以编写Rust代码,这些代码将被打包成一个TokenStream,并可以被视为数据。你应该把TokenStream视为代表Rust源代码片段。
在 quote! 宏中,使用 #var 进行插值。任何实现了 quote::ToTokens 特性的类型都可以进行插值。这包括大多数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! 类似。这会遍历重复中插值的任何变量的元素,并为每个元素插入重复体的副本。插值中的变量可以是任何实现了 IntoIterator 的东西,包括 Vec 或现有迭代器。
#(#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 的构造函数。我们有一个类型变量 field_type,类型为 syn::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(),这会正确地工作。
对于特例方法,也适用类似的模式。
quote! {
let value = <#field_type as core::default::Default>::default();
}
hygiene
任何插值令牌都保留其 Span 信息,这些信息由其 ToTokens 实现提供。来自 quote! 调用的令牌使用 Span::call_site() 方法进行范围标记。
可以通过 quote_spanned! 宏显式地提供不同的范围。
许可证
在您的选择下,根据 Apache License, Version 2.0 或 MIT 许可证 许可。除非您明确说明,否则根据 Apache-2.0 许可证定义的,您提交的任何旨在包含在此软件包中的贡献,将按照上述方式双重许可,没有任何附加条款或条件。
依赖关系
约150KB