84 个版本 (37 个稳定版本)

1.0.37 2024年8月22日
1.0.36 2024年4月10日
1.0.35 2024年1月2日
1.0.34 2023年12月31日
0.3.10 2016年11月26日

#5过程宏

Download history 3486067/week @ 2024-05-02 3370869/week @ 2024-05-09 3480023/week @ 2024-05-16 3350289/week @ 2024-05-23 3667486/week @ 2024-05-30 3539788/week @ 2024-06-06 3774345/week @ 2024-06-13 3625623/week @ 2024-06-20 3488880/week @ 2024-06-27 3379041/week @ 2024-07-04 3734021/week @ 2024-07-11 3705975/week @ 2024-07-18 3767931/week @ 2024-07-25 3869550/week @ 2024-08-01 4241840/week @ 2024-08-08 3994026/week @ 2024-08-15

16,584,108 每月下载量
用于 99,548 个crate (9,023 直接)

MIT/Apache

96KB
1.5K SLoC

Rust Quasi-Quoting

github crates.io docs.rs build status

此crate提供了quote!宏,将Rust语法树数据结构转换为源代码的标记。

Rust中的过程宏接收一个标记流作为输入,执行任意Rust代码以确定如何操作这些标记,并产生一个标记流返回给编译器以编译成调用者的crate。Quasi-quoting是解决这个问题的一种方法——生成返回给编译器的标记。

Quasi-quoting的想法是我们编写将作为数据处理的代码。在quote!宏内部,我们可以编写看起来像代码的内容,供我们的文本编辑器或IDE使用。我们可以获得编辑器括号匹配、语法高亮、缩进以及可能自动完成的全部好处。但不是将其作为代码编译到当前crate中,而是将其作为数据处理,传递它,修改它,最终将其作为标记返回给编译器,以便将其编译成宏调用者的crate。

此crate受过程宏用例的启发,但是一个通用的Rust quasi-quoting库,并不局限于过程宏。

[dependencies]
quote = "1.0"

版本要求:Quote支持rustc 1.56及以上。
发行说明


语法

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!。这会遍历重复中任何插值的变量元素,并为每个元素插入一个重复体的副本。插值中的变量可能是一个 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 的构造函数。我们有一个名为 field_type 的变量,其类型为 syn::Type,并希望调用构造函数。

// incorrect
quote! {
    let value = #field_type::new();
}

这有时是有效的。如果 field_typeString,则展开的代码包含 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! 宏显式地提供不同的范围。


非宏代码生成器

当在 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 License, Version 2.0MIT 许可证 许可。
除非您明确声明,否则根据 Apache-2.0 许可证定义的,您有意提交以包含在此软件包中的任何贡献,都将根据上述条款进行双许可,不附加任何额外条款或条件。

依赖关系

~57KB