🎓 A1Learner
🔍

Rust The Proc-Macro Workspace

A proc-macro workspace is one workspace that holds a macro crate and an app crate. Cargo builds the macro crate first, then runs the macro while it builds the app.

The workspace root

The root Cargo.toml only lists the members. It has no [package] part:

# Cargo.toml in the top folder
[workspace]
resolver = "2"
members = ["toolkit", "app"]

The macro crate opts in

One line under [lib] makes this crate a macro crate. The rest looks like a normal library:

# toolkit/Cargo.toml
[package]
name = "toolkit"
version = "0.1.0"
edition = "2021"

[lib]
proc-macro = true

[dependencies]
syn = "2.0"
quote = "1.0"

A tiny first macro

This derive adds one method to any type. The method answers with the type's own name:

#[proc_macro_derive(TypeName)]
pub fn type_name_derive(input: TokenStream) -> TokenStream {
    let ast = parse_macro_input!(input as DeriveInput);
    let name = &ast.ident;
    quote! {
        impl #name {
            pub fn type_name() -> &'static str {
                stringify!(#name)
            }
        }
    }.into()
}

The app points at the folder

The app lists the macro crate as a path dependency. No registry is involved:

# app/Cargo.toml
[dependencies]
toolkit = { path = "../toolkit" }

The app side stays tiny

The app only writes the derive line. The method arrives from the macro crate:

#[derive(TypeName)]
struct Point { x: i32, y: i32 }

println!("first type: {}", Point::type_name());

Example

// Two crates, one workspace.
// toolkit is the proc-macro crate. app is the user crate below.
// Built and run with syn 2.0.119 and quote 1.0.47.
use toolkit::TypeName;

#[derive(TypeName)]
struct Point { x: i32, y: i32 }

#[derive(TypeName)]
struct User { name: String }

fn main() {
    println!("first type: {}", Point::type_name());
    println!("second type: {}", User::type_name());
}

Key points

  • A workspace is one folder with a shared Cargo.toml
  • Its members build together
  • They share one Cargo.lock
  • The macro crate sets proc-macro = true under [lib]
  • That one line turns its functions into macros
  • The app depends on the macro crate by path
  • A path dependency points at a folder on your disk
  • A proc-macro crate may export only macros
  • It cannot offer plain functions or types to anyone
  • The macro crate is built for the machine that runs the build
  • It is not built for the target
  • It runs during the build, then steps aside
  • A procedural macro lives in its own crate.

In this example, two crates share one workspace. Cargo reads the root Cargo.toml and finds both members. It builds toolkit first. The app depends on toolkit, so toolkit must finish first. Toolkit is a proc-macro crate. Cargo builds it for the machine that runs the build. Then Cargo builds the app. Each derive line runs the generator from toolkit. The generator reads the type name. It writes a small impl with that name inside. The impl gives the type a type_name method. Main prints what the method returns. No macro code lives in the app at all.

Note

One crate can never use its own procedural macros

The compiler must finish building the macro before any code can call it

Try it Yourself

Edit the code, then press Run.

Running is simulated · Rust Playground

Exercise

Why must the macro live in a separate crate from the app?