#proc-macro #quote #macro #performance #syn

quote_into

通过宏引用轻松高效地生成代码

2个不稳定版本

0.2.0 2023年1月7日
0.1.0 2023年1月6日

1060过程宏 中排名

Download history 79/week @ 2024-03-13 42/week @ 2024-03-20 42/week @ 2024-03-27 60/week @ 2024-04-03 21/week @ 2024-04-10 35/week @ 2024-04-17 22/week @ 2024-04-24 22/week @ 2024-05-01 29/week @ 2024-05-08 67/week @ 2024-05-15 31/week @ 2024-05-22 74/week @ 2024-05-29 64/week @ 2024-06-05 592/week @ 2024-06-12 278/week @ 2024-06-19 177/week @ 2024-06-26

1,123 每月下载次数
用于 5 个crate(其中3个直接使用)

MIT/Apache

8KB

快速代码生成宏

通过引用轻松高效地生成代码

use proc_macro2::TokenStream;
use quote_into::quote_into;

let mut stream = TokenStream::new();
quote_into!(stream += println!("hello world!"));
assert_eq!(stream.to_string(), "println ! (\"hello world!\")");

变量插值

您可以使用变量的名称(以 # 为前缀)插值任何实现 ToTokens 的值。

use proc_macro2::TokenStream;
use quote_into::quote_into;

let mut stream = TokenStream::new();
let variable = false;
quote_into!(stream += let boolean = #variable;);
assert_eq!(stream.to_string(), "let boolean = false ;");

插值块

您可以使用 #{} 插入Rust代码块。在内部,您可以进行函数调用、使用条件语句或循环,或任何其他允许在代码块中进行的操作。

use proc_macro2::TokenStream;
use quote_into::quote_into;

let mut stream = TokenStream::new();
quote_into!(stream += let array = [#{
    for i in 0..3 {
        quote_into!(stream += Some(#i),)
    }
}];);
assert_eq!(stream.to_string(), "let array = [Some (0i32) , Some (1i32) , Some (2i32) ,] ;");

注意:如果您有引用标记中的组(如 {}()[]),将创建一个新的 TokenStream。在上面的示例中,外部的 stream#{} 插值中的 stream 不相同,因为它被 [] 包装。

表达式插值

您还可以使用 #() 插值任何Rust表达式。

use proc_macro2::TokenStream;
use quote_into::quote_into;

let mut s = TokenStream::new();
quote_into!(s += const ANSWER: u32 =  #(7 * 6););
assert_eq!(s.to_string(), "const ANSWER : u32 = 42i32 ;");

quote 的比较

代码生成的既定标准是 quote crate。这个crate对其API和性能提出了一些小的改进。

API

当涉及到循环插值时,Quote似乎在很大程度上受到了 macro_rules 的启发。虽然表达力强且简洁,但在我看来,它们学习起来和阅读起来有点困难。

相反,quote_into 提供了一个简单的“插值块” #{ },您可以在其中编写常规的Rust代码。由于基本上没有额外的语法,使用插值块应该感觉更直观。

性能

quote 返回一个新的 TokenStream,而 quote_into 接受一个 TokenStream 以向其中追加标记。虽然这使得 quote 的代码稍微简洁一些,但在组合引用片段时,效率略低。通常,代码生成发生在不同的函数中,然后组合在一起。推荐的方法是创建中间的 TokenStream,并使用 # 进行插值。

use quote::quote;

let type_definition = quote! {...};
let methods = quote! {...};

let tokens = quote! {
    #type_definition
    #methods
};

即使您只创建了几个 TokenStreams 并将它们组合,额外的分配也会对性能产生轻微的影响。更极端的情况是深度递归函数,它反复构建和插值许多标记流。例如,考虑以下函数,它生成如下形式的代码 Box<Box<...<bool>>>

use quote::quote;
use quote_into::quote_into;
use proc_macro2::TokenStream;

// Using quote:
fn quote_box_wrap(levels: usize) -> TokenStream {
    match levels {
        0 => quote!(bool),
        other => {
            let inner = quote_box_wrap(other - 1);
            quote!(Box<#inner>)
        }
    }
}

// Using quote_into:
fn quote_into_box_wrap(levels: usize) -> TokenStream {
    fn inner(s: &mut TokenStream, levels: usize) {
        match levels {
            0 => quote_into!(s += bool),
            other => quote_into! {s += Box < #{
                inner(s, other - 1)
            } >},
        }
    }

    // there's a bit of boilerplate, but only once per macro
    let mut s = TokenStream::new();
    inner(&mut s, levels);
    s
}

对于 levels=100 的结果

quote:      591.14 µs
quote_into: 17.247 µs

由于使用 quote 需要创建许多中间 TokenStreams,因此在这种情况下速度较慢。这又是一个极端的例子。除非您像上面那样不高效地嵌套多个插值的 TokenStreams 多层,否则切换到 quote_into 后,代码生成速度不太可能提高 30 倍。

状态:原型

虽然您可以在项目中技术上使用 quote_into,但它目前处于原型阶段。

  • API 可能会更改,因为我对一些事情还不确定。
  • 它是使用 quote 实现的...我不确定这有多“糟糕”(它可能会增加编译时间),但我有一种感觉,它应该在某个时候使用手动标记生成代码进行重写。

依赖项

~1.5MB
~35K SLoC