Beim Schreiben eines deklarativen ( macro_rules!
) Makros erhalten wir automatisch eine Makrohygiene . In diesem Beispiel deklariere ich eine f
im Makro benannte Variable und übergebe einen Bezeichner, f
der zu einer lokalen Variablen wird:
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);
}
Dieser Code wird kompiliert, aber wenn Sie sich den teilweise erweiterten Code ansehen, können Sie feststellen, dass ein offensichtlicher Konflikt vorliegt:
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)
}
}
Ich schreibe das Äquivalent dieses deklarativen Makros als prozedurales Makro, weiß jedoch nicht, wie ich potenzielle Namenskonflikte zwischen den vom Benutzer angegebenen Bezeichnern und den von meinem Makro erstellten Bezeichnern vermeiden kann. Soweit ich sehen kann, hat der generierte Code keine Vorstellung von Hygiene und ist nur eine Zeichenfolge:
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"
Mit Rust 1.40 erzeugt dies den Compilerfehler:
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;
|
Welche Techniken gibt es, um meine Bezeichner von Bezeichnern außerhalb meiner Kontrolle zu benennen?
quelle
Antworten:
Zusammenfassung : Sie können noch keine hygienischen Kennungen mit Proc-Makros auf stabilem Rost verwenden. Am besten verwenden Sie einen besonders hässlichen Namen wie
__your_crate_your_name
.Sie erstellen Bezeichner (insbesondere
f
) mithilfe vonquote!
. Dies ist sicherlich praktisch, aber es ist nur ein Hilfsmittel für die eigentliche Proc-Makro-API, die der Compiler anbietet . Schauen wir uns also diese API an, um zu sehen, wie wir Bezeichner erstellen können! Am Ende brauchen wir einTokenStream
, denn das ist es, was unser Proc-Makro zurückgibt. Wie können wir einen solchen Token-Stream erstellen?Wir können es aus einer Zeichenfolge analysieren, z
"let f = 3;".parse::<TokenStream>()
. Dies war jedoch im Grunde eine frühe Lösung und wird jetzt nicht empfohlen. In jedem Fall verhalten sich alle auf diese Weise erstellten Kennungen nicht hygienisch, sodass Ihr Problem dadurch nicht gelöst wird.Die zweite Möglichkeit (die
quote!
unter der Haube verwendet wird) besteht darin, eineTokenStream
manuell zu erstellen, indem eine Reihe vonTokenTree
s erstellt wird . Eine Art vonTokenTree
ist einIdent
(Bezeichner). Wir können einIdent
Via erstellennew
:Der
string
Parameter ist selbsterklärend, aber derspan
Parameter ist der interessante Teil! ASpan
speichert die Position von etwas im Quellcode und wird normalerweise für die Fehlerberichterstattung verwendet (um beispielsweiserustc
auf den falsch geschriebenen Variablennamen zu verweisen). Im Rust-Compiler enthalten die Bereiche jedoch mehr als nur Standortinformationen: die Art der Hygiene! Wir können zwei Konstruktorfunktionen sehen fürSpan
:fn call_site() -> Span
: Erstellt eine Spanne mit Anrufstellenhygiene . Dies ist das, was Sie als "unhygienisch" bezeichnen und entspricht "Kopieren und Einfügen". Wenn zwei Bezeichner dieselbe Zeichenfolge haben, kollidieren sie oder beschatten sich gegenseitig.fn def_site() -> Span
: das ist was du suchst. Technisch als Definition Site Hygiene bezeichnet , ist dies das, was Sie als "hygienisch" bezeichnen. Die von Ihnen definierten Bezeichner und die Ihres Benutzers leben in verschiedenen Universen und werden niemals kollidieren. Wie Sie in den Dokumenten sehen können, ist diese Methode immer noch instabil und daher nur auf einem nächtlichen Compiler verwendbar. Schade!Es gibt keine wirklich guten Problemumgehungen. Das Offensichtliche ist, einen wirklich hässlichen Namen wie zu verwenden
__your_crate_some_variable
. Um es Ihnen ein bisschen einfacher zu machen, können Sie diesen Bezeichner einmal erstellen und darin verwendenquote!
( etwas bessere Lösung hier ):Manchmal können Sie sogar alle Bezeichner des Benutzers durchsuchen, die mit Ihren kollidieren könnten, und dann einfach algorithmisch einen Bezeichner auswählen, der nicht kollidiert. Dies ist eigentlich das, wofür wir es getan haben
auto_impl
, mit einem super hässlichen Fallback-Namen. Dies diente hauptsächlich dazu, die generierte Dokumentation zu verbessern, indem sie super hässliche Namen enthält.Abgesehen davon fürchte ich, dass Sie nichts wirklich tun können.
quelle
Sie können dank einer UUID:
quelle