Procedural Macros
Cairo procedural macros are Rust functions that takes Cairo code as input and returns a modified Cairo code as output, enabling developers to extend Cairo's syntax and create reusable code patterns. In the previous chapter, we discussed some Cairo built-in macros like println!
, format!
, etc. In this chapter, we will explore how to create and use custom procedural macros in Cairo.
Types of Procedural Macros
There are three types of procedural macros in Cairo:
-
Expression Macros (
macro!()
): These macros are used like function calls and can generate code based on their arguments. -
Attribute Macros (
#[macro]
): These macros can be attached to items like functions or structs to modify their behavior or implementation. -
Derive Macros (
#[derive(Macro)]
): These macros automatically implement traits for structs or enums.
Creating a Procedural Macro
Before creating or using procedural macros, we need to ensure that the necessary tools are installed:
- Rust Toolchain: Cairo procedural macros are implemented in Rust, so we will need the Rust toolchain setup on our machine.
- To set up Rust, visit rustup and follow the installation instructions for your operating system.
Since procedural macros are in fact Rust functions, we will need to add a Cargo.toml
file to the root directory ( same level as the Scarb.toml
file ). In the Cargo.toml
file, we need to add a crate-type = ["cdylib"]
on the [lib]
target, and also add the cairo-lang-macro
crate as a dependency.
It is essential that both the
Scarb.toml
andCargo.toml
have the same package name, or there will be an error when trying to use the macro.
Below is an example of the Scarb.toml
and Cargo.toml
files:
# Scarb.toml
[package]
name = "no_listing_15_pow_macro"
version = "0.1.0"
edition = "2024_07"
# See more keys and their definitions at https://docs.swmansion.com/scarb/docs/reference/manifest.html
[cairo-plugin]
[dependencies]
[dev-dependencies]
cairo_test = "2.9.1"
# Cargo.toml
[package]
name = "no_listing_15_pow_macro"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
bigdecimal = "0.4.5"
cairo-lang-macro = "0.1"
cairo-lang-parser = "2.9.1"
cairo-lang-syntax = "2.9.1"
[workspace]
Also notice that you can also add other rust dependencies in your Cargo.toml
file. In the example above, we added the bigdecimal
, cairo-lang-parser
and cairo-lang-syntax
crates as a dependencies.
Listing 12-7 shows the rust code for creating an inline macro in Rust:
use bigdecimal::{num_traits::pow, BigDecimal};
use cairo_lang_macro::{inline_macro, Diagnostic, ProcMacroResult, TokenStream};
use cairo_lang_parser::utils::SimpleParserDatabase;
/// Compile-time power function.
///
/// Takes two arguments, `x, y`, calculates the value of `x` raised to the power of `y`.
///
/// ```
/// const MEGABYTE: u64 = pow!(2, 20);
/// assert_eq!(MEGABYTE, 1048576);
/// ```
#[inline_macro]
pub fn pow(token_stream: TokenStream) -> ProcMacroResult {
let db = SimpleParserDatabase::default();
let (parsed, _diag) = db.parse_virtual_with_diagnostics(token_stream);
let macro_args: Vec<String> = parsed
.descendants(&db)
.next()
.unwrap()
.get_text(&db)
.trim_matches(|c| c == '(' || c == ')')
.split(',')
.map(|s| s.trim().to_string())
.collect();
if macro_args.len() != 2 {
return ProcMacroResult::new(TokenStream::empty()).with_diagnostics(
Diagnostic::error(format!("Expected two arguments, got {:?}", macro_args)).into(),
);
}
let base: BigDecimal = match macro_args[0].parse() {
Ok(val) => val,
Err(_) => {
return ProcMacroResult::new(TokenStream::empty())
.with_diagnostics(Diagnostic::error("Invalid base value").into());
}
};
let exp: usize = match macro_args[1].parse() {
Ok(val) => val,
Err(_) => {
return ProcMacroResult::new(TokenStream::empty())
.with_diagnostics(Diagnostic::error("Invalid exponent value").into());
}
};
let result: BigDecimal = pow(base, exp);
ProcMacroResult::new(TokenStream::new(result.to_string()))
}
The essential dependency for building a cairo macro cairo_lang_macro
is imported here with inline_macro, Diagnostic, ProcMacroResult, TokenStream
. The inline_macro
is used for implementing an expression macro, ProcMacroResult
is used for the function return, TokenStream
as the input, and the Diagnostic
is used for error handling. We also use the cairo-lang-parser
crate to parse the input code. Then the pow
function is defined utilizing the imports to create a macro that calculate the pow based on the TokenStream
input.
How to Use Existing Procedural Macros
Note: While you need Rust installed to use procedural macros, you don't need to know Rust programming to use existing macros in your Cairo project.
Incorporating an Existing Procedural Macro Into Your Project
Similar to how you add a library dependency in your Cairo project, you can also add a procedural macro as a dependency in your Scarb.toml
file.
#[test]
fn test_pow_macro() {
assert_eq!(super::TWO_TEN, 144);
assert_eq!(pow!(10, 2), 100);
assert_eq!(pow!(20, 30), 1073741824000000000000000000000000000000_felt252);
assert_eq!(
pow!(2, 255),
57896044618658097711785492504343953926634992332820282019728792003956564819968_u256,
);
}
You'd notice a pow!
macro, which is not a built-in Cairo macro being used in this example above. It is a custom procedural macro that calculates the power of a number as defined in the example above on creating a procedural macro.
#[derive(Add, AddAssign, Sub, SubAssign, Mul, MulAssign, Div, DivAssign, Debug, Drop, PartialEq)]
pub struct B {
pub a: u8,
pub b: u16,
}
The example above shows using a derive macro on a struct
B, which grants the custom struct the ability to perform addition, subtraction, multiplication, and division operations on the struct.
let b1 = B { a: 1, b: 2 };
let b2 = B { a: 3, b: 4 };
let b3 = b1 + b2;
Summary
Procedural macros offer a powerful way to extend Cairo's capabilities by leveraging Rust functions to generate new Cairo code. They allow for code generation, custom syntax, and automated implementations, making them a valuable tool for Cairo developers. While they require some setup and careful consideration of performance impacts, the flexibility they provide can significantly enhance your Cairo development experience.