Foreword
Written for the Rust Auckland meetup 2019-12-02.
- Rust Auckland meetup: https://www.meetup.com/rust-akl/
- Slack: https://rust-akl.slack.com/messages/CCC7KUXMY/
- Repository: https://github.com/azriel91/rust_macros
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
-
#[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; } }
-
#[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
- Takes in any well-formed tokens.
- 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
- Attached to a struct / enum.
- Generates additional tokens.
- Can have helper attributes.
- 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
- See everything about an item.
- 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-macro
s, 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
- Rust Lang Reference: Procedural Macros
- The Little Book of Rust Macros
cargo-expand
proc_macro_roids
: Improves ergonomics when writing proc macros.