Creating libraries with somewhat-hygenic procedural macros to avoid dependency explosions?

I'm working on a relatively simple library that provides a derive macro. As required by the use of proc_macro, it's split into two crates, which I'll call bibliophile and bibliophile-macros for demonstration. The derive macro in bibliophile-macros, which I'll call ReadingList, needs to reference traits, structs, and also crate imports of bibliophile.

The way I have things currently set up is that in the main bibliophile crate, I re-export the necessary crates in a re_export module, and then have something like the following derivation macro implementation :

use syn::{parse_macro_input,DeriveInput};
use quote::quote;
#[proc_macro_derive(ReadingListA, attributes(readinglist))]
pub fn derive_entity(tokens: TokenStream) -> TokenStream {
    let input = parse_macro_input!(tokens as DeriveInput);
    let ident = &input.ident;
    quote!{
        impl ::bibliophile::TraitA for #ident {
            fn a_func(_param: ::bibliophile::re_export::some_crate::SomeType) { }
        }
    }.into()
}

The problem with this approach is that if I chain the dependencies by implementing an avidreader library crate that depends on bibliophile, and then a bookclub binary crate that depends on avidreader, using the re-exported bibliophile-macro derive macro in bookclub will fail unless the end bookclub crate also has bibliophile in its list of imported crates.

This seems like an undesirable property that each library requires its users to also list some of its dependencies (and my modicum of experience with the Rust ecosystem leads me to believe that it's unlikely to actually be the case!), and yet it seems like an unavoidable property of procedural macros given their unhygenic nature.

A quick survey of existing implementations of other libraries has not turned up anything useful yet. Is there a concept or trick that I'm missing?

Thank you!

The strategy I've used in include_dir is to make sure that the include_dir crate re-exports the macro and any features that get enabled for include_dir automatically enable the same features in include_dir_macros.

From the end user's perspective, they don't even know include_dir_macros exists.

3 Likes

This is the pattern I've always seen: re-export the dependencies of your macro. Instead of using a sub-module, though, I normally see them at the top level with #[doc(hidden)].

Minor quibble, but probably this should be using $crate instead of ::bibliophile as you can rename the crate you're importing in Cargo.lock.

1 Like

The $crate syntax doesn't work for proc-macros, so you need to refer to ::bibliophile by name if you want to use fully qualified paths.

Normally, I'd write impl TraitA for #ident { ... } and then rely on the user to use bibliophile::TraitA in the file.

Boo. I guess the $crate would be the proc macro crate itself, so that makes sense. Ideally you could emit a proc macro dependency as a symbol directly: perhaps the compiler emits some "super qualified" path based on the crate hash. But now the foo and foo-macro crates depend on each other. Sigh.

tbh, I think the best solution would be to "just" allow implementing proc-macros in normal crates so we don't have this foo/foo-macro crate split in the first place. Then $crate would be quite natural and work as expected.

As always, there's an issue for this: