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.
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
| Rust | Swift 5.9+ |
|---|---|
#[derive(Debug)] | @Observable (attached macro) |
#[tokio::main] | @attached(member) macros |
println!() | #stringify() (freestanding macro) |
| Compile-time SQL check | No 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
| Aspect | Rust Macros | Swift Macros |
|---|---|---|
| Maturity | Since 1.0 (2015) | New in 5.9 (2023) |
| Power | Can do almost anything | More restricted for safety |
| Compile-time checks | SQL, regex, etc. | Limited |
| Syntax | ! or #[...] | # or @ |
| Error messages | Can be cryptic | Designed 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! 🙏