lloydmeta
lloydmeta

Reputation: 1339

Preserving struct field visibility with a macro

I'm trying to write a Rust macro that allows me to make use of the field names and types of a struct declaration, but I still need to emit the struct.

I've got it working with optional attributes, visibility of the struct (thanks to The Little Book of Rust Macros), but can't figure out how to deal with the optional presence of pub in the individual fields.

So far I've got:

macro_rules! with_generic {
    ($(#[$struct_meta:meta])*
    pub struct $name:ident { $($fname:ident : $ftype:ty), *}
    ) => {
        with_generic![(pub) $(#[$struct_meta])* struct $name {$($fname: $ftype) ,*}];
    };

    ($(#[$struct_meta:meta])*
    struct $name:ident { $($fname:ident : $ftype:ty), *}
    ) => {
        with_generic![() $(#[$struct_meta])* struct $name {$($fname: $ftype), *}];
    };

    (
    ($($vis:tt)*)
    $(#[$struct_meta:meta])*
    struct $name:ident { $($fname:ident : $ftype:ty), *}
    ) => {
        // emit the struct here
        $(#[$struct_meta])*
        $($vis)* struct $name {
            $($fname: $ftype,)*
        }

        // I work with fname and ftypes here
    }
}

And it works with something like

with_generic! {
    #[derive(PartialEq, Eq, Debug)]
    pub struct Person {
        first_name: String,
        last_name:  String
    }
}

or

with_generic! {
    #[derive(PartialEq, Eq, Debug)]
    struct PrivatePerson {
        first_name: String,
        last_name:  String
    }
}

but doesn't work with

with_generic! {
    #[derive(PartialEq, Eq, Debug)]
    struct MixedPerson {
        pub first_name: String,
        last_name:  String
    }
}

I'd like to get some help on how to make the macro work with that last case. I feel like I might be missing something basic here, such as the type used for binding visibility. If there's a way to bind the whole struct tree while getting the field names and types, that would also be fine.

I'd also like to learn how to get it to work with structs that have lifetime parameters, but maybe that should be a separate question.

Upvotes: 3

Views: 1935

Answers (3)

trent
trent

Reputation: 28005

Since Rust 1.30, you can match visibility keywords with the vis specifier. A vis metavariable will match nothing if there is no visibility keyword to match, so you don't even need to use it inside $()*. This change makes with_generic vastly simpler:

macro_rules! with_generic {
    ($(#[$struct_meta:meta])*
    $sv:vis struct $name:ident { $($fv:vis $fname:ident : $ftype:ty), *}
    ) => {
        // emit the struct here
        $(#[$struct_meta])*
        $sv struct $name {
            $($fv $fname: $ftype,)*
        }
        // do whatever else you need here
    }
}

Upvotes: 2

lloydmeta
lloydmeta

Reputation: 1339

Rust 1.15 was officially released shortly after I asked this question and brings procedural macros (custom derive) support like @DK. has said.

Going forward, I think custom derives w/ syn and quote will be the standard way of doing this kind of thing and side-steps this issue completely since you no longer need to manually re-emit the struct.

Upvotes: 1

DK.
DK.

Reputation: 59095

You can't. At least, not with a single, non-recursive rule. This is because Rust doesn't have a macro matcher for visibility.

The parse-macros crate contains a parse_struct! macro that shows the work necessary to completely parse a struct definition. Short version: you need to parse each field individually, with one rule for each of "has pub" and "doesn't have pub".

I'd also just note that there's another case your macro doesn't yet account for: attributes on the fields, which is needed for doc-comments on them to work.

Quite soon, macros 1.1 should be stabilised, which might provide an easier approach (assuming you can express your macro as a derivation, and don't care about older versions of Rust).

Upvotes: 2

Related Questions