Foreword

Written for the Rust Auckland meetup 2019-12-02.

Feel free to use and improve on these.

What Are (Rust) Macros

(not azriel's) Definition: Copy-paste code without the pasta. And varargs.

  • Shortcode:

    Original:

    
    #![allow(unused_variables)]
    fn main() {
    use std::ops::Add;
    
    #[derive(Clone, Copy, PartialEq)]
    pub struct HealthPoints(pub u32);
    
    impl Add for HealthPoints {
        type Output = HealthPoints;
    
        fn add(self, other: HealthPoints) -> HealthPoints {
            HealthPoints(self.0 + other.0)
        }
    }
    }
    

    Macro-treated:

    #[derive(derive_more::Add, Clone, Copy, PartialEq)]
    pub struct HealthPoints(pub u32);
    
  • Varargs:

    
    #![allow(unused_variables)]
    fn main() {
    let (a, b) = (3., 2.);
    
    println!();
    println!("hello");
    println!(
        "{a:.1} รท {b:.1} = {answer:.2}",
        a = a,
        b = b,
        answer = a / b
    );
    }
    

Why Macros

Mainly to increase development ergonomics:

  • ๐Ÿ‘ฅ Reduce duplication.
  • ๐Ÿ—๏ธ Reduce boilerplate.
  • ๐Ÿš‚ Varargs.
Not because you can do this
macro_rules! java {
    (static void $name:ident($($_:tt)+) { $($body:tt)+ }) => {
        fn $name() { java!($($body)+); }
    };

    ($_:ident.$__:ident.$fn_name:ident($args:tt);) => {
        println!($args);
    };
}

java! {
    static void main(String[] args) {
        System.out.println("jRust!");
    }
}

// Need to do this to get playpen to detect main function.
#[cfg(test)]
fn main() {}

๐Ÿ‘ฅ Reduce duplication

Scenario: Writing similar syntax.

Without macro
#[test]
fn on_start_delegates_to_state() {
    let (mut state, mut world, invocations): (RobotState<(), ()>, _, _) =
        setup_without_intercepts();

    state.on_start(StateData::new(&mut world, &mut ()));

    assert_eq!(vec![Invocation::OnStart], *invocations.borrow());
}

#[test]
fn on_stop_delegates_to_state() {
    let (mut state, mut world, invocations): (RobotState<(), ()>, _, _) =
        setup_without_intercepts();

    state.on_stop(StateData::new(&mut world, &mut ()));

    assert_eq!(vec![Invocation::OnStop], *invocations.borrow());
}

#[test]
fn on_pause_delegates_to_state() {
    let (mut state, mut world, invocations): (RobotState<(), ()>, _, _) =
        setup_without_intercepts();

    state.on_pause(StateData::new(&mut world, &mut ()));

    assert_eq!(vec![Invocation::OnPause], *invocations.borrow());
}

// ..
With macro
#[macro_use]
macro_rules! delegate_test {
    ($test_name:ident, $function:ident, $invocation:expr) => {
        #[test]
        fn $test_name() {
            let (mut state, mut world, invocations): (RobotState<(), ()>, _, _) =
                setup_without_intercepts();

            state.$function(StateData::new(&mut world, &mut ()));

            assert_eq!(vec![$invocation], *invocations.borrow());
        }
    };
}

delegate_test!(on_start_delegates_to_state, on_start, Invocation::OnStart);
delegate_test!(on_stop_delegates_to_state, on_stop, Invocation::OnStop);
delegate_test!(on_pause_delegates_to_state, on_pause, Invocation::OnPause);
// ..

๐Ÿ—๏ธ Reduce boilerplate

Scenario: Newtype that behaves like a number -- math operators work as any numeric type.

/// Good programmer: Strongly typed instead of Stringly typed.
pub struct HealthPoints(pub u32);

let mut me = HealthPoints(99);
let heal = HealthPoints(1);

// Want: 100 health

me.0 = me.0 + heal.0; // Not ergonomic
me = me + heal;
// me = me - heal;
// me += heal;
// me -= heal;
No macros

#![allow(unused_variables)]
fn main() {
use std::ops::{Add, AddAssign, Sub, SubAssign};

/// Character health points.
#[derive(Clone, Copy, PartialEq)]
pub struct HealthPoints(pub u32);

impl Add for HealthPoints {
    type Output = HealthPoints;

    fn add(self, other: HealthPoints) -> HealthPoints {
        HealthPoints(self.0 + other.0)
    }
}

impl AddAssign for HealthPoints {
    fn add_assign(&mut self, other: HealthPoints) {
        *self = HealthPoints(self.0 + other.0);
    }
}

impl Sub for HealthPoints {
    type Output = HealthPoints;

    fn sub(self, other: HealthPoints) -> HealthPoints {
        HealthPoints(self.0 - other.0)
    }
}

impl SubAssign for HealthPoints {
    fn sub_assign(&mut self, other: HealthPoints) {
        *self = HealthPoints(self.0 - other.0);
    }
}
}
Reduction level 1 -- `proc_macro_derive`
/// Character health points.
#[derive(
    derive_more::Add,
    derive_more::AddAssign,
    derive_more::Sub,
    derive_more::SubAssign,
    derive_more::Display,
    derive_more::From,
    Clone,
    Copy,
    PartialEq,
    Eq,
    PartialOrd,
    Ord,
    Hash,
)]
pub struct HealthPoints(pub u32);

/// Character skill points.
#[derive(
    derive_more::Add,
    derive_more::AddAssign,
    derive_more::Sub,
    derive_more::SubAssign,
    derive_more::Display,
    derive_more::From,
    Clone,
    Copy,
    PartialEq,
    Eq,
    PartialOrd,
    Ord,
    Hash,
)]
pub struct SkillPoints(pub u32);
Reduction level 2 -- `proc_macro_attribute`
use numeric_newtype_derive::numeric_newtype;

/// Character health points.
#[numeric_newtype]
pub struct HealthPoints(pub u32);

/// Character skill points.
#[numeric_newtype]
pub struct SkillPoints(pub u32);

๐Ÿš‚ Varargs

Rust doesn't support function overloading.

// Not supported:
fn my_println<A>(format: &'static str, a: A) -> String { /* .. */ }
fn my_println<A, B>(format: &'static str, a: A, b: B) -> String { /* .. */ }
fn my_println<A, B, C>(format: &'static str, a: A, b: B, c: C) -> String { /* .. */ }

Instead, macros can be used to generate a function body:


#![allow(unused_variables)]
fn main() {
macro_rules! my_println {
    ($($token:tt),*) => {
        $(
            println!("{}", $token);
        )*
    };
}

my_println!("hello", "world");
my_println!("hello", "rust", "akl");

// Short for writing:
//
// println!("{}", "hello");
// println!("{}", "world");
// println!("{}", "hello");
// println!("{}", "rust");
// println!("{}", "akl");
}

Macro Types

Macros come in multiple forms:

  • Declarative

  • Procedural

    • Function-like
    • Derive
    • Attribute

Declarative

Human: "Macro, here are some (well-formed) tokens."

Macro: "Here are some code tokens." ย ย  OR
Macro: "No rules expected the token `..`"

Definition


#![allow(unused_variables)]
fn main() {
macro_rules! my_macro {
    (pattern_0) => {
        println!("some tokens");
    };

    (pattern_1) => {
        println!("other tokens");
    };

    // $param_name:param_type
    // $( $repeated_param:param_type ),* // Zero or more, comma delimited
    // $( $repeated_param:param_type ),+ // One or more, comma delimited
    //
    // See <https://danielkeep.github.io/tlborm/book/mbe-macro-rules.html#captures>
    ($name:ident) => {
        println!("{}", stringify!($name));
    };
}
}

Usage


#![allow(unused_variables)]
fn main() {
macro_rules! my_macro {
    (pattern_0) => { println!("some tokens"); };
    (pattern_1) => { println!("other tokens"); };
    ($name:ident) => { println!("ident: {}", stringify!($name)); };
    (pattern_2) => { println!("even more tokens"); };
}

my_macro!(pattern_0);
my_macro!(pattern_1);
my_macro!(single_identifier);
my_macro!(pattern_2); // note: rules are evaluated in order.
}

Declarative macros in the wild
  • bitflags:

    #[macro_use]
    extern crate bitflags;
    
    bitflags! {
        struct Flags: u32 {
            const A = 0b00000001;
            const B = 0b00000010;
            const C = 0b00000100;
            const ABC = Self::A.bits | Self::B.bits | Self::C.bits;
        }
    }
    
  • lazy_static!

    #[macro_use]
    extern crate lazy_static;
    
    use std::collections::HashMap;
    
    lazy_static! {
        static ref HASHMAP: HashMap<u32, &'static str> = {
            let mut m = HashMap::new();
            m.insert(0, "foo");
            m.insert(1, "bar");
            m.insert(2, "baz");
            m
        };
        static ref COUNT: usize = HASHMAP.len();
        static ref NUMBER: u32 = times_two(21);
    }
    
    fn times_two(n: u32) -> u32 { n * 2 }
    
    fn main() {
        println!("The map has {} entries.", *COUNT);
        println!("The entry for `0` is \"{}\".", HASHMAP.get(&0).unwrap());
        println!("A expensive calculation on a static results in: {}.", *NUMBER);
    }
    

See also: The Little Book of Rust Macros (comprehensive guide)

Procedural

Differences from declarative macros:

  • ๐ŸŒฒ Parsing AST instead of matching patterns.
  • ๐Ÿ”บ Can write procedural logic.
  • ๐Ÿฆ€ Better diagnostics.
  • ๐Ÿ’ฏ Easier to test.
  • ๐Ÿ“ฆ Dedicated crate (not a selling point).

Types:

  • Function-like
  • Derive
  • Attribute

Function-Like

  1. Takes in any well-formed tokens.
  2. Outputs replacement tokens.
function_like!("Looks just like `macro_rules!`");
function_like! {
    struct Name {
        field: Type
    }
}

my_vec![];
map! {
    "key_0" => 123,
    "key_1" => 456,
};

#![allow(unused_variables)]
fn main() {
println!("hi");
println! { "hi" };
println!["hi"];

let numbers_0 = vec! { 1, 2, 3 };
let numbers_1 = vec!(1, 2, 3);
println!("{:?}", numbers_0);
println! { "{:?}", numbers_1 };
}

Derive

  1. Attached to a struct / enum.
  2. Generates additional tokens.
  3. Can have helper attributes.
  4. Cannot see sibling derives.
#[derive(CustomDerive)]
struct Struct; // Can see this.

// Can't see this.
impl Struct {
    // ..
}

#[derive(Clone, Copy, a_crate::CustomDerive)]
#[custom_derive(Debug)] // type level helper attribute
enum Enum {
    #[custom_derive(skip = "true")] // field level helper attribute
    Variant0,
    Variant1 { value: u32 },
}

This is not reflection!

Attribute

  1. See everything about an item.
  2. Takes in tokens, and outputs replacement tokens.
// See <https://github.com/azriel91/proc_macro_roids>.
use macro_crate::append_cd;

#[append_cd]
struct StructNamed { a: u32, b: i32 }

// outputs:
struct StructNamed { a: u32, b: i32, c: i64, d: usize }

๐Ÿš€ Rocket framework:

#[get("/hello/<name>/<age>")]
fn hello(name: String, age: u8) -> String {
    format!("Hello, {} year old named {}!", age, name)
}

fn main() {
    rocket::ignite().mount("/", routes![hello]).launch();
}

Debugging

Panic

For proc-macros, well placed panic!()s are immensely useful:

let token_stream = quote! { .. };

// This method will work even if tokens are invalid
panic!("{}", token_stream.to_string());

Cargo Expand

cargo-expand is your friend.

cargo-expand will expand macros to code tokens, for both declarative and procedural macros:

fn main() {
    println!("hello {}, one {:?}, two {:.2}", "hello", 1.1, 2.5);
}

Expands to:

#![feature(prelude_import)]
#[prelude_import]
use std::prelude::v1::*;
#[macro_use]
extern crate std;
fn main() {
    {
        ::std::io::_print(::core::fmt::Arguments::new_v1_formatted(
            &["hello ", ", one ", ", two ", "\n"],
            &match (&"hello", &1.1, &2.5) {
                (arg0, arg1, arg2) => [
                    ::core::fmt::ArgumentV1::new(arg0, ::core::fmt::Display::fmt),
                    ::core::fmt::ArgumentV1::new(arg1, ::core::fmt::Debug::fmt),
                    ::core::fmt::ArgumentV1::new(arg2, ::core::fmt::Display::fmt),
                ],
            },
            &[
                ::core::fmt::rt::v1::Argument {
                    position: ::core::fmt::rt::v1::Position::At(0usize),
                    format: ::core::fmt::rt::v1::FormatSpec {
                        fill: ' ',
                        align: ::core::fmt::rt::v1::Alignment::Unknown,
                        flags: 0u32,
                        precision: ::core::fmt::rt::v1::Count::Implied,
                        width: ::core::fmt::rt::v1::Count::Implied,
                    },
                },
                ::core::fmt::rt::v1::Argument {
                    position: ::core::fmt::rt::v1::Position::At(1usize),
                    format: ::core::fmt::rt::v1::FormatSpec {
                        fill: ' ',
                        align: ::core::fmt::rt::v1::Alignment::Unknown,
                        flags: 0u32,
                        precision: ::core::fmt::rt::v1::Count::Implied,
                        width: ::core::fmt::rt::v1::Count::Implied,
                    },
                },
                ::core::fmt::rt::v1::Argument {
                    position: ::core::fmt::rt::v1::Position::At(2usize),
                    format: ::core::fmt::rt::v1::FormatSpec {
                        fill: ' ',
                        align: ::core::fmt::rt::v1::Alignment::Unknown,
                        flags: 0u32,
                        precision: ::core::fmt::rt::v1::Count::Is(2usize),
                        width: ::core::fmt::rt::v1::Count::Implied,
                    },
                },
            ],
        ));
    };
}

Why Not Macros

  • Compilation time increases.

    • Code tokens have to be generated before the actual-code-to-compile exists.
    • Rust has to compile proc-macro crates, then run it over your crate.
    • Multiple versions of proc-macro crates means multiple compilation.
  • Not IDE friendly.

    Generated accessors are not indexed by RLS / Rust analyzer, so you don't get "Jump to definition".

Links