Search code examples
ruststructmacrosrust-macros

Expose struct generated from quote macro without appearing out of nowhere


How can I expose a struct generated from the quote macro in my derive macro without having to introduce a struct name out of the blue in my usage file (due to macro expansion)?

To illustrate the point, currently, my code looks something like this:

// "/my_derive/lib.rs"
// inside a derive macro function
let tokens = quote! {
  struct MyDeriveMacroInternalStruct {
    variant: #ident_name,
    // other stuff ...
  }
  impl #ident_name {
    pub fn something() -> Vec<MyDeriveMacroInternalStruct> {
      vec![MyDeriveMacroInternalStruct { variant: #ident_name::#variant_name, /*...*/ }, /*...*/]
    }
  }
};

tokens.into()

The usage of my code would look something like this:

use my_derive::MyDerive;

#[derive(MyDerive)]
enum Something {
  A,
  B,
  C,
}

fn process_data() -> Vec<MyDeriveMacroInternalStruct> { // having to write that struct name that came out of nowhere bothers me
  Something::something()
}

fn main() {
  let result = process_data();
  // do stuff...
}

This is a condensed version of my actual code (process_data is in another file). To reiterate my question in light of the example, how can I access the struct without having it randomly appear out of nowhere (due to macro expansion)? To me the code unchanged is hard to understand, read, and change.

I would like to be able to do something like this:

use my_derive::{MyDerive, MyDeriveStruct};

#[derive(MyDerive)]
enum Something {
  A,
  B,
  C,
}

fn process_data() -> Vec<MyDeriveStruct> { // importing the struct instead of magically appearing
  Something::something()
}

fn main() {
  let result = process_data();
  // do stuff...
}

Obviously the idea seems quite stupid, but there has to be a way around it (an arbitrary struct definition). If what I imagined isn't possible, is there some way to be more clear about where the random struct came from?


Solution

  • Actually I thought of something better. Your derive should probably be associated with a trait of the same name.

    Add an associated type to your trait:

    trait MyDerive {
        type Output;
        ...
    }
    

    Then set the associated type when you impl the trait:

      struct MyDeriveMacroInternalStruct {
        variant: #ident_name,
        // other stuff ...
      }
    
      impl MyDerive for #ident_name {
        type Output = MyDeriveMacroInternalStruct;
    
        pub fn something() -> Vec<MyDeriveMacroInternalStruct> {
          vec![MyDeriveMacroInternalStruct { variant: #ident_name::#variant_name, /*...*/ }, /*...*/]
        }
      }
    

    Then you can refer to that associated type in return position or wherever:

    use my_derive::MyDerive;
    
    #[derive(MyDerive)]
    enum Something {
      A,
      B,
      C,
    }
    
    fn process_data() -> Vec<<Something as MyDerive>::Output> {
      Something::something()
    }
    
    fn main() {
      let result = process_data();
      // do stuff...
    }
    

    Note: the convention is for #[derive(Trait)] to correspond to an impl for the given Trait, but your proc macro crate can't export a trait directly for importing in your library code.

    So generally the solution is to have two crates:

    • my-trait is the "library" crate which contains the MyTrait trait definition
      • my-trait-derive is the proc-macro crate which contains the derive macro code

    my-trait has my-trait-derive as a direct dependency, and re-exports the proc macro from it:

    // my-trait lib.rs
    
    pub use my_trait_derive::MyTrait;
    
    // macro and trait names can overlap as they're 
    // treated as different item kinds
    pub trait MyTrait {
        type Output;
        
        fn something();
    }
    

    see how clap does it here (they also re-export the whole clap_derive)

    Then a user can use your proc macro + trait like this:

    use my_trait::MyTrait;
    
    #[derive(MyTrait)]
    enum Something {}
    
    fn process_data() -> Vec<<Something as MyTrait>::Output> {
      Something::something()
    }
    

    Older Answer

    What I would do is create a trait MyDeriveOutput or something with whatever stuff you want exposed from MyDeriveMacroInternalStruct:

    trait MyDeriveOutput {
        fn variant() ...
    }
    

    And then generate an impl for each internal struct you create:

    struct MyDeriveMacroInternalStruct {
        variant: #ident_name,
        // other stuff ...
    }
    impl MyDeriveOutput for MyDeriveMacroInternalStruct {
        // whatever
    }
    

    Then you can expose the trait and require it to be imported and used with impl Trait in return position:

    use my_derive::{MyDerive, MyDeriveOutput};
    
    #[derive(MyDerive)]
    enum Something {
      A,
      B,
      C,
    }
    
    fn process_data() -> Vec<impl MyDeriveOutput> {
      Something::something()
    }
    
    fn main() {
      let result = process_data();
      // do stuff...
    }