手続き型マクロによって生成されたコードで衛生的な識別子を作成するにはどうすればよいですか?


8

宣言型(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;
  |

私のコントロールの外にある識別子から私の識別子に名前空間を付けるにはどのようなテクニックがありますか?


1
明白なアイデア(それが機能するかどうかわからない):宣言的なマクロを生成するprocマクロを記述し、それを呼び出すか?
trentcl

このユーティリティのLisp用語はgensymであり、明らかにそのためのクレートが少なくとも1つあります。ただし、実装はフランス語の回答とまったく同じです。
user4815162342

回答:


6

要約:安定したRustでは、まだ衛生的な識別子をprocマクロで使用できません。あなたの最善の策は、などの特に醜い名前を使用すること__your_crate_your_nameです。


を使用して識別子(特にf)を作成していquote!ます。これは確かに便利ですが、コンパイラが提供する実際のprocマクロAPIのヘルパーにすぎません。では、そのAPIを見て、識別子を作成する方法を見てみましょう!最後に、我々は必要なことは何私たちのprocのマクロ戻っだとして、。このようなトークンストリームを作成するにはどうすればよいですか?TokenStream

文字列から解析できます"let f = 3;".parse::<TokenStream>()。しかし、これは基本的に初期の解決策であり、現在は推奨されません。いずれにせよ、この方法で作成されたすべての識別子は非衛生的な方法で動作するため、これは問題を解決しません。

2番目の方法(quote!内部で使用)はTokenStreamTokenTreesの束を作成して手動で作成することです。1つの種類TokenTreeIdent(識別子)です。Identvia を作成できますnew

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

stringパラメータは自明であるが、spanパラメータが興味深い部分です!A Spanはソースコード内の何かの場所を格納し、通常はエラー報告に使用されます(rustcたとえば、スペルミスの変数名を指すために)。しかし、Rustコンパイラでは、スパンは位置情報以上のものを運びます:一種の衛生状態です!次の2つのコンストラクター関数を確認できますSpan

  • fn call_site() -> Span呼び出しサイトの衛生状態でスパンを作成します。これは「非衛生的」と呼ばれるもので、「コピーと貼り付け」に相当します。2つの識別子が同じ文字列を持っている場合、それらは互いに衝突またはシャドウします。

  • fn def_site() -> Span:これはあなたが求めているものです。技術的に呼ばれる定義サイトの衛生、これはあなたが「衛生的」と呼んでいます。定義した識別子とユーザーの識別子は異なるユニバースに存在し、衝突することはありません。あなたがドキュメントで見ることができるように、この方法はまだ不安定であり、したがって毎晩のコンパイラでのみ使用できます。残念!

本当に素晴らしい回避策はありません。明らかなのは、のような本当に醜い名前を使用すること__your_crate_some_variableです。少し簡単にするために、その識別子を一度作成して、その中で使用することができますquote!ここで少し良い解決策):

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

場合によっては、自分と衝突する可能性のあるユーザーのすべての識別子を検索して、衝突しない識別子をアルゴリズムで選択することもできます。これは実際にのためauto_impl行ったものであり、フォールバックの超醜い名前です。これは主に、生成されたドキュメントに超醜い名前が含まれないようにするためです。

それとは別に、あなたは本当に何もできないと思います。


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

何かが、私が生成した識別子と(不幸にも)一致する識別子をユーザーが渡せないようにしていますか?
Shepmaster

1
@Shepmaster私が推測する確率の法則
Boiethios

2
@Shepmaster v4 UUIDは128個のランダムビットで構成されるため、天文学的にありそうもないイベントです。正しくシードされたPRNGを使用すると、同じSHA1への2つのコミットが不運にもハッシュ化されてgitリポジトリが壊れるかどうかを尋ねるのと同じようになります。
user4815162342
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.