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 and Cargo.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"

Listing 11-5: Example Scarb.toml file needed for building a procedural macro.

# 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]

Listing 11-6: Example Cargo.toml file needed for building a procedural macro.

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 11-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()))
}

Listing 11-7: Code for creating inline pow procedural macro

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,
        );
    }

Listing 11-8: Using pow procedural macro

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,
}

Listing 11-9: Using derive procedural macro

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.