Writing Procedural Macros
WARNING
Procedural macros, by design, introduce a lot of overhead during the compilation. They may also be harder to maintain. Prefer the declarative inline macros written directly in Cairo, unless you have a specific reason to use procedural macros. There are several reasons for this:
- Compilation overhead: procedural macros are Rust crates compiled into shared libraries, adding a full Rust compilation step (via Cargo) on top of the Cairo build and significantly increasing build times.
- Rust toolchain dependency: anyone using your macro must have the Rust toolchain (Cargo) installed on their machine (unless the macro is distributed as a precompiled shared library, which is not always the case).
- Harder to debug: errors in macro expansion surface as confusing Cairo compiler diagnostics, making them difficult to diagnose.
- Higher maintenance burden: they require knowledge of both Rust and Cairo, and the FFI boundary between them adds complexity.
Please see the declarative macros chapter in Cairo Book for more information.
INFO
To use procedural macros, you need to have Rust toolchain (Cargo) installed on your machine. Please see Rust installation guide for more information.
Scarb procedural macros are, in fact, Rust functions that take Cairo code as input and return modified Cairo code as an output.
A procedural macro is implemented as a Rust library which defines functions that implement these transformations (later called macro expansions). This Rust code is then compiled into a shared library (shared object) and loaded into Scarb process memory during the Cairo project compilation. Scarb will call expansions from the loaded shared library, thus allowing you to inject custom logic to the Cairo compilation process.
Procedural macro author perspective
To implement a procedural macro, a programmer has to:
- Create a new package, with a
Scarb.tomlmanifest file,Cargo.tomlmanifest file and asrc/directory besides. - The Scarb manifest file must define a
cairo-plugintarget type. - The Cargo manifest file must define a
crate-type = ["cdylib"]on[lib]target. - Write a Rust library, inside the
src/directory that implements the procedural macro API. - A Rust crate exposing an API for writing procedural macros is published for programmers under the name
cairo-lang-macro. This crate must be added to theCargo.tomlfile. - The Rust library contained in the package has to implement a function responsible for code expansion.
- This function accepts a
TokenStreamas an input and returns aProcMacroResultas an output, both defined in the helper library. - The result struct contains the transformed
TokenStream. Three kinds of results are possible:- If the
TokenStreamis the same as the input, the AST is not modified. - If the
TokenStreamis different from the input, the input is replaced with the generated code. - If the
TokenStreamis empty, the input is removed.
- If the
- Alongside the new TokenStream, a procedural macro can emit compiler diagnostics, auxiliary data and full path identifiers, described in detail in advanced macro usage section.
TokenStream is an abstraction that represents a piece of Cairo source code as a sequence of tokens. Rather than exposing the compiler's internal AST directly — which would tightly couple procedural macros to Cairo's internal representations and make them fragile across compiler versions — TokenStream provides a stable, serializable interface that can be safely passed across the FFI boundary between Scarb and the macro's shared library. This means macro authors work with raw Cairo source text rather than compiler internals, keeping the API simple and stable.
Creating procedural macros with helpers
The API for writing procedural macros for Cairo is defined in the cairo-lang-macro crate. This interface includes both structures shared between the procedural macro and Scarb, as well as a set of helper macros that hide the details of the FFI communication from the procedural macro author.
These three macro helpers are:
#[inline_macro]- Implements an expression macro. Should be used on function that accepts single token stream.#[attribute_macro]- Implements an attribute macro. Should be used on function that accepts two token streams - first for the attribute arguments (#[macro(arguments)]) and second for the item the attribute is applied to.#[derive_macro]- Implements a derive macro. Should be used on function that accepts single token stream, the item the derive is applied to. Note that derives cannot replace the original item, but rather add new items to the module.
You can find documentation for these helpers in attribute macros section of the cairo-lang-macro crate documentation.
Building the output with quote!
The primary way to produce a TokenStream output from your macro is the quote! macro provided by cairo-lang-macro. It lets you write Cairo code inline in Rust and interpolate Rust variables directly into it using the #variable syntax:
use cairo_lang_macro::{attribute_macro, quote, ProcMacroResult, TokenStream};
#[attribute_macro]
pub fn my_macro(_args: TokenStream, body: TokenStream) -> ProcMacroResult {
ProcMacroResult::new(quote! {
const MY_CONST: u32 = 42;
#body
})
}When applied to a function in Cairo, the macro prepends the constant declaration before the annotated item:
#[my_macro]
fn example() -> u32 {
MY_CONST
}This expands to:
const MY_CONST: u32 = 42;
fn example() -> u32 {
MY_CONST
}Any variable used with # must implement the ToPrimitiveTokenStream trait. This includes TokenStream itself, as well as syntax nodes from the cairo-lang-syntax AST — making it straightforward to embed pieces of Cairo code from the input directly into the output.
While you can also construct a TokenStream manually by wrapping individual tokens in Token, TokenTree, and TokenStream, this is tedious for anything beyond the most trivial output and should be avoided in favour of quote!.
See the examples for full end-to-end usage of quote!, including composing token streams and working with syntax nodes.
Minimal example: a macro that removes code
# Scarb.toml
[package]
name = "remove_item_macro"
version = "0.1.0"
[cairo-plugin]# Cargo.toml
[package]
name = "remove_item_macro"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
crate-type = ["cdylib"]
[dependencies]
cairo-lang-macro = "0.2"// src/lib.rs
use cairo_lang_macro::{ProcMacroResult, TokenStream, attribute_macro};
#[attribute_macro]
pub fn remove_item(_args: TokenStream, _body: TokenStream) -> ProcMacroResult {
ProcMacroResult::new(TokenStream::empty())
}You can test this macro by annotating some function with it:
# hello_world/Scarb.toml
[package]
name = "hello_world"
version = "0.1.0"
edition = "2023_10"
[dependencies]
remove_item_macro = { path = "../remove_item_macro" }// hello_world/src/lib.cairo
fn main() -> u32 {
fib(16)
}
#[remove_item]
fn fib(mut n: u32) -> u32 {
let mut a: u32 = 0;
let mut b: u32 = 1;
while n != 0 {
n = n - 1;
let temp = b;
b = a + b;
a = temp;
};
a
}And the compilation will fail with following error:
Compiling hello_world v0.1.0 (../hello_world/Scarb.toml)
error[E0006]: Function not found.
--> ../hello_world/src/lib.cairo:2:5
fib(16)
^^^
error: could not compile `hello_world` due to previous errorMaybe it's not the most productive code you wrote, but the function has been removed during the compilation.
Example projects
See the examples for working end-to-end macro implementations.