Rust vs Swift: Feature Flags and Conditional Compilation
Compare Rust's compile-time feature flags with Swift's conditional compilation and understand when to use each approach.
Rust has a powerful feature flag system that enables compile-time optional functionality. Swift developers may wonder how this compares to #if conditional compilation.
What Are Rust Features?
From the Cargo documentation:
“Cargo features provide a mechanism to express conditional compilation and optional dependencies.”
Features let crate authors mark parts of their library as optional. Consumers choose what to include at dependency declaration time.
How Rust Features Work
Crate authors define features in Cargo.toml:
1
2
3
4
5
6
# Inside tokio's Cargo.toml (simplified)
[features]
default = [] # Nothing enabled by default
rt-multi-thread = ["rt"] # Multi-threaded runtime
macros = ["tokio-macros"] # Enables proc macros
full = ["rt-multi-thread", "macros", "net", "io-util", "time", "signal"]
When depending on tokio, you choose what to include:
1
2
3
4
5
# Minimal - only what we need
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
# Everything - larger binary
tokio = { version = "1", features = ["full"] }
Code guarded by features uses #[cfg] attributes:
1
2
3
4
5
#[cfg(feature = "full")]
pub mod advanced_stuff { }
#[cfg(feature = "macros")]
pub use tokio_macros::*;
Swift’s Conditional Compilation
Swift uses #if preprocessor directives:
1
2
3
4
5
6
7
8
9
10
11
12
13
#if canImport(UIKit)
import UIKit
#endif
#if DEBUG
print("Debug mode")
#endif
#if os(Linux)
import Glibc
#else
import Darwin
#endif
These are evaluated at compile time but differ fundamentally from Rust features.
Key Differences
| Aspect | Rust Features | Swift Equivalent |
|---|---|---|
| Compile-time selection | ✅ Per-dependency | ❌ Global only |
| Per-dependency control | ✅ Yes | ❌ Not available |
| Reduces binary size | ✅ Automatic | ❌ Requires separate packages |
| Declaration location | Cargo.toml | Build settings / compiler flags |
The Package Split Problem
In Rust, a single crate can expose multiple features:
1
2
# One crate, multiple features
serde = { version = "1.0", features = ["derive"] }
In Swift, this requires separate packages:
1
2
3
// Package.swift - separate packages for optional functionality
.package(url: "https://github.com/Alamofire/Alamofire", from: "5.0.0"),
.package(url: "https://github.com/Alamofire/AlamofireImage", from: "4.0.0"),
AlamofireImage is a separate package, not a feature flag on Alamofire.
Closest Swift Approximation
Swift Package Manager doesn’t support feature flags, but you can simulate them with build settings:
1
2
3
4
5
6
7
// Package.swift
.target(
name: "MyLibrary",
swiftSettings: [
.define("FEATURE_ANALYTICS", .when(configuration: .release))
]
)
Then in code:
1
2
3
4
5
6
7
8
9
#if FEATURE_ANALYTICS
import AnalyticsFramework
func trackEvent(_ name: String) {
Analytics.track(name)
}
#else
func trackEvent(_ name: String) { }
#endif
This requires the consumer to set the same flags—not ideal.
Binary Size Impact
Rust features directly affect binary size. Enabling fewer features means smaller binaries:
1
2
3
4
5
# Minimal - includes only selected modules
tokio = { version = "1", features = ["rt-multi-thread"] }
# Full - includes everything
tokio = { version = "1", features = ["full"] }
Swift lacks this granularity. Importing a framework includes all its code unless you split into separate packages.
When This Matters
Rust feature flags excel when:
- Building minimal binaries for embedded systems
- Reducing compile times by excluding unused functionality
- Providing optional integrations (database drivers, serialization formats)
Swift’s approach works when:
- Platform-specific code (
#if os(iOS)) - Debug/release differences (
#if DEBUG) - Testing configurations (
#if targetEnvironment(simulator))
Summary
Rust features provide dependency-level compile-time selection that Swift Package Manager lacks. Swift developers compensate by:
- Creating separate packages for optional functionality
- Using
#iffor platform/configuration differences - Accepting larger binaries with unused code
For iOS development, this rarely impacts app size significantly. For command-line tools or embedded systems, Rust’s approach offers meaningful advantages.
☕ 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! 🙏