为什么Rust找不到使用proc_macro_attribute生成的枚举方法?

6

我正在尝试编写可以接受 Rust 枚举类型的过程宏。

#[repr(u8)]
enum Ty {
    A,
    B
}

并生成一个枚举的方法,让我可以将 u8 转换为允许的变体,如下所示

fn from_byte(byte: u8) -> Ty {
    match {
        0 => Ty::A,
        1 => Ty::B,
        _ => unreachable!()
    }
}

这是我使用proc_macro库实现的内容。(没有外部库)
#![feature(proc_macro_diagnostic)]
#![feature(proc_macro_quote)]
extern crate proc_macro;

use proc_macro::{TokenStream, Diagnostic, Level, TokenTree, Ident, Group, Literal};
use proc_macro::quote;

fn report_error(tt: TokenTree, msg: &str) {
    Diagnostic::spanned(tt.span(), Level::Error, msg).emit();
}

fn variants_from_group(group: Group) -> Vec<Ident> {
    let mut iter = group.stream().into_iter();
    let mut res = vec![];
    while let Some(TokenTree::Ident(id)) = iter.next() {
        match iter.next() {
            Some(TokenTree::Punct(_)) | None => res.push(id),
            Some(tt) => {
                report_error(tt, "unexpected variant. Only unit variants accepted.");
                return res
            }
        }
    }
    res
}

#[proc_macro_attribute]
pub fn procmac(args: TokenStream, input: TokenStream) -> TokenStream {
    let _ = args;
    let mut res = TokenStream::new();
    res.extend(input.clone());
    let mut iter = input.into_iter()
        .skip_while(|tt| if let TokenTree::Punct(_) | TokenTree::Group(_) = tt {true} else {false})
        .skip_while(|tt| tt.to_string() == "pub");
    match iter.next() {
        Some(tt @ TokenTree::Ident(_)) if tt.to_string() == "enum" => (),
        Some(tt) => {
            report_error(tt, "unexpected token. this should be only used with enums");
            return res
        },
        None => return res
    }

    match iter.next() {
        Some(tt) => {
            let variants = match iter.next() {
                Some(TokenTree::Group(g)) => {
                    variants_from_group(g)
                }
                _ => return res
            };
            let mut match_arms = TokenStream::new();
            for (i, v) in variants.into_iter().enumerate() {
                let lhs = TokenTree::Literal(Literal::u8_suffixed(i as u8));
                if i >= u8::MAX as usize {
                    report_error(lhs, "enum can have only u8::MAX variants");
                    return res
                }
                let rhs = TokenTree::Ident(v);
                match_arms.extend(quote! {
                    $lhs => $tt::$rhs,
                })
            }
            res.extend(quote!(impl $tt {
                pub fn from_byte(byte: u8) -> $tt {
                    match byte {
                        $match_arms
                        _ => unreachable!()
                    }
                }
            }))
        }
        _ => ()
    }
    
    res
}

这就是我使用它的方式。

use helper_macros::procmac;

#[procmac]
#[derive(Debug)]
#[repr(u8)]
enum Ty {
    A,
    B
}

fn main() {
    println!("TEST - {:?}", Ty::from_byte(0))
}

这会导致编译器报错。具体错误如下:
error[E0599]: no variant or associated item named `from_byte` found for enum `Ty` in the current scope
  --> main/src/main.rs:91:32
   |
85 | enum Ty {
   | ------- variant or associated item `from_byte` not found here
...
91 |     println!("TEST - {:?}", Ty::from_byte(0))
   |                                ^^^^^^^^^ variant or associated item not found in `Ty`

运行 cargo expand 可以生成正确的代码。并且直接运行该代码可以如预期般工作。但目前我被卡住了,可能是我对于 proc_macros 的使用方式有所遗漏,因为这是我第一次尝试使用它们,而我没有看到任何会导致这个错误的东西。我正在按照 proc_macro_workshop0 中的 "sorted" 部分进行操作。唯一的不同之处在于,我直接使用 TokenStream 而不是使用 syn 和 quote crates。另外,如果我打错方法名,rust编译器会提示一个类似名称的方法存在。

1个回答

5
这是一个 Playground 演示代码:https://play.rust-lang.org/?version=nightly&mode=debug&edition=2018&gist=02c1ee77bcd80c68967834a53c011e41 所以,你提到的确实是正确的:扩展后的代码可以复制粘贴并且可以工作。当这种情况发生时(宏扩展和“手动复制粘贴扩展”之间存在不同的行为),有两种可能性:
  • macro_rules! metavariables

    When emitting code using macro_rules! special captures, some of these captures are wrapped with special invisible parenthesis that already tell the parser how the thing inside should be parsed, which make it illegal to use in other places (for instance, one may capture a $Trait:ty, and then doing impl $Trait for ... will fail (it will parse $Trait as a type, thus leading to it being interpreted as a trait object (old syntax)); see also https://github.com/danielhenrymantilla/rust-defile for other examples.

    This is not your case, but it's good to keep in mind (e.g. my initial hunch was that when doing $tt::$rhs if $tt was a :path-like capture, then that could fail).

  • macro hygiene/transparency and Spans

    Consider, for instance:

    macro_rules! let_x_42 {() => (
        let x = 42;
    )}
    
    let_x_42!();
    let y = x;
    

    This expands to code that, if copy-pasted, does not fail to compile.

    Basically the name x that the macro uses is "tainted" to be different from any x used outside the macro body, precisely to avoid misinteractions when the macro needs to define helper stuff such as variables.

    And it turns out that this is the same thing that has happened with your from_byte identifier: your code was emitting a from_byte with private hygiene / a def_site() span, which is something that normally never happens for method names when using classic macros, or classic proc-macros (i.e., when not using the unstable ::proc_macro::quote! macro). See this comment: https://github.com/rust-lang/rust/issues/54722#issuecomment-696510769

    And so the from_byte identifier is being "tainted" in a way that allows Rust to make it invisible to code not belonging to that same macro expansion, such as the code in your fn main.

在这一点上,解决方案很简单:使用明确的非def_site()Span(例如Span::call_site()或更好的是Span::mixed_site(),以模仿macro_rules!宏的规则)锻造一个带有from_bytes Identifier,以防止它被::proc_macro:: quote!使用的默认def_site() Span获取:
use ::proc_macro::Span;
// ...
let from_byte = TokenTree::from(Ident::new("from_byte", Span::mixed_site()));
res.extend(quote!(impl $tt {
//         use an interpolated ident rather than a "hardcoded one"
//         vvvvvvvvvv
    pub fn $from_byte(byte: u8) -> $tt {
        match byte {
            $match_arms
            _ => unreachable!()
        }
    }
}))

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接