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

2

5

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.

Wivinah answered 9/10, 2020 at 23:18 Comment(0)
M
10

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
Mochun answered 10/10, 2020 at 0:24 Comment(4)
Thanks for this information, it clears up some doubts I had regarding re-exporting macros. But I think that if my macro expansion inserts the "use trait_crate::MyTrait" into the client source code, then anyway they would have to depend directly on the "trait_crate" crate in their crate too, right?Wivinah
No, you can make your macros use fully-qualified paths, so they don't have to. I'll update the answer to add that.Mochun
Thank you so much! Rust is so awesome! =)Wivinah
proc-macro-crate is useful to get the fully-qualified path in case the user aliases the crateCourante
S
1

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.
Sacellum answered 20/7, 2023 at 16:28 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.