Al escribir una macro_rules!
macro declarativa ( ), obtenemos automáticamente la higiene macro . En este ejemplo, declaro una variable nombrada f
en la macro y paso un identificador f
que se convierte en una variable local:
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);
}
Este código se compila, pero si observa el código parcialmente expandido, puede ver que hay un conflicto aparente:
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)
}
}
Estoy escribiendo el equivalente de esta macro declarativa como una macro de procedimiento, pero no sé cómo evitar posibles conflictos de nombres entre los identificadores proporcionados por el usuario y los identificadores creados por mi macro. Hasta donde puedo ver, el código generado no tiene noción de higiene y es solo una cadena:
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"
Con Rust 1.40, esto produce el error del compilador:
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;
|
¿Qué técnicas existen para el espacio de nombres de mis identificadores de identificadores fuera de mi control?
fuente
Respuestas:
Resumen : aún no puede utilizar identificadores higiénicos con macros de proceso en Rust estable. Su mejor opción es usar un nombre particularmente feo como
__your_crate_your_name
.Está creando identificadores (en particular
f
) mediantequote!
. Esto es ciertamente conveniente, pero es solo una ayuda para la macro API de proceso real que ofrece el compilador . ¡Echemos un vistazo a esa API para ver cómo podemos crear identificadores! Al final necesitamos unTokenStream
, ya que eso es lo que devuelve nuestra macro de proceso. ¿Cómo podemos construir una secuencia de tokens?Podemos analizarlo desde una cadena, por ejemplo
"let f = 3;".parse::<TokenStream>()
. Pero esta fue básicamente una solución temprana y ahora se desaconseja. En cualquier caso, todos los identificadores creados de esta manera se comportan de manera no higiénica, por lo que esto no resolverá su problema.La segunda forma (que
quote!
usa debajo del capó) es crear unTokenStream
manual creando un grupo deTokenTree
s . Un tipo deTokenTree
es unIdent
(identificador). Podemos crear unaIdent
víanew
:El
string
parámetro se explica por sí mismo, ¡pero elspan
parámetro es la parte interesante! ASpan
almacena la ubicación de algo en el código fuente y generalmente se usa para informar errores (pararustc
señalar el nombre de la variable mal escrita, por ejemplo). Pero en el compilador de Rust, los tramos llevan más que información de ubicación: ¡el tipo de higiene! Podemos ver dos funciones de constructor paraSpan
:fn call_site() -> Span
: crea un espacio con la higiene del sitio de la llamada . Esto es lo que llamas "antihigiénico" y es equivalente a "copiar y pegar". Si dos identificadores tienen la misma cadena, colisionarán o se sombrearán entre sí.fn def_site() -> Span
: esto es lo que buscas. Técnicamente llamado higiene del sitio de definición , esto es lo que usted llama "higiénico". Los identificadores que define y los de su usuario viven en universos diferentes y nunca colisionarán. Como puede ver en los documentos, este método todavía es inestable y, por lo tanto, solo se puede usar en un compilador nocturno. ¡Gorrón!No hay soluciones realmente buenas. La obvia es usar un nombre realmente feo como
__your_crate_some_variable
. Para hacerlo un poco más fácil, puede crear ese identificador una vez y usarlo dentroquote!
( una solución un poco mejor aquí ):A veces, incluso puede buscar a través de todos los identificadores del usuario que podrían colisionar con los suyos y luego simplemente elegir algorítmicamente un identificador que no colisione. Esto es en realidad lo que hicimos
auto_impl
, con un nombre super feo alternativo. Esto fue principalmente para mejorar la documentación generada por tener nombres súper feos.Aparte de eso, me temo que realmente no puedes hacer nada.
fuente
Puedes gracias a un UUID:
fuente