Proc macros - using third party crate

The Rust solution to this, more general than just to procedural macros, are public dependencies through public reexports:

//! Your `lib.rs`

// 2018 edition
pub use ::itertools;
// old style
pub extern crate itertools;

and then within a macro_rules! macro, for instance, you expand to a path referring to your crate first, which must contain the itertools crate as the second element of the path / as if it were a top-level module:

macro_rules! my_izip {(
    $($input:tt)*
) => (
    $crate // self crate
    ::
    itertools
    ::
    izip! { $($input) * }
)}

This way users of your crate will not need to depend on itertools on their own.


This is all well and fine, but with procedural macros things get a bit more complicated. Indeed:

  • there is no $crate magic metavar for procedural macros.

    • You'll have to hardcode the name of your crate instead of $crate:

      ::name_of_my_crate::itertools::izip! { ... }
      
  • a proc-macro = true crate currently cannot export any items other than functions tagged with #[proc_macro], #[proc_macro_derive], or #[proc_macro_attribute]:

    Screenshot 2020-05-12 at 11.42.29

The solution to the second problem is easy, but a bit more cumbersome: a "normal" Rust crate that wants to define, among other things, procedural macros, needs, in practice, to be split in two crates:

  • the proc-macro / derive / internals / helper crate, which defines the procedural macros;

  • and the "frontend" crate, which reexports the proc-macro crate, while defining all the other stuff.

    • (The name of the crate to hardcode in the expanded paths is that of the frontend crate)

This means you need to:

Create your helper proc-macro = true crate

Run
cargo new --lib src/proc_macros --name your_crate_name-proc_macros
(cd src/proc_macros && mv src/lib.rs mod.rs && rmdir src)
Add to src/proc_macros/Cargo.toml
[lib]
proc-macro = true
path = "mod.rs"

[dependencies]
proc-macro2.version = "1.0.x"
quote.version = "1.0.y"
syn.version = "1.0.z"
syn.features = []  # you may need "full" features
Edit src/proc_macros/mod.rs to your liking

Set up your frontend crate

Add to your Cargo.toml
[dependencies]

[dependencies.your_crate_name-proc_macros]
path = "src/proc_macros"
version = "..."  # Same as main version

[workspace]
members = ["src/proc_macros"]
Add to your src/lib.rs
pub use your_crate_name_proc_macros::{
    /* your procedural macros here */
};

#[doc(hidden)] pub /* hide from doc since it is just a tool for your macros */
use ::itertools;
// or
#[doc(hidden)] pub /* hide from doc since it is just a tool for your macros */
extern crate itertools;

What your procedural macro should expand to to refer to itertools' izip!:

src/proc_macros/mod.rs
use ::proc_macro::TokenStream;
use ::proc_macro2::{
    Span,
    TokenStream as TokenStream2,
};
use ::quote::{
    quote,
    quote_spanned,
    ToTokens,
};
use ::syn::{*,
    parse::{Parse, Parser, ParseStream},
    punctuated::Punctuated,
    Result, /* explicitly shadow `Result` */
};

#[proc_macro...] pub
fn your_proc_macro (/* args: TokenStream,*/ input: TokenStream)
  -> TokenStream
{
    let input = parse_macro_input!(input as /* ... */);
    // ...
    let izip = quote! {
        ::your_crate_name::itertools::izip
    };
    TokenStream::from(quote! {
        /* your expansion here */
        #izip! { /* ... */ }
    })
}
  • You could even reexport izip directly (pub use ::itertools::izip) and have the proc-macro expand to ::your_crate_name::izip.
8 Likes