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

标签 rust macros hygiene 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);
}

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

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

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

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,这会产生编译器错误:

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

存在哪些技术可以将我的标识符与我无法控制的标识符命名空间?

最佳答案

总结:您还不能在稳定的 Rust 上将卫生标识符与 proc 宏一起使用。最好的选择是使用一个特别难看的名称,例如 __your_crate_your_name

<小时/>

您正在使用 f 创建标识符(特别是 quote! ) 。这固然很方便,但它只是 the actual proc macro API the compiler offers 周围的一个 helper 。 。那么让我们看一下该 API,看看如何创建标识符!最终我们需要a TokenStream ,因为这就是我们的 proc 宏返回的内容。我们如何构建这样的 token 流?

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

第二种方法(quote! 在后台使用)是创建一个 TokenStream手动创建一堆 TokenTree s 。一种TokenTree Ident (标识符)。我们可以创建一个Ident通过new :

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

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

  • fn call_site() -> Span :创建一个具有调用站点卫生的跨度。这就是你所说的“不卫生”,相当于“复制粘贴”。如果两个标识符具有相同的字符串,它们将相互碰撞或相互遮挡。

  • fn def_site() -> Span : 这就是你所追求的。技术上称为定义站点卫​​生,这就是您所说的“卫生”。您定义的标识符和用户的标识符位于不同的宇宙中,并且永远不会发生冲突。正如您在文档中看到的,此方法仍然不稳定,因此只能在夜间编译器上使用。真糟糕!

没有真正好的解决方法。最明显的就是使用一个非常难看的名字,比如 __your_crate_some_variable 。为了让您更轻松,您可以创建该标识符一次并在 quote! 内使用它。 (slightly better solution here):

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

有时,您甚至可以搜索可能与您的用户冲突的所有标识符,然后简单地通过算法选择不冲突的标识符。这实际上就是we did for auto_impl ,有一个后备 super 丑陋的名字。这主要是为了改进生成的文档,避免其中包含 super 难看的名称。

除此之外,恐怕你什么也做不了。

关于rust - 如何在程序宏生成的代码中创建卫生标识符?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/59618213/

相关文章:

struct - 如何为 defstruct 的递归 "slot-value"编写宏?

macros - 将宏变量传递给函数进行插值

rust - 在 Rust "macro_rules"宏中的调用站点使用本地绑定(bind)

error-handling - Rust 中有哪些不同的错误类型?

rust - 为什么将函数移至默认特征方法会导致借入错误?

generics - 如何在具有通用参数的结构上实现非通用特征

rust - Rust 的词汇语法是正则的、上下文无关的还是上下文敏感的?

algorithm - 计算鼠标与文本输入算法的 Big O 时间复杂度

c++ - 在不使用宏的情况下减少语法 "noise"

rust - 如何关闭宏观卫生?