Macros
We’ve used macros like println! throughout this book, but we haven’t fully
explored what a macro is and how it works. The term macro refers to a family
of features in Cairo: declarative macros with macro and three kinds
of procedural macros covered in Procedural Macros:
- Custom
#[derive]macros that specify code added with thederiveattribute used on structs and enums - Attribute-like macros that define custom attributes usable on any item
- Function-like macros that look like function calls but operate on the tokens specified as their argument
We’ll talk about each of these in turn, but first, let’s look at why we even need macros when we already have functions.
The Difference Between Macros and Functions
Fundamentally, macros are a way of writing code that writes other code, which
is known as metaprogramming. In Appendix C, we discuss derivable traits and the derive
attribute, which generates an implementation of various traits for you. We’ve
also used the println! and array! macros throughout the book. All of these
macros expand to produce more code than the code you’ve written manually.
Metaprogramming is useful for reducing the amount of code you have to write and maintain, which is also one of the roles of functions. However, macros have some additional powers that functions don’t.
A function signature must declare the number and type of parameters the
function has. Macros, on the other hand, can take a variable number of
parameters: we can call println!("hello") with one argument or
println!("hello {}", name) with two arguments. Also, macros are expanded
before the compiler interprets the meaning of the code, so a macro can, for
example, implement a trait on a given type. A function can’t, because it gets
called at runtime and a trait needs to be implemented at compile time.
The downside to implementing a macro instead of a function is that macro definitions are more complex than function definitions because you’re writing Cairo code — or, even more complex, Rust code — that writes Cairo code. Due to this indirection, macro definitions are generally more difficult to read, understand, and maintain than function definitions.
Another important difference between macros and functions is that you must define macros or bring them into scope before you call them in a file, as opposed to functions you can define anywhere and call anywhere.
Declarative Inline Macros for General Metaprogramming
The most simple form of macros in Cairo is the declarative macro, also sometimes referred to as
just plain “macros.” At their core, declarative macros allow you to write something similar to a
match expression. As discussed in Chapter 6, match expressions are control structures that
take an expression, compare the resultant value of the expression to patterns, and then run the code
associated with the matching pattern. Macros also compare a value to patterns that are associated
with particular code: in this situation, the value is the literal Cairo source code passed to the
macro; the patterns are compared with the structure of that source code; and the code associated
with each pattern, when matched, replaces the code passed to the macro. This all happens during
compilation.
To define a macro, you use the macro construct. Let’s explore this style by
looking at how an “array-building” macro works. Earlier, we used Cairo’s
built-in array! macro to create arrays with particular values. For example,
the following creates a new array containing three integers:
let a = array![1, 2, 3];
We could also use array! to make an array of two integers or five values of
other types, because the macro can accept a variable number of arguments.
We wouldn’t be able to use a regular function to do the same because we
wouldn’t know the number or types of values up front.
Below is a slightly simplified definition of an array-building macro written in
Cairo. It isn’t the exact array! macro from the core library, but it shows
the same core idea using declarative inline macros:
macro make_array {
($($x:expr), *) => {
{
let mut arr = $defsite::ArrayTrait::new();
$(arr.append($x);)*
arr
}
};
}
#[cfg(test)]
#[test]
fn test_make_array() {
let a = make_array![1, 2, 3];
let expected = array![1, 2, 3];
assert_eq!(a, expected);
}
mod hygiene_demo {
// A helper available at the macro definition site
fn def_bonus() -> u8 {
10
}
// Adds the defsite bonus, regardless of what exists at the callsite
pub macro add_defsite_bonus {
($x: expr) => { $x + $defsite::def_bonus() };
}
// Adds the callsite bonus, resolved where the macro is invoked
pub macro add_callsite_bonus {
($x: expr) => { $x + $callsite::bonus() };
}
// Exposes a variable to the callsite using `expose!`.
pub macro apply_and_expose_total {
($base: expr) => {
let total = $base + 1;
expose!(let exposed_total = total;);
};
}
// A helper macro that reads a callsite-exposed variable
pub macro read_exposed_total {
() => { $callsite::exposed_total };
}
// Wraps apply_and_expose_total and then uses another inline macro
// that accesses the exposed variable via `$callsite::...`.
pub macro wrapper_uses_exposed {
($x: expr) => {
{
$defsite::apply_and_expose_total!($x);
$defsite::read_exposed_total!()
}
};
}
}
use hygiene_demo::{
add_callsite_bonus, add_defsite_bonus, apply_and_expose_total, wrapper_uses_exposed,
};
#[cfg(test)]
#[test]
fn test_hygiene_e2e() {
// Callsite defines its own `bonus` — used only by callsite-resolving macro
let bonus = | | -> u8 {
20
};
let price: u8 = 5;
assert_eq!(add_defsite_bonus!(price), 15); // uses defsite::def_bonus() = 10
assert_eq!(add_callsite_bonus!(price), 25); // uses callsite::bonus() = 20
// Call in statement position; it exposes `exposed_total` at the callsite
apply_and_expose_total!(3);
assert_eq!(exposed_total, 4);
// A macro invoked by another macro can access exposed values via `$callsite::...`
let w = wrapper_uses_exposed!(7);
assert_eq!(w, 8);
}
Note: The built-in
array!macro in the standard library may include optimizations (like reserving capacity) that we don’t include here to keep the example simple.
The structure of the macro body is similar to a match expression. Here we
have one arm with the pattern ($($x:expr), *), followed by => and the
block of code associated with this pattern. If the pattern matches, the
associated block of code is emitted. More complex macros can have multiple
arms, each with a different pattern.
Pattern syntax in macro definitions differs from pattern syntax used when matching values: macro patterns are matched against Cairo source code structure. Let’s walk through the pattern pieces in the example above:
- We use parentheses to encompass the whole matcher pattern.
- A dollar sign (
$) introduces a macro variable that will capture the code matching the subpattern. Within$()is$x:expr, which matches any Cairo expression and gives that expression the name$x. - The comma following
$()requires literal commas between each matched expression. - The
*quantifier specifies the subpattern can repeat zero or more times.
When we call this macro with make_array![1, 2, 3], the $x pattern matches
three times: the expressions 1, 2, and 3.
Now look at the expansion side: $(arr.append($x);)* is generated once for
each match of $() in the pattern. The $x is replaced with each matched
expression. Calling make_array![1, 2, 3] expands to code like the following:
Note: The VSCode extension can help you inspect the expanded code by doing
Ctrl+Shift+Pand thenCairo: Recursively expand macros for item at caret.
{
let mut arr = ArrayTrait::new();
arr.append(1);
arr.append(2);
arr.append(3);
arr
}
We’ve defined a macro that can take any number of arguments of any type and generate code to create an array containing the specified elements.
Usage looks like this:
macro make_array {
($($x:expr), *) => {
{
let mut arr = $defsite::ArrayTrait::new();
$(arr.append($x);)*
arr
}
};
}
#[cfg(test)]
#[test]
fn test_make_array() {
let a = make_array![1, 2, 3];
let expected = array![1, 2, 3];
assert_eq!(a, expected);
}
mod hygiene_demo {
// A helper available at the macro definition site
fn def_bonus() -> u8 {
10
}
// Adds the defsite bonus, regardless of what exists at the callsite
pub macro add_defsite_bonus {
($x: expr) => { $x + $defsite::def_bonus() };
}
// Adds the callsite bonus, resolved where the macro is invoked
pub macro add_callsite_bonus {
($x: expr) => { $x + $callsite::bonus() };
}
// Exposes a variable to the callsite using `expose!`.
pub macro apply_and_expose_total {
($base: expr) => {
let total = $base + 1;
expose!(let exposed_total = total;);
};
}
// A helper macro that reads a callsite-exposed variable
pub macro read_exposed_total {
() => { $callsite::exposed_total };
}
// Wraps apply_and_expose_total and then uses another inline macro
// that accesses the exposed variable via `$callsite::...`.
pub macro wrapper_uses_exposed {
($x: expr) => {
{
$defsite::apply_and_expose_total!($x);
$defsite::read_exposed_total!()
}
};
}
}
use hygiene_demo::{
add_callsite_bonus, add_defsite_bonus, apply_and_expose_total, wrapper_uses_exposed,
};
#[cfg(test)]
#[test]
fn test_hygiene_e2e() {
// Callsite defines its own `bonus` — used only by callsite-resolving macro
let bonus = | | -> u8 {
20
};
let price: u8 = 5;
assert_eq!(add_defsite_bonus!(price), 15); // uses defsite::def_bonus() = 10
assert_eq!(add_callsite_bonus!(price), 25); // uses callsite::bonus() = 20
// Call in statement position; it exposes `exposed_total` at the callsite
apply_and_expose_total!(3);
assert_eq!(exposed_total, 4);
// A macro invoked by another macro can access exposed values via `$callsite::...`
let w = wrapper_uses_exposed!(7);
assert_eq!(w, 8);
}
To use them, enable the experimental feature in your Scarb.toml:
# [package]
# name = "listing_inline_macros"
# version = "0.1.0"
# edition = "2024_07"
#
experimental-features = ["user_defined_inline_macros"]
#
# [cairo]
#
# [dependencies]
# cairo_execute = "2.12.0"
#
# [dev-dependencies]
# snforge_std = "0.48.0"
# assert_macros = "2.12.0"
#
# [scripts]
# test = "snforge test"
#
# [tool.scarb]
# allow-prebuilt-plugins = ["snforge_std"]
Inline macros are defined with macro name { ... } where each arm matches a code pattern and expands to replacement code. Like Rust’s macros-by-example, you capture syntax fragments with $var: kind and can repeat matches with $()*, $()+, or $()?.
Hygiene, $defsite/$callsite, and expose!
Cairo’s inline macros are hygienic: names
introduced in the macro definition don’t leak into the call site unless you
explicitly expose them. Name resolution within macros can reference either the
macro definition site or the call site using $defsite:: and $callsite::.
Note that, similarly to Rust, macros are expected to expand to a single
expression; thus, if your macro defines several statements, you should wrap
them with an additional {} block that returns a final expression.
The following end-to-end example illustrates all of these aspects together:
macro make_array {
($($x:expr), *) => {
{
let mut arr = $defsite::ArrayTrait::new();
$(arr.append($x);)*
arr
}
};
}
#[cfg(test)]
#[test]
fn test_make_array() {
let a = make_array![1, 2, 3];
let expected = array![1, 2, 3];
assert_eq!(a, expected);
}
mod hygiene_demo {
// A helper available at the macro definition site
fn def_bonus() -> u8 {
10
}
// Adds the defsite bonus, regardless of what exists at the callsite
pub macro add_defsite_bonus {
($x: expr) => { $x + $defsite::def_bonus() };
}
// Adds the callsite bonus, resolved where the macro is invoked
pub macro add_callsite_bonus {
($x: expr) => { $x + $callsite::bonus() };
}
// Exposes a variable to the callsite using `expose!`.
pub macro apply_and_expose_total {
($base: expr) => {
let total = $base + 1;
expose!(let exposed_total = total;);
};
}
// A helper macro that reads a callsite-exposed variable
pub macro read_exposed_total {
() => { $callsite::exposed_total };
}
// Wraps apply_and_expose_total and then uses another inline macro
// that accesses the exposed variable via `$callsite::...`.
pub macro wrapper_uses_exposed {
($x: expr) => {
{
$defsite::apply_and_expose_total!($x);
$defsite::read_exposed_total!()
}
};
}
}
use hygiene_demo::{
add_callsite_bonus, add_defsite_bonus, apply_and_expose_total, wrapper_uses_exposed,
};
#[cfg(test)]
#[test]
fn test_hygiene_e2e() {
// Callsite defines its own `bonus` — used only by callsite-resolving macro
let bonus = | | -> u8 {
20
};
let price: u8 = 5;
assert_eq!(add_defsite_bonus!(price), 15); // uses defsite::def_bonus() = 10
assert_eq!(add_callsite_bonus!(price), 25); // uses callsite::bonus() = 20
// Call in statement position; it exposes `exposed_total` at the callsite
apply_and_expose_total!(3);
assert_eq!(exposed_total, 4);
// A macro invoked by another macro can access exposed values via `$callsite::...`
let w = wrapper_uses_exposed!(7);
assert_eq!(w, 8);
}
Usage at the call site:
macro make_array {
($($x:expr), *) => {
{
let mut arr = $defsite::ArrayTrait::new();
$(arr.append($x);)*
arr
}
};
}
#[cfg(test)]
#[test]
fn test_make_array() {
let a = make_array![1, 2, 3];
let expected = array![1, 2, 3];
assert_eq!(a, expected);
}
mod hygiene_demo {
// A helper available at the macro definition site
fn def_bonus() -> u8 {
10
}
// Adds the defsite bonus, regardless of what exists at the callsite
pub macro add_defsite_bonus {
($x: expr) => { $x + $defsite::def_bonus() };
}
// Adds the callsite bonus, resolved where the macro is invoked
pub macro add_callsite_bonus {
($x: expr) => { $x + $callsite::bonus() };
}
// Exposes a variable to the callsite using `expose!`.
pub macro apply_and_expose_total {
($base: expr) => {
let total = $base + 1;
expose!(let exposed_total = total;);
};
}
// A helper macro that reads a callsite-exposed variable
pub macro read_exposed_total {
() => { $callsite::exposed_total };
}
// Wraps apply_and_expose_total and then uses another inline macro
// that accesses the exposed variable via `$callsite::...`.
pub macro wrapper_uses_exposed {
($x: expr) => {
{
$defsite::apply_and_expose_total!($x);
$defsite::read_exposed_total!()
}
};
}
}
use hygiene_demo::{
add_callsite_bonus, add_defsite_bonus, apply_and_expose_total, wrapper_uses_exposed,
};
#[cfg(test)]
#[test]
fn test_hygiene_e2e() {
// Callsite defines its own `bonus` — used only by callsite-resolving macro
let bonus = | | -> u8 {
20
};
let price: u8 = 5;
assert_eq!(add_defsite_bonus!(price), 15); // uses defsite::def_bonus() = 10
assert_eq!(add_callsite_bonus!(price), 25); // uses callsite::bonus() = 20
// Call in statement position; it exposes `exposed_total` at the callsite
apply_and_expose_total!(3);
assert_eq!(exposed_total, 4);
// A macro invoked by another macro can access exposed values via `$callsite::...`
let w = wrapper_uses_exposed!(7);
assert_eq!(w, 8);
}
What this demonstrates:
$defsite::...resolves to items next to the macro definition, stable across call sites.$callsite::...resolves to items visible where the macro is invoked.- Names don’t leak by default;
expose!can deliberately introduce new items into the call site. - Exposed names are accessible to other inline macros invoked inside your macro
body via
$callsite::name.
Notes:
- This feature is experimental; syntax and capabilities may evolve.
- Item-producing macros (structs, enums, functions, etc.) are not yet supported; support will be added in future versions.
- For attributes, derives, and crate-wide transformations, prefer procedural macros (next section).