1.0.0-rc3 2019年8月11日
1.0.0-rc2 2019年8月10日
1.0.0-rc1 2019年7月29日
0.0.0 2019年7月14日

#9 in #quasi-quoting


2 crate 中使用

MIT/Apache

67KB
932

Rust 伪引号

Build Status Latest Version Rust Documentation

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

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

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

这个crate由过程宏用例激发,但它是一个通用的Rust伪引号库,并不特定于过程宏。

版本要求:Quote支持Rust 1.15.0中首次支持过程宏以来的任何编译器版本。

发行说明

[dependencies]
quote = "0.6"

语法

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;
}

解决方案是使用 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_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! 显式地提供不同的 span

限制

  • 在重复块内部可能无法插值非重复变量 (#7)。
  • 在重复块内部,同一变量可能无法插值超过一次 (#8)。

递归限制

quote! 宏依赖于深度递归,因此在编译时,某些大调用可能会因为 "达到递归限制" 而失败。如果它失败了,可以通过向你的 crate 中添加 #![recursion_limit = "128"] 来增加递归限制。对于特别大的调用,可能需要更高的限制。除非编译器告诉你需要它,否则不需要这样做。


许可

根据您的选择,受 Apache License, Version 2.0MIT 许可证 许可。
除非您明确说明,否则根据 Apache-2.0 许可证定义的,您有意提交以包含在此 crate 中的任何贡献,均将按照上述方式双许可,没有任何附加条款或条件。

依赖项

~180KB