Macros
Macros are one of Oxide's most powerful features. They allow you to write code that writes other code, enabling abstractions that would be difficult or impossible with functions alone.
What Are Macros?
Macros are programs that generate code at compile time. They're different from functions in important ways:
| Aspect | Functions | Macros |
|---|---|---|
| Type checking | Checked | Generated code is checked |
| Parameters | Must match types | Can accept variable number of arguments |
| Monomorphization | Functions are shared | Code can be duplicated |
| Speed | Runtime | Compile time |
There are two kinds of macros in Oxide:
- Declarative macros - Define rules for pattern matching and code generation
- Procedural macros - Functions that manipulate token streams
Declarative Macros
Declarative macros, also called "macros by example," let you define a syntax pattern and the code to generate when that pattern matches.
The vec! Macro
The vec! macro is a classic example:
// Calling the macro
let v = vec![1, 2, 3]
// Equivalent to
let v = {
var v = Vec.new()
v.push(1)
v.push(2)
v.push(3)
v
}
Defining Declarative Macros
The syntax is:
Note:
macro_rules!blocks use Rust's macro syntax (including=>for rule arms). This is a deliberate compatibility island; outside macros, Oxide uses->.
macro_rules! name {
(pattern1) => { expansion1 };
(pattern2) => { expansion2 };
}
Here's a simple example:
macro_rules! shout {
($e:expr) => {
println!("\(($e).toUppercase())!")
};
}
fn main() {
shout!("hello") // Prints: HELLO!
shout!("world") // Prints: WORLD!
}
Macro Patterns and Fragments
The part in parentheses after the macro name defines what patterns it accepts. Common fragment specifiers:
| Specifier | Matches |
|---|---|
expr | Expressions |
stmt | Statements |
item | Items (functions, structs, etc.) |
ty | Types |
ident | Identifiers |
path | Paths |
tt | Token trees |
block | Block expressions |
Here's a macro that accepts multiple expressions:
macro_rules! printAll {
($($e:expr),*) => {
$(
println!("\($e)")
)*
};
}
fn main() {
printAll!(1, 2, 3, "hello", true)
}
The $(...)* syntax means "repeat this pattern zero or more times."
Building the vec! Macro
Let's build our own version of vec!:
macro_rules! myVec {
// Empty vec
() => {
Vec.new()
};
// Single element
($e:expr) => {
{
var v = Vec.new()
v.push($e)
v
}
};
// Multiple elements
($($e:expr),+) => {
{
var v = Vec.new()
$(
v.push($e)
)*
v
}
};
// Comma-separated list with trailing comma
($($e:expr),+ ,) => {
myVec!($($e),*)
};
}
fn main() {
let v1 = myVec!()
let v2 = myVec!(1)
let v3 = myVec!(1, 2, 3)
let v4 = myVec!(1, 2, 3,)
println!("\(v1:?)") // []
println!("\(v2:?)") // [1]
println!("\(v3:?)") // [1, 2, 3]
println!("\(v4:?)") // [1, 2, 3]
}
Repetition Patterns
The repetition syntax uses several operators:
// * means 0 or more times
($($e:expr),*)
// + means 1 or more times
($($e:expr),+)
// ? means 0 or 1 times
($e:expr)?
// You can separate with any token
($($e:expr);*) // semicolon-separated
($($e:expr) |*) // pipe-separated
Advanced Example: DSL for Assertions
Here's a declarative macro that creates a domain-specific language (DSL) for assertions:
macro_rules! assertEqWithMsg {
($left:expr, $right:expr) => {
assertEqWithMsg!($left, $right, "")
};
($left:expr, $right:expr, $msg:expr) => {
{
let left = &($left)
let right = &($right)
if left != right {
panic!(
"assertion failed: {} == {}, message: {}",
left, right, $msg
)
}
}
};
}
fn main() {
assertEqWithMsg!(5, 5)
assertEqWithMsg!(10, 5 * 2, "Basic math")
// assertEqWithMsg!(5, 6) // Would panic
}
Procedural Macros
Procedural macros are more powerful but also more complex. They're functions that take a token stream as input and produce a token stream as output.
Derive Macros
The most common procedural macros are derive macros, which automatically implement traits:
#[derive(Clone, Copy, Debug)]
struct Point {
x: Int,
y: Int,
}
fn main() {
let p = Point { x: 5, y: 10 }
let p2 = p.clone()
println!("\(p:?)") // Point { x: 5, y: 10 }
println!("\(p2:?)") // Point { x: 5, y: 10 }
}
In this example, #[derive(Debug)] automatically implements the Debug trait, so we can print the struct with the :? format specifier.
Common derive macros:
Debug- Printable representationClone- CloningCopy- Copy semanticsDefault- Default valuesHash- HashingPartialEq,Eq- EqualityPartialOrd,Ord- Ordering
Attribute Macros
Attribute macros modify items:
#[route(GET, "/")]
fn index(): String {
"Hello, world!".toString()
}
The #[route(...)] macro is called with the attribute parameters and the item it decorates.
Function-like Macros
Function-like procedural macros look like function calls but process tokens:
let sql = sql!(
SELECT * FROM users
WHERE age > 18
)
Macro Rules and Macros Defined in Terms of Other Macros
Macros can call other macros:
macro_rules! fiveTimes {
($e:expr) => {
$e + $e + $e + $e + $e
};
}
macro_rules! timesFifteen {
($e:expr) => {
fiveTimes!(5 * $e) + fiveTimes!(2 * $e)
};
}
fn main() {
println!("\(timesFifteen!(2))") // (5 * 2) * 5 + (2 * 2) * 5 = 70
}
Common Macro Patterns
Variadic Functions
Implement function-like behavior with variable arguments:
macro_rules! sum {
($e:expr) => { $e };
($e:expr, $($rest:expr),+) => {
$e + sum!($($rest),+)
};
}
fn main() {
println!("\(sum!(1))") // 1
println!("\(sum!(1, 2))") // 3
println!("\(sum!(1, 2, 3, 4))") // 10
}
Type Repetition
Generate code for multiple types:
macro_rules! implForTypes {
($trait:ident, $method:ident, $($ty:ty),*) => {
$(
extension $ty: $trait {
fn $method(): String {
stringify!($ty).toString()
}
}
)*
};
}
Debug Printing
Create convenient debugging utilities:
macro_rules! dbg {
($($e:expr),*) => {
$(
eprintln!("{} = {:?}", stringify!($e), &$e)
)*
};
}
fn main() {
let x = 5
let y = 10
dbg!(x, y) // Prints debug info
}
Hygiene
Macros are "hygienic," meaning they don't accidentally capture or shadow variables:
macro_rules! setup {
($n:expr) => {
let x = $n // This x is local to the macro
};
}
fn main() {
let x = "outer"
setup!(5)
println!("\(x)") // Prints: outer, not 5
}
This is different from macros in C, where they're simple text substitution.
Useful Built-in Macros
Oxide provides several useful built-in macros:
// Print debugging
println!("Hello, \(name)!")
// Panic with a message
panic!("Something went wrong")
// Assert conditions
assert!(condition)
assertEq!(left, right)
// Create vectors
vec![1, 2, 3]
// Stringify expressions
let s = stringify!(x + y)
// Get file and line info
file!()
line!()
column!()
// Access environment variables at compile time
env!("HOME")
When to Use Macros
Use macros when:
- Repetitive code generation - Avoid boilerplate
- Domain-specific languages - Create natural syntax
- Metaprogramming - Generate code based on input
- Zero-cost abstractions - Generate specialized code
- Syntax extensions - Extend the language
Don't use macros when:
- A function would work - Functions are simpler and more testable
- You need dynamic behavior - Macros run at compile time
- Code clarity matters more - Macros can hide what's happening
- You need debugging - Macro expansion can be hard to debug
Debugging Macros
To see what a macro expands to, use cargo expand (requires the nightly compiler):
cargo expand
This will show the expanded code, helping you understand what the macro is generating.
Best Practices
Document Your Macros
/// Creates a vector with the given elements.
///
/// # Examples
///
/// ```
/// let v = myVec![1, 2, 3]
/// assertEq!(v.len(), 3)
/// ```
macro_rules! myVec {
// ...
}
Keep Them Simple
// Good: simple, focused
macro_rules! triple {
($e:expr) => { $e + $e + $e };
}
// Avoid: complex, hard to understand
macro_rules! complex {
// ... 50 lines of rules
}
Use Descriptive Names
// Good
macro_rules! assertError {
// ...
}
// Avoid
macro_rules! ae {
// ...
}
Test Your Macros
#[test]
fn testMyVecMacro() {
let v = myVec![1, 2, 3]
assertEq!(v.len(), 3)
assertEq!(v[0], 1)
}
Summary
Macros in Oxide:
- Declarative macros - Pattern matching and code generation
- Procedural macros - More powerful, work with token streams
- Derive macros - Automatically implement traits
- Hygiene - Safe variable scoping
- Powerful abstractions - Can eliminate boilerplate and create DSLs
Macros are advanced because they're powerful but require careful design. Use them judiciously, and they can make your code cleaner and more expressive. When in doubt, prefer functions—they're easier to understand and debug.