Convenient Rust crates with procedural macros and runnable code
Procedural macros in Rust are a great thing for many purposes (implementing custom derives, domain-specific languages inside Rust, etc). However, the use of procedural macros imposes one very inconvenient constraint: a crate that defines procedural macros can export nothing but procedural macros. This usually leads us to usi multiple crates to do exactly one thing (remember serde
and serde_derive
?). In this article, I will review an approach to this problem I have seen in the failure
crate that allows us to import exactly one crate.
The project structure
All libraries I have seen so far rely on workspaces when they need to build a "simple" crate and a proc-macro
crate. The generic structure for that is:
workspace
βββ lib_crate/
βββ proc_macro_crate/
βββ Cargo.toml
with Cargo.toml
that looks like that:
[workspace]
members = ["lib_crate", "proc_macro_crate"]
Usually, lib_crate
contains the definitions of traits, structures and procedures and proc_macro_crate
contains procedural macros that generate code which reuses definitions from lib_crate
.
The actual trick
In many libraries what you need to do is to import two crates:
#[macro_use]
extern crate proc_macro_crate;
extern crate lib_crate;
If you don't want your users to do that, you can use a pretty simple hack. Cargo does not prohibit to re-export procedural macros, so you can just re-export them in your lib_crate
!
lib_crate/Cargo.toml
:
[dependencies]
proc_macro_crate = { path = "../proc_macro_crate", version = "0.1.0" }
lib_crate/src/lib.rs
:
#[macro_use]
extern crate proc_macro_crate;
pub use proc_macro_crate::*;
As they do not need to import two separate crates anymore Cargo.toml
and their code look a bit cleaner.