[Solved] Derive and proc_macro : Add field to an existing struct

Hi Rustceans :crab:,

I'm having trouble with proc_marcos and Derive. I'm trying to add a field on an existing struct with derive. But obviously my implementation doesn't work.

I already take a look at this question but without success.

Here's what I'm trying to achieve :

use proc_macro_issue_minimal_example::AddField;

#[derive(AddField)]
struct Foo {}

// Foo should be expanded to :
// struct Foo {
//  pub a: String
// }

let bar = Foo { a: "lorem ipsum".to_string()};

But the compiler throw an error :

error[E0560]: struct `Foo` has no field named `a`
  --> tests/01-shoud-add-struct-field.rs:17:25
   |
17 |         let bar = Foo { a: "lorem ipsum".to_string()};
   |                         ^ `Foo` does not have this field

Here's my current implementation :

use proc_macro::TokenStream;
use syn::{parse_macro_input, DeriveInput, parse::Parser};
use quote::quote;

#[proc_macro_derive(AddField, attributes(unimarc))]
pub fn derive(input: TokenStream) -> TokenStream {
    let mut ast = parse_macro_input!(input as DeriveInput);
    match &mut ast.data {
        syn::Data::Struct(ref mut struct_data) => {           
            match &mut struct_data.fields {
                syn::Fields::Named(fields) => {
                    fields
                        .named
                        .push(syn::Field::parse_named.parse2(quote! { pub a: String }).unwrap());
                }   
                _ => {
                    ()
                }
            }              
            
            // I tried
            // 
            // return quote! {
            //     #ast
            // }.into();
            //
            // But it fails with error :  `Foo` redefined here previous definition of the type `Foo` here
            //
            // So instead I return an empty TokenStream but the field is not added

            TokenStream::new()
        }
        _ => panic!("AddField has to be used with structs "),
    }
}

I created a minimal (not) working example with tests on GitHub : https://github.com/eonm-abes/proc-macro-issue-minimal-example

Thank you,
Best Regards

Derive macros are unable to change the passed struct, they only add other items. To change the struct, you have to use attribute macro and return a new struct definition, as you've tried to do here.

2 Likes

Thank you very much @Cerberuser :+1:
It make sense now !

For people interested by the working implementation :
[lib.rs]

use proc_macro::TokenStream;
use syn::{parse_macro_input, DeriveInput, parse::Parser};
use quote::quote;

#[proc_macro_attribute]
pub fn add_field(_args: TokenStream, input: TokenStream) -> TokenStream  {
    let mut ast = parse_macro_input!(input as DeriveInput);
    match &mut ast.data {
        syn::Data::Struct(ref mut struct_data) => {           
            match &mut struct_data.fields {
                syn::Fields::Named(fields) => {
                    fields
                        .named
                        .push(syn::Field::parse_named.parse2(quote! { pub a: String }).unwrap());
                }   
                _ => {
                    ()
                }
            }              
            
            return quote! {
                #ast
            }.into();
        }
        _ => panic!("`add_field` has to be used with structs "),
    }
}

[test.rs]

use proc_macro_issue_minimal_example::add_field;
#[add_field]
#[derive(Debug, Clone)]
struct Foo {}

let bar = Foo { a: "lorem ipsum".to_string()};

Now that we are at it, let's add some feedback :slightly_smiling_face:

Error handling

With procedural macros, panic!-king should be avoided (this is not different from Rust libraries). That is, a procedural macro should only panic! in case of some programming error, such as reaching a supposedly unreachable path.

This is for two reasons:

  • For people who are not acquainted with the world of procedural macros, seeing procedural macro panicked does sound a bit alarming;

  • In case of a panic, the error reporting mechanism of the compiler blames / points to (spans over) the macro itself, rather than the problematic input. Indeed, compare:

    error: custom attribute panicked
     --> src/main.rs:3:1
      |
    3 | #[attr]
      | ^^^^^^^
      |
      = help: message: `#[attr]` has to be used on `struct`s
    

    to:

    error: `#[attr]` has to be used on `struct`s
     --> src/main.rs:6:1
      |
    6 | enum Foo {}
      | ^^^^
    

So, in order to produce nice error messages, ::syn's Error type provides a very handy .to_compile_error().into() method that generates a "magic" TokenStream with a nice error message and pointing to / spanned over whatever Span was fed to the Error when constructing it.

Another way of producing these error messages is through the parse_macro_input! helper, by, ideally, providing it a ::syn type (or custom Parseable type) whereby all its instances are valid inputs to your macro.

In your case, for instance, this is not the case: you used a DeriveInput, which happily parses and accepts structs, enums, and unions. Another choice would have been to use an ItemStruct.

Handling the _args / attrs (first parameter of a proc_macro_attribute)

This matches the TokenStream (if any), present inside the attribute's parenthesis:

#[some_proc_macro_attr(<optional extra stuff here>)]

Sadly, if you leave it ignored like you did, any user can go and put crazy stuff there. This means that deciding, later on, to accept a parameter, will be a breaking change the moment parsing that parameter can fail (or worse, if it doesn't fail, it may lead the proc-macro to behave differently depending on whether the <optional extra stuff here> that the user fed is ignored or taken into account, which, within a semver compatible universe is just asking for trouble).

So, in order to conservatively prevent any user from misusing that part of the macro's input, you can simply parse it as Nothing:

let _ = parse_macro_input!(args as parse::Nothing);
1 Like

Thank you @Yandros for such detailed answer :clap:

I used panic for convenience in my example, but I have to admit that I hadn't yet looked at the error handling part of the syn crate. I'll take a look at .to_compile_error().into(). Thank you for pointing that out!

If I understand you well I can use ItemStruct to reduce boilerplate code (that I used to handle all variants produced by DeriveInput), and also to automatically return an error when the add_field attribute is not applied to a Struct. Like this:

#[proc_macro_attribute]
pub fn add_field(args: TokenStream, input: TokenStream) -> TokenStream {
    let mut item_struct = parse_macro_input!(input as ItemStruct);
    let _ = parse_macro_input!(args as parse::Nothing);

    if let syn::Fields::Named(ref mut fields) = item_struct.fields {
        fields.named.push(
            syn::Field::parse_named
                .parse2(quote! { pub a: String })
                .unwrap(),
        );
    }

    return quote! {
        #item_struct
    }
    .into();
}

But It's possible to go further by parsing item_struct.fields into a syn::Fields::Named instead of using a if let statement as I did? I tried to use parse but without success.

The unimarc attribute in my first example is a left over of my real code :sweat_smile:. But I will try to keep in mind the parse::Nothing trick which seems very useful, especially with #[proc_macro_attribute].

I updated the code thanks to @Yandros's advice.

2 Likes