如何在程序宏生成的代码中创建卫生标识符?

She*_*ter 8 macros hygiene rust rust-proc-macros

在编写声明性 ( macro_rules!) 宏时,我们会自动获得宏卫生。在这个例子中,我声明了一个f在宏中命名的变量并传入一个标识符f,该标识符成为一个局部变量:

macro_rules! decl_example {
    ($tname:ident, $mname:ident, ($($fstr:tt),*)) => {
        impl std::fmt::Display for $tname {
            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
                let Self { $mname } = self;
                write!(f, $($fstr),*)
            }
        }
    }
}

struct Foo {
    f: String,
}

decl_example!(Foo, f, ("I am a Foo: {}", f));

fn main() {
    let f = Foo {
        f: "with a member named `f`".into(),
    };
    println!("{}", f);
}
Run Code Online (Sandbox Code Playgroud)

这段代码可以编译,但是如果查看部分展开的代码,您会发现存在明显的冲突:

impl std::fmt::Display for Foo {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let Self { f } = self;
        write!(f, "I am a Foo: {}", f)
    }
}
Run Code Online (Sandbox Code Playgroud)

我正在编写这个声明性宏的等价物作为程序宏,但不知道如何避免用户提供的标识符和我的宏创建的标识符之间的潜在名称冲突。据我所知,生成的代码没有卫生概念,只是一个字符串:

src/main.rs

use my_derive::MyDerive;

#[derive(MyDerive)]
#[my_derive(f)]
struct Foo {
    f: String,
}

fn main() {
    let f = Foo {
        f: "with a member named `f`".into(),
    };
    println!("{}", f);
}
Run Code Online (Sandbox Code Playgroud)

Cargo.toml

[package]
name = "example"
version = "0.1.0"
edition = "2018"

[dependencies]
my_derive = { path = "my_derive" }
Run Code Online (Sandbox Code Playgroud)

my_derive/src/lib.rs

extern crate proc_macro;

use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput, Meta, NestedMeta};

#[proc_macro_derive(MyDerive, attributes(my_derive))]
pub fn my_macro(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);

    let name = input.ident;

    let attr = input.attrs.into_iter().filter(|a| a.path.is_ident("my_derive")).next().expect("No name passed");
    let meta = attr.parse_meta().expect("Unknown attribute format");
    let meta = match meta {
        Meta::List(ml) => ml,
        _ => panic!("Invalid attribute format"),
    };
    let meta = meta.nested.first().expect("Must have one path");
    let meta = match meta {
        NestedMeta::Meta(Meta::Path(p)) => p,
        _ => panic!("Invalid nested attribute format"),
    };
    let field_name = meta.get_ident().expect("Not an ident");

    let expanded = quote! {
        impl std::fmt::Display for #name {
            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
                let Self { #field_name } = self;
                write!(f, "I am a Foo: {}", #field_name)
            }
        }
    };

    TokenStream::from(expanded)
}
Run Code Online (Sandbox Code Playgroud)

my_derive/Cargo.toml

[package]
name = "my_derive"
version = "0.1.0"
edition = "2018"

[lib]
proc-macro = true

[dependencies]
syn = "1.0.13"
quote = "1.0.2"
proc-macro2 = "1.0.7"
Run Code Online (Sandbox Code Playgroud)

在 Rust 1.40 中,这会产生编译器错误:

macro_rules! decl_example {
    ($tname:ident, $mname:ident, ($($fstr:tt),*)) => {
        impl std::fmt::Display for $tname {
            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
                let Self { $mname } = self;
                write!(f, $($fstr),*)
            }
        }
    }
}

struct Foo {
    f: String,
}

decl_example!(Foo, f, ("I am a Foo: {}", f));

fn main() {
    let f = Foo {
        f: "with a member named `f`".into(),
    };
    println!("{}", f);
}
Run Code Online (Sandbox Code Playgroud)

存在哪些技术可以将我的标识符命名为不受我控制的标识符?

Luk*_*odt 7

Summary: you can't yet use hygienic identifiers with proc macros on stable Rust. Your best bet is to use a particularly ugly name such as __your_crate_your_name.


You are creating identifiers (in particular, f) by using quote!. This is certainly convenient, but it's just a helper around the actual proc macro API the compiler offers. So let's take a look at that API to see how we can create identifiers! In the end we need a TokenStream, as that's what our proc macro returns. How can we construct such a token stream?

We can parse it from a string, e.g. "let f = 3;".parse::<TokenStream>(). But this was basically an early solution and is discouraged now. In any case, all identifiers created this way behave in a non-hygienic manner, so this won't solve your problem.

The second way (which quote! uses under the hood) is to create a TokenStream manually by creating a bunch of TokenTrees. One kind of TokenTree is an Ident (identifier). We can create an Ident via new:

fn new(string: &str, span: Span) -> Ident
Run Code Online (Sandbox Code Playgroud)

The string parameter is self explanatory, but the span parameter is the interesting part! A Span stores the location of something in the source code and is usually used for error reporting (in order for rustc to point to the misspelled variable name, for example). But in the Rust compiler, spans carry more than location information: the kind of hygiene! We can see two constructor functions for Span:

  • fn call_site() -> Span: creates a span with call site hygiene. This is what you call "unhygienic" and is equivalent to "copy and pasting". If two identifiers have the same string, they will collide or shadow each other.

  • fn def_site() -> Span: this is what you are after. Technically called definition site hygiene, this is what you call "hygienic". The identifiers you define and the ones of your user live in different universes and won't ever collide. As you can see in the docs, this method is still unstable and thus only usable on a nightly compiler. Bummer!

There are no really great workarounds. The obvious one is to use a really ugly name like __your_crate_some_variable. To make it a bit easier for you, you can create that identifier once and use it within quote! (slightly better solution here):

let ugly_name = quote! { __your_crate_some_variable };
quote! {
    let #ugly_name = 3;
    println!("{}", #ugly_name);
}
Run Code Online (Sandbox Code Playgroud)

Sometimes you can even search through all identifiers of the user that could collide with yours and then simply algorithmically chose an identifier that does not collide. This is actually what we did for auto_impl, with a fallback super ugly name. This was mainly to improve the generated documentation from having super ugly names in it.

Apart from that, I'm afraid you cannot really do anything.


Fre*_*ios 5

你可以感谢一个 UUID:

fn generate_unique_ident(prefix: &str) -> Ident {
    let uuid = uuid::Uuid::new_v4();
    let ident = format!("{}_{}", prefix, uuid).replace('-', "_");

    Ident::new(&ident, Span::call_site())
}
Run Code Online (Sandbox Code Playgroud)

  • @Shepmaster 这是一个天文上不可能的事件,因为 v4 UUID 由 128 个随机位组成。有了正确种子的 PRNG,它应该类似于询问您的 git 存储库是否会被两次提交不幸地散列到相同的 SHA1 所破坏。 (5认同)
  • @Shepmaster 我猜概率定律 (4认同)
  • 有时这会破坏增量重新编译吗?编译中随机事件的想法让我有点害怕。 (2认同)