Search code examples
genericsruststructrust-proc-macros

How can I use derive macro on a generic struct?


I'm working on implementing dynamic listeners for the communication layer of my embedded program. I want to store a Vec<ReaderContext<T>> in my Reader structure. Since T can change, I need to hide it behind a trait: Vec<Box<dyn AnyReaderContext>> (following this SO question.)

I tried to follow the how-to-write-a-custom-derive-macro guide of the rust book, and I have the following minimal code:

  • Crate any_reader_context

    # Cargo.toml
    [package]
    name = "any_reader_context"
    version = "0.1.0"
    edition = "2021"
    
    // lib.rs
    pub trait AnyReaderContext {
        fn my_derived_function();
    }
    
  • Crate any_reader_context_derive

    # Cargo.toml
    [package]
    name = "any_reader_context_derive"
    version = "0.1.0"
    edition = "2021"
    
    [lib]
    proc-macro = true
    
    [dependencies]
    syn = "2.0.26"
    quote = "1.0"
    
    // lib.rs
    use proc_macro::TokenStream;
    use quote::quote;
    use syn;
    
    #[proc_macro_derive(AnyReaderContext)]
    pub fn any_reader_context_derive(input: TokenStream) -> TokenStream {
        let ast = syn::parse(input).unwrap();
    
        // Build the trait implementation
        impl_any_reader_context(&ast)
    }
    
    fn impl_any_reader_context(ast: &syn::DeriveInput) -> TokenStream {
        let name = &ast.ident;
        let gen = quote! {
            impl AnyReaderContext for #name {
                fn my_derived_function() {
                    println!("Hello, Macro! My name is {}!", stringify!(#name));
                }
            }
        };
        gen.into()
    }
    
  • Main crate

    # Cargo.toml
    [package]
    name = "any_reader_test"
    version = "0.1.0"
    edition = "2021"
    
    [dependencies]
    any_reader_context = { path = "../any_reader_context" }
    any_reader_context_derive = { path = "../any_reader_context_derive" }
    
    // main.rs
    use any_reader_context_derive::AnyReaderContext;
    use any_reader_context::AnyReaderContext;
    
    #[derive(AnyReaderContext)]
    pub struct Reader<T> {
        value: T
    }
    
    impl<T> Reader<T> {
        fn new(value: T) -> Self {
            Self {value}
        }
    }
    
    fn main() {
        let readers: Vec<Box<dyn AnyReaderContext>> = vec![];
        readers.push(Reader::<u32>::new(42));
    }
    

any_reader_context and any_reader_context_derive crates compile properly.

Main crate has the following cryptic compilation error:

$ cargo build
   Compiling any_reader_context v0.1.0 (rust-sandbox/any_reader_context)
   Compiling any_reader_test v0.1.0 (rust-sandbox/any_reader_test)
error[E0107]: missing generics for struct `Reader`
 --> src/main.rs:5:12
  |
5 | pub struct Reader<T> {
  |            ^^^^^^ expected 1 generic argument
  |
note: struct defined here, with 1 generic parameter: `T`
 --> src/main.rs:5:12
  |
5 | pub struct Reader<T> {
  |            ^^^^^^ -
help: add missing generic argument
  |
5 | pub struct Reader<T><T> {
  |                  +++

I tried to add a second <T>, but obviously, the compiler didn't like it:

$ cargo build
   Compiling any_reader_test v0.1.0 (/home/tcravic/workspace/avionics/prototypes/rust-sandbox/any_reader_test)
error: expected `where`, `{`, `(`, or `;` after struct name, found `<`
 --> src/main.rs:5:21
  |
5 | pub struct Reader<T><T> {
  |                     ^ expected `where`, `{`, `(`, or `;` after struct name

Can anyone help with this one?


Solution

  • You completely ignore the generics which you simply can't do. You could simply forward them from the ast :

    fn impl_any_reader_context(ast: &syn::DeriveInput) -> TokenStream {
        let name = &ast.ident;
        let (impl_generics, type_generics, where_clause) = ast.generics.split_for_impl();
        let gen = quote! {
            impl #impl_generics AnyReaderContext for #name #type_generics #where_clause {
                fn my_derived_function() {
                    println!("Hello, Macro! My name is {}!", stringify!(#name));
                }
            }
        };
        gen.into()
    }