Implementing a derive macro in terms of another derive

Let's say I have a crate that provides a derive macro:

#[derive(crate1::Foo)]
#[foo_param(blah)]
struct WantsFoo;

can I (in a separate crate) provide a derive macro that just delegates to the first one? e.g. I'd like this:

#[derive(crate2::FooBlah)]
struct WantsFoo;

to expand to the original definition.

The use case is to provide a special case ergonomic wrapper around the original (flexible and generic) functionality. I understand I can create a helper crate with ~all the logic from crate1::Foo and share that between two unrelated macros, but ideally, I'd achieve this without touching crate1 (no real reason it should know about crate2 and go out of its way to help it).

If that's what you need it to do, sure. Any procedural macro - be it #[derive] or otherwise - simply transforms the given source item's TokenStream into another one. In your case:

#[proc_macro_derive(FooBla)]
pub fn derive_foo_bla(item: TokenStream) -> TokenStream {
    todo!("add some #[derive(...)] on top")
}

With the only caveats being, your crate2 must be declared with an explicit proc-macro = true. If it's your first time writing it, use a workspace for it. It'll help to keep things simple and neat.

Well yeah. I'm curious about the todo!() part. :wink: A derive macro cannot modify the original TokenStream, just append to it, so there's nothing to attach the #[derive] attribute to.

I can make it an attribute proc macro (#[fooblah] rather than #[derive(FooBlah)]) easily enough, but that (to the user) suggests more magic than is actually happening.

My ideal outcome would be to import the crate1 derive macro as a library and call it from within my own derive macro but apparently that's not a thing (you can only invoke the derive macro, not call it).

My bad, that's indeed the case. At best, you can require the enum or struct declaration being passed in to be annotated with some customary prefix or suffix (i.e. AutoWants or WantsFooStub), then strip it away in your own macro and re/create the original with the #[derive]'s required.

Which would only work for other proc-macro crates, and not for any built-in ones; might not be ideal in the longer term, either. Quite an annoying limitation to an otherwise trivial task, I'll admit.

An alternative name, to keep things (a bit more) clear: #[auto_derive(...)]

The whole 'Foo' thing isn't very clear since it's abstract and meaningless names, but I imagine from the earlier exchanges that you want your custom derive macro FooBlah to simply instantiate two attributes in front of its content, struct WantsFoo:

#[derive(crate1::Foo)]
#[foo_param(blah)]

A derive macro is neutral regarding the attached item: it will only add items after it. So I believe you need an attribute macro, which is able to modify its content. In this case, you want to add attributes in front of it, that you will likely specify in argument, like a list of the attributes you want:

#[proc_macro_attribute]
pub fn multi_attr(args: TokenStream, input: TokenStream) -> TokenStream {
    macro_multi_attr(args.into(), input.into()).into()
}

where macro_multi_attr(args: proc_macro2::TokenStream, input: proc_macro2::TokenStream). These proc_macro2 types are just wrappers around the proc_macro::TokenStream of the top-level macro, and the reason I mention this is because they're heavily used in the traits and macros provided in the syn create, which you definitely want to use. And having a separate function using only the proc_macro2 type will give you much more flexibility (if you want to put that in another non-proc crate, do unit testing, etc). The .into() converts one type into the other.

In that function, you need to

  • parse the arguments from args
  • adds the appropriate attributes to item to create the output TokenStream

The typical way to go about it would be to store the data required to generate the attributes into an object, let's say a struct MyArgs, and implement Parse and VisitMut for that object.

The Parse trait allows you to parse the argument stream, args, and extract the data you need (a list of attribute names, possibly their types, etc):

impl Parse for MyArgs {
    fn parse(args: ParseStream) -> syn::parse::Result<Self> {
        /* parsing your format, likely a comma-separated list, 
           so using something like 
           Punctuated::<Ident, Token![,]>::parse_terminated(args)?...  */
        Ok(MyArgs{ /* ... */ })
    }
}

The VisitMut trait allows you to modify items without having to parse and re-create everything. This one is used to process the actual content, input. The top item in the TokenStream will be syn::File, to which you can add attributes, so you could modify only that, and your code would look like this:

impl VisitMut for MyArgs {
    fn visit_file_mut(&mut self, file: &mut File) {
        let attrs: Vec<syn::Attribute> = /* ... */;
        file.attrs.extend(attrs);
    }
}

You can create each attribute by using quote!, for example.

I forgot to mention that, in order for those traits to be used, you'll need to call the relevant methods:

  • to parse the arguments, my_args = syn::parse2::<MyArgs>(args)?
  • to modify the input, my_args.visit_file_mut(&mut input)

I know how to do this with an attribute macro but I was hoping to keep the #[derive(...)] syntax. Still, thanks for confirming there's no way to do this.

Heh. I misread one of your comments, indeed. :smiley:

Yes, derive macros only append to the source code, and you can call their functions only from the same crate, like the other types.

If you want a derive macro, you'll have to stick to those limitations:

  • you need to make sure the original derive macros expose a top function like the macro_multi_attr() I mentioned earlier (but the equivalent for a derive macro), which practically means you must own them.
  • you can only append, so you can't delegate to other types of procedural macros, obviously; e.g. like the foo_param(blah) one in your first example.

As long as the name is evocative and the arguments are clear, I think an attribute macro is fine, but I don't know your context. In general, its users will have to add the dependency anyway, so they should be aware of the "magic" involved. It's perhaps even better than trying to hide it.