我目前正在尝试为自定义特征编写派生宏。 这就是我到目前为止所得到的:
use proc_macro2::TokenStream;
use quote::{quote, quote_spanned};
use syn::spanned::Spanned;
use syn::{
parse_macro_input, parse_quote, Data, DeriveInput, Fields, GenericParam, Generics, Index,
};
#[proc_macro_derive(HeapSize)]
pub fn derive_heap_size(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
// Parse the input tokens into a syntax tree.
let input = parse_macro_input!(input as DeriveInput);
// Used in the quasi-quotation below as `#name`.
let name = input.ident;
// Add a bound `T: HeapSize` to every type parameter T.
let generics = add_trait_bounds(input.generics);
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
// Generate an expression to sum up the heap size of each field.
let sum = heap_size_sum(&input.data);
let expanded = quote! {
// The generated impl.
impl #impl_generics lestream::FromLeBytes for #name #ty_generics #where_clause {
fn heap_size_of_children(&self) -> usize {
#sum
}
}
};
// Hand the output tokens back to the compiler.
proc_macro::TokenStream::from(expanded)
}
// Add a bound `T: HeapSize` to every type parameter T.
fn add_trait_bounds(mut generics: Generics) -> Generics {
for param in &mut generics.params {
if let GenericParam::Type(ref mut type_param) = *param {
type_param.bounds.push(parse_quote!(lestream::FromLeBytes));
}
}
generics
}
// Generate an expression to sum up the heap size of each field.
fn heap_size_sum(data: &Data) -> TokenStream {
match *data {
Data::Struct(ref data) => {
match data.fields {
Fields::Named(ref fields) => {
// Expands to an expression like
//
// 0 + self.x.heap_size() + self.y.heap_size() + self.z.heap_size()
//
// but using fully qualified function call syntax.
//
// We take some care to use the span of each `syn::Field` as
// the span of the corresponding `heap_size_of_children`
// call. This way if one of the field types does not
// implement `HeapSize` then the compiler's error message
// underlines which field it is. An example is shown in the
// readme of the parent directory.
let q = quote! {
Self {
};
for field in fields.named {
let item_name = field.ident.expect("macro only works with named fields");
let item_type = field.ty;
quote! {
let #item_name = #item_type::from_le_bytes()
}
}
}
_ => panic!("The FromLeBytes derive can only be applied to structs"),
}
}
Data::Enum(_) | Data::Union(_) => unimplemented!(),
}
}
这个想法是通过按顺序调用结构体每个成员的特征方法
from_le_bytes()
的方式来实现它,从而派生出结构体的特征:
use std::fmt::{Display, Formatter};
#[derive(Debug)]
pub enum Error {
UnexpectedEndOfStream,
}
impl Display for Error {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::UnexpectedEndOfStream => write!(f, "unexpected end of stream"),
}
}
}
impl std::error::Error for Error {}
pub trait FromLeBytes: Sized {
fn from_le_bytes<T>(bytes: &mut T) -> Result<Self, Error>
where
T: Iterator<Item = u8>;
}
即:
#[derive(FromLeBytes)]
struct Foo {
bar: u8;
spamm: u16;
}
应该会产生类似
的实现impl FromLeBytes for Foo {
fn from_le_bytes<T>(bytes: &mut T) -> Result<Self, Error>
where
T: Iterator<Item = u8>;
{
Ok(Self { bar: u8::from_le_bytes(bytes)?, spamm: u16::from_le_bytes(bytes)? })
}
但是,我无法弄清楚如何在
quote!
宏中转义结构构造函数的大括号。
这是我第一次编写宏,所以如果 quote!
不是正确的工具,我也愿意接受其他建议。
我通过使用成对的大括号并使用
#ident
语法来呈现有效大括号对之间的上下文,从而避免了这个问题。我认为这对于未来的变化也应该不太容易出错。
use proc_macro2::TokenStream;
use quote::quote;
use syn::{parse_macro_input, parse_quote, Data, DeriveInput, Fields, GenericParam, Generics};
#[proc_macro_derive(FromLeBytes)]
pub fn derive_heap_size(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
// Parse the input tokens into a syntax tree.
let input = parse_macro_input!(input as DeriveInput);
// Used in the quasi-quotation below as `#name`.
let name = input.ident;
// Add a bound `T: HeapSize` to every type parameter T.
let generics = add_trait_bounds(input.generics);
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
// Generate an expression to sum up the heap size of each field.
let body = impl_body(&input.data);
let expanded = quote! {
// The generated impl.
impl #impl_generics lestream::FromLeBytes for #name #ty_generics #where_clause {
fn from_le_bytes<T>(bytes: &mut T) -> lestream::Result<Self>
where
T: Iterator<Item = u8>
{
#body
}
}
};
// Hand the output tokens back to the compiler.
proc_macro::TokenStream::from(expanded)
}
// Add a bound `T: HeapSize` to every type parameter T.
fn add_trait_bounds(mut generics: Generics) -> Generics {
for param in &mut generics.params {
if let GenericParam::Type(ref mut type_param) = *param {
type_param.bounds.push(parse_quote!(lestream::FromLeBytes));
}
}
generics
}
// Generate an expression to sum up the heap size of each field.
fn impl_body(data: &Data) -> TokenStream {
match *data {
Data::Struct(ref strct) => {
match strct.fields {
Fields::Named(ref fields) => {
// Expands to an expression like
//
// 0 + self.x.heap_size() + self.y.heap_size() + self.z.heap_size()
//
// but using fully qualified function call syntax.
//
// We take some care to use the span of each `syn::Field` as
// the span of the corresponding `heap_size_of_children`
// call. This way if one of the field types does not
// implement `HeapSize` then the compiler's error message
// underlines which field it is. An example is shown in the
// readme of the parent directory.
let mut tokens = TokenStream::new();
let mut constructor_fields = TokenStream::new();
for field in &fields.named {
let item_name = field.ident.clone().unwrap();
let item_type = &field.ty;
tokens.extend(quote! {
let #item_name = <#item_type as lestream::FromLeBytes>::from_le_bytes(bytes)?;
});
constructor_fields.extend(quote! {
#item_name,
});
}
tokens.extend(quote! { Ok(Self { #constructor_fields }) });
tokens
}
_ => panic!("The FromLeBytes derive can only be applied to structs"),
}
}
Data::Enum(_) | Data::Union(_) => unimplemented!(),
}
}
自我回答中已经描述了一种方法,但我会尝试为所讨论的问题添加更多背景知识。
出现此错误的原因是
quote
的输出必须是一系列有效的 Rust 标记,或者更准确地说,是 TokenTree
s 的序列。 Rust 没有单个左大括号或右大括号的标记;相反,它有一个 group 的概念,即放置在 inside 匹配的大括号对(或其他 Delimiter
s)中的标记子序列。
因此,
TokenStream
中任何位置存在不匹配的分隔符都是无效的。这正是您想要做的事情 quote!{ Self { }
。
至于为什么一定要这样——让我们想想下面的代码:
fn foo() -> proc_macro2::TokenStream {
quote!{ { }; // (1)
// imagine here's some code generating `TokenStream`,
// so that the function would be valid if this `quote` is valid
}
fn bar() -> proc_macro2::TokenStream {
quote!{ { }; // (2)
// imagine here's the same code as above in `foo`
}
}
让我们问自己:解析器在每种情况下应该如何遍历这段代码?
请注意,这里的函数
bar
实际上是编译——当然,它没有做任何有用的事情,但它是正确的;按原样,其中的 quote
宏会生成 TokenStream
,其中包含一个空块和一个分号(注释已被删除)。换句话说,如果注释被某些代码替换,则该代码将被传递到 quote
并且 不会被 Rust 编译器解析 - 仅进行词法分析。从解析器的角度来看,这很可能是无意义的,但由于是 quote
接收了这些标记 - 这种“无意义”实际上并不重要。bar
解析器将看到宏的左大括号,然后消耗所有内容,直到按原样匹配右大括号。
想象一下,现在,我们希望
foo
也能编译,并且 quote
生成单左大括号的 TokenStream
。这意味着解析器必须将第 (1) 行的右大括号视为关闭 quote
宏,并实际在其余标记上运行自身,因为它们现在 not 在宏上下文中,因此 must被解析。
但现在请注意,在解析第 (1) 行和第 (2) 行时,实际上无法区分这两种情况:
foo
和 bar
是完全相同的标记序列,除了一个额外的右大括号之外。为了检查这个额外的大括号是否确实在这里,解析器必须使用无限前瞻 - 也就是说,扫描直到文件末尾,然后,在看到大括号实际上不匹配后,倒回并再次开始解析。
此外,严格来说,很可能不可能知道哪个确切的括号必须被视为不匹配。想一想:
fn foo() {
quote::quote!{ { }; { };
}
如果 Rust 允许在宏中使用不匹配的大括号,则这段代码将是不明确的:
quote
到底必须在哪里结束?在第一个右大括号上,以便将其后面的一对大括号解析为一个块?或者在最后一个上,以便 quote
本身获得一个块作为输入(在单大括号之前)?在这种情况下,编译器不会做出决定 - 它会再次出错。
简而言之:允许不匹配的大括号将为编译器作者和语言用户打开太多复杂性的大门。