Post

Rust Macros vs Swift Macros: A Comparison

Compare Rust's mature macro system with Swift 5.9+ macros and understand the key differences in metaprogramming approaches.

Rust Macros vs Swift Macros: A Comparison

Macros are code that writes code at compile time. Rust’s macro system has been mature since 1.0 (2015), while Swift introduced macros in 5.9 (2023).

To illustrate macro usage in Rust, we’ll use Tokio as our example. Tokio is Rust’s most popular async runtime library, widely used in production. It provides the #[tokio::main] attribute macro that transforms your async main function into working async code. To use it, enable the macros feature:

1
tokio = { version = "1", features = ["macros"] }

This feature flag pulls in tokio-macros as a dependency, which provides the procedural macros.

Two Types of Rust Macros

1. Declarative Macros (macro_rules!)

Pattern-matching that expands to code. Identified by the ! suffix.

1
2
3
4
println!("Hello, {}!", name);

// Expands roughly to:
std::io::stdout().write_fmt(format_args!("Hello, {}!", name));

Common examples: println!(), vec![], format!().

2. Procedural Macros

Functions that take code as input and produce code as output. Three subtypes:

a) Derive Macros - #[derive(...)]

1
2
3
4
5
#[derive(Debug, Clone, Serialize)]
struct User {
    name: String,
    age: u32,
}

The compiler generates implementations automatically. #[derive(Debug)] generates:

1
2
3
4
5
6
7
8
impl std::fmt::Debug for User {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        f.debug_struct("User")
            .field("name", &self.name)
            .field("age", &self.age)
            .finish()
    }
}

b) Attribute Macros - #[something]

Using our Tokio example, the #[tokio::main] attribute macro transforms async code:

1
2
3
4
#[tokio::main]
async fn main() {
    // async code
}

This expands to:

1
2
3
4
5
6
7
8
9
fn main() {
    tokio::runtime::Builder::new_multi_thread()
        .enable_all()
        .build()
        .unwrap()
        .block_on(async {
            // your code here
        })
}

c) Function-like Macros - name!(...)

1
sqlx::query!("SELECT * FROM users WHERE id = $1", user_id)

This validates SQL at compile time against your actual database.

Swift Macro Equivalents

RustSwift 5.9+
#[derive(Debug)]@Observable (attached macro)
#[tokio::main]@attached(member) macros
println!()#stringify() (freestanding macro)
Compile-time SQL checkNo equivalent

Example: Observable vs Derive

1
2
3
4
5
6
7
// Swift 5.9+
@Observable
class User {
    var name: String
    var age: Int
}
// Macro generates observation tracking code
1
2
3
4
5
6
7
// Rust
#[derive(Debug, Clone)]
struct User {
    name: String,
    age: u32,
}
// Macro generates Debug and Clone implementations

Key Differences

AspectRust MacrosSwift Macros
MaturitySince 1.0 (2015)New in 5.9 (2023)
PowerCan do almost anythingMore restricted for safety
Compile-time checksSQL, regex, etc.Limited
Syntax! or #[...]# or @
Error messagesCan be crypticDesigned to be clear

When to Use Each

Rust macros excel at:

  • Compile-time validation (SQL, regex)
  • Reducing repetitive trait implementations
  • Code generation from external data sources

Swift macros work best for:

  • Observation tracking (@Observable)
  • Property wrapper-like transformations
  • AST transformations with clear error messages

☕ Support My Work

If you found this post helpful and want to support more content like this, you can buy me a coffee!

Your support helps me continue creating useful articles and tips for fellow developers. Thank you! 🙏

This post is licensed under CC BY 4.0 by the author.