Как я могу создать гигиенические идентификаторы в коде, созданном с помощью процедурных макросов?
При написании декларативного (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;
|
Какие существуют методы для создания пространства имен моих идентификаторов из идентификаторов вне моего контроля?
2 ответа
Резюме: вы пока не можете использовать гигиенические идентификаторы с макросами proc в стабильной версии Rust. Лучше всего использовать особенно уродливое имя, например__your_crate_your_name
.
Вы создаете идентификаторы (в частности, f
) используя quote!
. Это, конечно, удобно, но это всего лишь помощник в отношении реального API макроса proc, который предлагает компилятор. Итак, давайте посмотрим на этот API, чтобы увидеть, как мы можем создавать идентификаторы! В конце концов, мы должны ATokenStream
, так как это то, что возвращает наш макрос proc. Как мы можем создать такой токен-поток?
Мы можем разобрать его из строки, например "let f = 3;".parse::<TokenStream>()
. Но это было в основном раннее решение, и сейчас оно не приветствуется. В любом случае все идентификаторы, созданные таким образом, ведут себя негигиенично, так что это не решит вашу проблему.
Второй способ (который quote!
используется под капотом) заключается в создании TokenStream
вручную, создав кучу TokenTree
с. Один вид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!
(здесь немного лучшее решение):
let ugly_name = quote! { __your_crate_some_variable };
quote! {
let #ugly_name = 3;
println!("{}", #ugly_name);
}
Иногда вы даже можете выполнить поиск по всем идентификаторам пользователя, который может столкнуться с вашим, а затем просто алгоритмически выбрать идентификатор, который не конфликтует. Это собственно то, что мы сделали дляauto_impl
, с запасным супер уродливым именем. В основном это было сделано для того, чтобы улучшить сгенерированную документацию, избавившись от ужасных имен.
Кроме того, боюсь, ты ничего не сможешь сделать.
Благодаря 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())
}