Is there a way to have a public trait in a proc-macro crate?

1.9k views Asked by At

I have a proc-macro crate with a macro that, when expanded, needs to use custom trait implementations for Rust built-in types. I tried to define the trait in the same crate, but Rust tells me that a proc-macro crate can only have public macros (the functions annotated with #[proc_macro]) and nothing else can be public. So I put the trait in another crate and in the proc-macro crate included it as a dependency. But this means that anyone that wants to use my proc-macro crate has to depend on the other trait crate too.

So I wonder if there is a way to add a public trait to the proc-macro crate, or otherwise to make the proc-macro and trait crates linked in some way so the end user can't try to use one without the other? If neither is possible, the only solution is documenting the dependency, which is kind of fragile.

2

There are 2 answers

4
Peter Hall On BEST ANSWER

The way this is usually dealt with is to not have users depend on your proc-macro crate at all.

Your problem can be solved with 3 crates:

  • "internal" crate containing type and trait definitions that are used by the proc-macro
  • proc-macro crate:
    • Depends on the internal crate, so it can use its types and traits
  • "public" crate:
    • Depends on both the internal and proc-macro crates
    • Re-exports all types, traits and macros that you want your users to use

Whenever your macro mentions the shared types in its generated code, you need to use the fully-qualified names so that users don't also need to import them.


Some popular examples of this pattern in the wild:

  • thiserror depends on thiserror-impl which contains the actual macros
  • pin-project depends on pin-project-internal which again contains the macros
  • darling depends on darling-core and darling-macro, which itself also depends on darling-core
0
Jim On

Agree with @PeterHall's answer.

My reply to this post is not an answer to it but some additional information as to why a minimum of 3 crates is often necessary. It is due to these 4 restrictions involving a proc-macro crate.

  1. A proc-macro must be defined in its own crate.

References:
https://developerlife.com/2022/03/30/rust-proc-macro/
https://towardsdatascience.com/nine-rules-for-creating-procedural-macros-in-rust-595aa476a7ff

  1. The macros usually used in writing procedural macros, syn and quote are unable to refer to include $crate to mean the current crate within the quote macro expansion. e.g. No such thing exist:
quote!(
    use $crate::types::AStruct;
);

To give expanded codes like so:

::crate::types::AStruct;
  1. You cannot have circular dependencies, i.e. crate A depends on crate B and crate B depends on crate A.
  2. A proc-macro crate CANNOT export internal types, e.g. pub mod MyMod; pub use MyMod::MyStruct; are not allowed. This is why you need an "internal" crate and also a "public" crate.