9个版本
0.4.0 | 2021年2月17日 |
---|---|
0.3.2 | 2020年3月23日 |
0.3.0 | 2019年8月15日 |
0.2.4 |
|
0.1.0 | 2019年1月30日 |
180 在 进程宏 中排名
每月12,287次下载
在 62 个 库中使用(直接使用20个)
36KB
124 行
Rust Quasiquoter
此包实现了quote!
宏作为进程宏,而不是使用macro_rules!
实现的原始quote!
宏。
quote!
宏将Rust语法树数据结构转换为源代码的标记。
Rust中的进程宏接收一个标记流作为输入,执行任意Rust代码以确定如何操作这些标记,并生成一个标记流返回给编译器以编译成调用者的包。准引用是解决该问题的一部分——生成返回给编译器的标记。
准引用的思路是编写我们将其视为数据的代码。在quote!
宏内部,我们可以编写看起来像代码的内容,就像我们在文本编辑器或IDE中一样。我们获得编辑器括号匹配、语法高亮、缩进以及可能的自动补全的所有好处。但我们可以将其作为数据编译到当前包中,而不是将其作为代码,我们可以将其作为数据传递、修改,并最终将其作为标记返回给编译器以编译到宏调用者的包中。
此包由进程宏用例激发,但它是一个通用的Rust准引用库,不特定于进程宏。
从quote
到proc-quote
此包与quote
具有相同的目的,但是它使用进程宏而不是macro_rules!
实现。从quote
切换到proc_quote
包不需要对代码进行任何更改。
更改您的Cargo.toml
依赖项后,更改以下内容
extern crate quote;
use quote::quote;
use quote::quote_spanned;
相应地改为
extern crate proc_quote;
use proc_quote::quote;
use proc_quote::quote_spanned;
这就完成了!
语法
quote crate 提供了一个 quote!
宏,在其中可以编写将被打包到 TokenStream
并作为数据处理的 Rust 代码。您应该将 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!
。这会遍历任何在重复内部插值的变量中的元素,并为每个元素插入重复体的副本。
#(#var)*
— 无分隔符#(#var),*
— 星号前的字符用作分隔符#( struct #var; )*
— 重复可以包含其他内容#( #k => println!("{}", #v), )*
— 甚至可以进行多次插值#( let #var = self.#var;)*
- 同一变量可以被多次使用
注意,#(#var,)*
和 #(#var),*
之间存在区别——后者不会产生尾随逗号。这符合 macro_rules!
中分隔符的行为。
proc_quote::Repeat
特性定义了在重复模式内部允许插值的类型。
Repeat
允许哪些类型
Iterator<T>
消耗迭代器,遍历每个元素。Borrow<[T]>
(包括Vec
、array
和slice
)使用slice::iter
方法迭代,因此不会消耗原始数据。ToTokens
,在每个迭代中将变量进行插值。
哪些类型不 Repeat
IntoIterator
,以避免歧义(例如:“对于实现了Vec
和Borrow<[T]>
的类型,将使用哪种行为;“对于实现了TokenStream
和ToTokens
的类型,将使用哪种行为?”)。要使用迭代器,您可以显式调用IntoIterator::into_iter
。- 至少实现了两个
Repeat
特性的歧义类型。在极不可能发生的情况下,可以通过将类型包裹在某些仅实现所需使用的特质的结构中来消除歧义。
将令牌返回给编译器
宏 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;
}
解决方案是使用 Syn 和 proc-macro2 提供的 API 进行标记级别的操作。
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();
}
宏卫生
任何插入的标记都会保留其 ToTokens
实现提供的 Span
信息。来自 quote!
调用的标记使用 Span::call_site()
进行标记。
可以通过 quote_spanned!
宏显式地提供不同的标记。
许可证
根据您的要求,受以下任一许可证的许可:
- Apache License,版本 2.0 (LICENSE-APACHE 或 https://apache.ac.cn/licenses/LICENSE-2.0)
- MIT 许可证 (LICENSE-MIT 或 http://opensource.org/licenses/MIT)
任选其一。
贡献
除非您明确说明,否则您根据Apache-2.0许可证定义的,有意提交以包含在此软件包中的任何贡献,应如上双授权,不附加任何额外条款或条件。
依赖项
~1.5MB
~36K SLoC