Post

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 vs Swift: Feature Flags and Conditional Compilation

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

AspectRust FeaturesSwift Equivalent
Compile-time selection✅ Per-dependency❌ Global only
Per-dependency control✅ Yes❌ Not available
Reduces binary size✅ Automatic❌ Requires separate packages
Declaration locationCargo.tomlBuild 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:

  1. Creating separate packages for optional functionality
  2. Using #if for platform/configuration differences
  3. 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! 🙏

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