简体   繁体   English

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

[英]How can I create hygienic identifiers in code generated by procedural macros?

When writing a declarative ( macro_rules! ) macro, we automatically get macro hygiene .在编写声明性( macro_rules! )宏时,我们会自动获得宏卫生 In this example, I declare a variable named f in the macro and pass in an identifier f which becomes a local variable:在这个例子中,我在宏中声明了一个名为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);
}

This code compiles, but if you look at the partially-expanded code, you can see that there's an apparent conflict:这段代码可以编译,但如果你查看部分展开的代码,你会发现存在明显的冲突:

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

I am writing the equivalent of this declarative macro as a procedural macro, but do not know how to avoid potential name conflicts between the user-provided identifiers and identifiers created by my macro.我正在编写这个声明性宏的等价物作为过程宏,但不知道如何避免用户提供的标识符和我的宏创建的标识符之间的潜在名称冲突。 As far as I can see, the generated code has no notion of hygiene and is just a string:据我所知,生成的代码没有卫生概念,只是一个字符串:

src/main.rs 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);
}

Cargo.toml Cargo.toml

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

[dependencies]
my_derive = { path = "my_derive" }

my_derive/src/lib.rs 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)
}

my_derive/Cargo.toml 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"

With Rust 1.40, this produces the compiler error:在 Rust 1.40 中,这会产生编译器错误:

error[E0599]: no method named `write_fmt` found for type `&std::string::String` in the current scope
 --> src/main.rs:3:10
  |
3 | #[derive(MyDerive)]
  |          ^^^^^^^^ method not found in `&std::string::String`
  |
  = help: items from traits can only be used if the trait is in scope
  = note: this error originates in a macro outside of the current crate (in Nightly builds, run with -Z external-macro-backtrace for more info)
help: the following trait is implemented but not in scope; perhaps add a `use` for it:
  |
1 | use std::fmt::Write;
  |

What techniques exist to namespace my identifiers from identifiers outside of my control?存在哪些技术可以将我的标识符命名为不受我控制的标识符?

Summary : you can't yet use hygienic identifiers with proc macros on stable Rust.总结:你还不能在稳定的 Rust 上使用带有 proc 宏的卫生标识符。 Your best bet is to use a particularly ugly name such as __your_crate_your_name .最好的办法是使用一个特别难看的名字,比如__your_crate_your_name


You are creating identifiers (in particular, f ) by using quote!您正在使用quote!创建标识符(特别是fquote! . . This is certainly convenient, but it's just a helper around the actual proc macro API the compiler offers .这当然很方便,但它只是编译器提供的实际 proc 宏 API 的辅助工具。 So let's take a look at that API to see how we can create identifiers!那么让我们来看看那个 API,看看我们如何创建标识符! In the end we need a TokenStream , as that's what our proc macro returns.最后我们需要一个TokenStream ,因为那是我们的 proc 宏返回的。 How can we construct such a token stream?我们如何构建这样的令牌流?

We can parse it from a string, eg "let f = 3;".parse::<TokenStream>() .我们可以从字符串中解析它,例如"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 ofTokenTree s .第二种方式(其quote!引擎盖下应用)是创建TokenStream通过创建一堆手动TokenTree小号 One kind of TokenTree is an Ident (identifier).一种TokenTreeIdent (标识符)。 We can create an Ident via new :我们可以通过new创建一个Ident

fn new(string: &str, span: Span) -> Ident

The string parameter is self explanatory, but the span parameter is the interesting part! string参数是不言自明的,但span参数是有趣的部分! 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).Span存储源代码中某些内容的位置,通常用于错误报告(例如,为了让rustc指向拼写错误的变量名称)。 But in the Rust compiler, spans carry more than location information: the kind of hygiene!但是在 Rust 编译器中,span 携带的不仅仅是位置信息:那种卫生! We can see two constructor functions for Span :我们可以看到Span两个构造函数:

  • fn call_site() -> Span : creates a span with call site hygiene . fn call_site() -> Span :创建一个带有呼叫站点卫生的跨度。 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. fn def_site() -> Span :这就是你所追求的。 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 .显而易见的是使用一个非常丑陋的名字,比如__your_crate_some_variable To make it a bit easier for you, you can create that identifier once and use it within quote!为了让您更轻松,您可以创建该标识符一次并在quote!使用它quote! ( slightly better solution here ): 这里稍微好一点的解决方案):

let ugly_name = quote! { __your_crate_some_variable };
quote! {
    let #ugly_name = 3;
    println!("{}", #ugly_name);
}

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.这实际上是我们为auto_impl ,带有一个超级丑陋的后备名称。 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.除此之外,恐怕你真的什么也做不了。

You can thanks to a UUID:你可以感谢一个 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())
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM