Làm cách nào tôi có thể tạo các mã định danh hợp vệ sinh trong mã được tạo bởi các macro thủ tục?


8

Khi viết macro_rules!macro khai báo ( ), chúng tôi sẽ tự động nhận vệ sinh macro . Trong ví dụ này, tôi khai báo một biến có tên ftrong macro và truyền vào một mã định danh ftrở thành biến cục bộ:

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

Mã này biên dịch, nhưng nếu bạn nhìn vào mã được mở rộng một phần, bạn có thể thấy rằng có một xung đột rõ ràng:

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

Tôi đang viết tương đương với macro khai báo này dưới dạng macro thủ tục, nhưng không biết làm thế nào để tránh xung đột tên tiềm năng giữa các định danh và định danh do người dùng cung cấp được tạo bởi macro của tôi. Theo như tôi có thể thấy, mã được tạo ra không có khái niệm về vệ sinh và chỉ là một chuỗi:

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

Vận chuyển hàng hóa

[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"

Với Rust 1.40, điều này tạo ra lỗi trình biên dịch:

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

Những kỹ thuật tồn tại để không gian tên định danh của tôi từ định danh ngoài tầm kiểm soát của tôi?


1
Ý tưởng rõ ràng (không biết nếu nó hoạt động): viết một macro macro tạo ra một khai báo, sau đó gọi nó là gì?
trentcl

Thuật ngữ Lisp cho tiện ích này là gensym , và rõ ràng có ít nhất một thùng cho điều đó . Tuy nhiên, việc thực hiện hoàn toàn giống như trong câu trả lời của Pháp.
dùng4815162342

Câu trả lời:


6

Tóm tắt : bạn chưa thể sử dụng số nhận dạng hợp vệ sinh với macro Proc trên Rust ổn định. Đặt cược tốt nhất của bạn là sử dụng một tên đặc biệt xấu như __your_crate_your_name.


Bạn đang tạo định danh (cụ thể, f) bằng cách sử dụng quote!. Điều này chắc chắn là thuận tiện, nhưng nó chỉ là một người trợ giúp xung quanh API macro macro thực tế mà trình biên dịch cung cấp . Vì vậy, hãy xem API đó để xem cách chúng tôi có thể tạo định danh! Cuối cùng, chúng ta cần mộtTokenStream , vì đó là những gì mà macro macro của chúng ta trả về. Làm thế nào chúng ta có thể xây dựng một luồng mã thông báo như vậy?

Chúng ta có thể phân tích nó từ một chuỗi, ví dụ "let f = 3;".parse::<TokenStream>(). Nhưng về cơ bản, đây là một giải pháp ban đầu và không được khuyến khích. Trong mọi trường hợp, tất cả các định danh được tạo theo cách này hoạt động theo cách không hợp vệ sinh, vì vậy điều này sẽ không giải quyết vấn đề của bạn.

Cách thứ hai ( quote!sử dụng dưới mui xe) là tạo TokenStreamthủ công bằng cách tạo một bó TokenTrees . Một loại TokenTreelà một Ident(định danh). Chúng tôi có thể tạo Identthông qua new:

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

Các stringtham số là tự giải thích, nhưng spantham số là phần thú vị! A Spanlưu trữ vị trí của một cái gì đó trong mã nguồn và thường được sử dụng để báo cáo lỗi ( rustcví dụ để trỏ đến tên biến sai chính tả). Nhưng trong trình biên dịch Rust, các nhịp mang nhiều hơn thông tin vị trí: loại vệ sinh! Chúng ta có thể thấy hai hàm tạo cho Span:

  • fn call_site() -> Span: tạo một nhịp với vệ sinh trang web cuộc gọi . Đây là những gì bạn gọi là "mất vệ sinh" và tương đương với "sao chép và dán". Nếu hai định danh có cùng một chuỗi, chúng sẽ va chạm hoặc tạo bóng cho nhau.

  • fn def_site() -> Span: đây là những gì bạn đang theo đuổi. Về mặt kỹ thuật được gọi là vệ sinh trang web định nghĩa , đây là những gì bạn gọi là "vệ sinh". Các định danh bạn xác định và những người dùng của bạn sống trong các vũ trụ khác nhau và sẽ không bao giờ va chạm. Như bạn có thể thấy trong các tài liệu, phương pháp này vẫn không ổn định và do đó chỉ có thể sử dụng được trên trình biên dịch hàng đêm. Bummer!

Không có cách giải quyết thực sự tuyệt vời. Một điều hiển nhiên là sử dụng một cái tên thực sự xấu xí như thế nào __your_crate_some_variable. Để giúp bạn dễ dàng hơn một chút, bạn có thể tạo định danh đó một lần và sử dụng nó trong quote!( giải pháp tốt hơn một chút ở đây ):

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

Đôi khi, bạn thậm chí có thể tìm kiếm thông qua tất cả các số nhận dạng của người dùng có thể va chạm với bạn và sau đó chỉ đơn giản là chọn thuật toán một số nhận dạng không va chạm. Đây thực sự là những gì chúng tôi đã làmauto_impl , với một cái tên siêu xấu xí dự phòng. Điều này chủ yếu là để cải thiện tài liệu được tạo ra từ việc có những cái tên siêu xấu xí trong đó.

Ngoài ra, tôi sợ bạn thực sự không thể làm gì.


5

Bạn có thể nhờ một 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())
}

Có điều gì ngăn người dùng chuyển qua một mã định danh mà (không) may mắn khớp với mã định danh mà tôi đã tạo không?
Người quản lý

1
@Shepmaster Luật xác suất tôi đoán
Boiethios

2
@Shepmaster Đó là một sự kiện không thể thực hiện được về mặt thiên văn, vì UUID v4 bao gồm 128 bit ngẫu nhiên. Với một PRNG được gieo hạt chính xác, sẽ giống như hỏi xem liệu git repo của bạn có thể bị phá vỡ bởi hai cam kết băm không may cho cùng một SHA1 hay không.
dùng4815162342
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.