Macros
Macros should have a decicated book to themselves. Rust supports both declarative and procedural macros, which are either built-in or user created. In this book, we'll cover some of the most common build-in macros.
For more information about Rust macros, also see The Little Book of Rust Macros
Declarative macros
println!
- prints to stdout. Requires a formatter depending on the data type. E.g.,
fn main() { println!("This is a string"); println!("This is an int: {}", 5); println!("{:?}", vec!["This", "is", "a", "vec"]); // {:?} means debug mode. }
vec!
- creates a Vec
based on the provided input.
fn main() { let x: Vec<usize> = vec![1, 2, 3, 4, 5]; println!("{:?}", x); }
panic!
- causes the program to exit and starts unwinding the stack.
fn main() { panic!("This will exit the program!") }
assert!
- runtime assert that a boolean expression evaluates to true
. E.g.,
fn main() { assert!(5 < 6); }
assert_eq!
- runtime equality assert. E.g.,
fn main() { assert_eq!(6, 5 + 1); }
Implementing our own declarative macro
I want to emphasize that I personally do not know that much about Rust macros. However, in this example we'll try to implement something that resembles Python's Path
, which is a part of the Pathlib
module. Using Path
, there is a very handy way to define a file path by chaining multiple directories.
from pathlib import Path
outdir = Path("my_outdir")
outfile = outdir / "sub_dir" / "another_sub_dir" / "my_file.txt"
Essentially, if the top directory outdir
is of type Path
, we can generate a file path through /
. Personally, I think this is way more neat than having to use an f-string or similar. Let's try to implement something similar using a Rust declarative macro.
There are endless ways of implementing this, but below is one example. We'll define our macro file_path
to require a base directory and at least one more argument. The syntax is a bit strange. It kinda looks like a function, but kinda not.
The expression ($base:expr $(, $sub:expr)+)
defines the pattern that we enforce. In this case, we require one expression $base
, followed by one or more comma-separated expressions $sub
.
We use import statements with a leading ::
to signify that we want the root crate std
to not accidentally use some locally defined crate called std
.
Finally, we create a PathBuf
from our base dir and iteratively build up the path.
use std::path::PathBuf; macro_rules! file_path { ($base:expr $(, $sub:expr)+) => {{ use ::std::path::PathBuf; use ::std::fs; let mut full_path = PathBuf::from($base); $( full_path.push($sub); )* full_path }}; } fn main(){ let outdir = "my_outdir".to_string(); let outfile = file_path!(outdir, "sub_dir", "another_sub_dir", "my_file.txt"); println!("{:?}", outfile); }
The point with the simple example above is not to generate a bullet proof, production ready macro but rather showcase that declarative macros can be very handy for defining custom behaviors. If we'd try to implement file_path
as a function, we'd probably have to handle the variable number of sub-directories through a Vec
or similar.
Procedural macros
Are divided into three categories, all of which are outside the scope of this book. Regardless, they are very handy for deriving traits, such as Debug
. As an example, assume we've created a Struct
that we'd want to be able to print to stdout using println!
. In this case, we need use derive the Debug
trait through #[derive(Debug)]
.
#[derive(Debug)] // Try commenting out this line! struct MyStruct{ my_vec: Vec<usize>, } fn main() { let my_struct = MyStruct { my_vec: vec![1, 2, 3, 4, 5] }; println!("{:?}", my_struct); }