如何在程序宏生成的代码中创建卫生标识符? [英] How can I create hygienic identifiers in code generated by procedural macros?

查看:97
本文介绍了如何在程序宏生成的代码中创建卫生标识符?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

When writing a declarative (macro_rules!) macro, we automatically get macro hygiene. In this example, I declare a variable named f in the macro and pass in an identifier f which becomes a local variable:

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

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

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

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

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

[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"

使用Rust 1.40,这会产生编译器错误:

With Rust 1.40, this produces the compiler error:

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?

推荐答案

摘要:您还不能在稳定的Rust上将卫生标识符与proc宏一起使用.最好的选择是使用一个特别丑陋的名称,例如__your_crate_your_name.

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.

您正在通过使用f) > quote! .这当然很方便,但是它只是实际的proc宏API的帮助者优惠.因此,让我们看一下该API,以了解如何创建标识符!最后,我们需要 a TokenStream ,因为我们的proc宏返回什么.我们如何构造这样的令牌流?

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?

我们可以从字符串中解析它,例如"let f = 3;".parse::<TokenStream>().但这基本上是一个早期解决方案,现在不建议这样做.无论如何,以这种方式创建的所有标识符的行为都不卫生,因此无法解决您的问题.

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.

第二种方法(quote!在内部使用)是通过创建一堆 Ident (标识符).我们可以通过new创建一个Ident:

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

string参数不言自明,但是span参数是有趣的部分! Span 在源代码中存储内容的位置代码,通常用于错误报告(例如,为了使rustc指向拼写错误的变量名称).但是在Rust编译器中,跨区承载的不仅仅是位置信息:卫生!我们可以看到Span的两个构造函数:

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 :使用呼叫站点卫生创建跨度.这就是您所说的不卫生",等同于复制和粘贴".如果两个标识符具有相同的字符串,则它们将相互碰撞或相互阴影.

  • 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 :这就是您所追求的.从技术上讲,这是<定义地点卫生,这就是您所说的卫生".您定义的标识符和用户的标识符位于不同的Universe中,永远不会发生冲突.正如您在文档中看到的那样,该方法仍然不稳定,因此只能在每夜编译器中使用.糟糕!

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!

没有非常好的解决方法.显而易见的是使用一个非常丑陋的名称,例如__your_crate_some_variable.为方便起见,您只需创建一次该标识符,然后在quote!中使用它(此处的解决方案稍微好一点):

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

有时,您甚至可以搜索可能与您的用户发生冲突的所有用户标识符,然后简单地通过算法选择一个不会发生冲突的标识符.这实际上是我们为auto_impl 做的所做的工作,但有一个后备超级丑陋的名字.这主要是为了改进其中包含的超级丑陋名称的生成文档.

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.

这篇关于如何在程序宏生成的代码中创建卫生标识符?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆