Exploring ABI stability, @frozen, and library evolution mode
I was poking around in the Swift Standard Library, as one does, and I kept bumping into the @frozen
keyword everywhere. I'd never used this keyword before, and as I tried to get a better handle on it, I found myself traveling further down the rabbit hole than I originally intended.
Let's break down @frozen
and discuss what it does, why it matters, and how it relates to Swift's ABI stability and library evolution mode.
What does @frozen do?
Using @frozen
on your enums
and structs
is simply a promise that the public interface of these types will never change.
It signals to Swift, as well as anyone else viewing the code, that everything about the public interface of these entities is set in stone, guaranteeing that no new cases or properties will be added or removed.
This promise also ensures that the type's layout in memory is fixed. As a result, Swift can boost its performance by depending on the consistency of these types, resulting in quicker memory access and execution.
We'll dive deeper into these optimizations shortly, but first, let's take a look at some examples.
@frozen in enums
Let's say we're working with this non-frozen enum
declared in CoreLocation
:
public enum CLAuthorizationStatus : Int32, @unchecked Sendable {
case notDetermined = 0
case restricted = 1
case denied = 2
case authorizedAlways = 3
case authorizedWhenInUse = 4
}
When we use a switch
statement with this enum
, we'll need to include @unknown default
to safeguard against any behavior (like adding or removing cases) we haven't accounted for.
switch locationAuthorizationStatus {
case .authorizedAlways, .authorizedWhenInUse:
...
case .notDetermined, .denied, .restricted:
...
@unknown default:
print("This is an unkown case. Yikes!")
}
Now, let's compare this to a @frozen enum
from the Swift Standard Library.
By specifying @frozen
, the Result
type informs Swift and other developers that it will only ever have these two cases, defined in exactly this order.
/// A value that represents either a success or a failure, including an
/// associated value in each case.
@frozen
public enum Result<Success: ~Copyable, Failure: Error> {
/// A success, storing a `Success` value.
case success(Success)
/// A failure, storing a `Failure` value.
case failure(Failure)
}
Since all types are now known at compile time, we no longer need to add a default
case.
@frozen in structs
As previously mentioned, applying @frozen
to a struct
means that its public properties, functions, and memory layout will never change.
So, if we had the following @frozen struct
:
@frozen
struct Point {
var x: Double
var y: Double
}
We can be assured that from here on out, Point
's memory layout will always look like this:
[ x (Double) ][ y (Double) ]
Similarly, in this example - as developers consuming the SwiftUI framework - we can trust that the memory layout and public interface of ViewThatFits
will remain unchanged, no matter which future version of SwiftUI we might adopt.
Maintaining a consistent and predictable memory structure is essential for ensuring binary compatibility across different versions of the Swift language and operating systems.
We'll explore these ideas more in the next section.
The following section covers more advanced concepts, so if you're only interested in the basics of @frozen
, this is a reasonable stopping point.
Subscribe to DigitalBunker
New articles straight to your inbox.
No spam. Unsubscribe anytime.
@frozen <> ABI Stability <> Library Evolution Mode
In Swift development, the Application Binary Interface (ABI), @frozen
keyword, and library evolution mode all work together to ensure that Swift libraries can evolve over time without breaking compatibility with existing code that depends on it.
Now that we understand @frozen
, let's dive into the specifics of the ABI and library evolution mode to better understand how they all work together.
What is an Application Binary Interface?
You can think of the Application Binary Interface (ABI) as the rulebook dictating how different aspects of your code communicate at a binary level.
Much like how a car manual details the inner workings of your car, the ABI outlines how data is stored in memory, how functions are called, named, and located within the binary executable, as well as the processes for passing parameters to functions and retrieving return values.
Introduced in Swift 5, "ABI stability" ensures that the binary interface of the language stays consistent across different compiler versions and platforms. This allows applications compiled with one version of Swift to seamlessly interact with libraries compiled with another version.
For example, imagine you have a library compiled with Swift 5, and you're using it in your app compiled with Swift 6. ABI stability ensures they can still talk to each other without you needing to recompile anything because it maintains the integrity of the public interfaces even at a binary level.
In simple terms, ABI stability ensures smooth communication between various versions of Swift code. It's one of the main reasons your app can continue running on newer versions of iOS or Swift without you needing to recompile and submit a new build.
When dynamic libraries and frameworks are compiled (i.e. Foundation, CoreData, MapKit), they too are subject to ABI stability rules. In this context, ABI stability ensures that applications relying on these libraries can use different versions of these libraries at runtime without issue.
The reason that this works is despite updates or changes within the library, the fundamental binary interface—the contract specifying how the application communicates with the library—remains unchanged.
So, maintaining a consistent ABI guarantees not only effective communication between different language versions, but also enables applications using dynamically linked libraries to incorporate updates and bug fixes without disrupting their existing functionality.
Before ABI Stability
The concept of Swift being ABI stable is relatively new. Before Swift 5.0, every application had to include its own copy of the Swift library, resulting in increased app bundle sizes because each application was bundled with its distinct Swift library and ABI.
With the introduction of Swift 5.0, the Swift library was incorporated into the operating system itself which not only enabled compatibility across different versions of Swift, but also helps pave the way for better interoperability in the future.
What is Library Evolution Mode?
Library evolution mode is a Swift compiler feature, activated by the -enable-library-evolution
flag, that allows Swift libraries to be updated over time without breaking compatibility with older versions. This mode enables the compiler to produce additional metadata detailing the structure and functionality of the library's types and functions (including their size, layout, and specifics about how functions are invoked along with their parameters).
Library evolution mode ensures that this metadata remains consistent across different versions of the library, even as the library's implementation details change. This behavior is essential for maintaining ABI stability.
Now, not every library uses library evolution mode. It's typically used for libraries that are intended to be shared and used wildly, such as Apple's UIKit or SwiftUI, which rely on library evolution mode for seamless operation across updates of iOS / macOS.
When compiling in this mode, marking a type as @frozen
restricts the changes that can be made to the type, ensuring that the binary representation of the type remains consistent. Anything that isn't explicitly marked as @frozen
is treated as non-frozen in this mode. This allows developers to signify parts of their library as stable amidst ongoing development. In non-library evolution settings, all types are @frozen
by default.
Wrapping Up
The ABI, @frozen
keyword, and library evolution mode all play a crucial role in the stability of the Swift ecosystem. Together, they contribute to ensuring compatibility across different language and operating system versions thereby creating a smoother development experience.
ABI stability allows for reliable communication at the binary interface and language level, @frozen
protects against accidental changes in developing libraries, and library evolution mode gives developers the flexibility to update libraries while still maintaining backwards compatibility.
Hope you enjoyed this article! If you did, please consider sharing 😊🙏.
If you're interested in more articles about iOS Development & Swift, check out my YouTube channel or follow me on Twitter.
And, if you're an indie iOS developer, make sure to check out my newsletter! Each issue features a new indie developer, so feel free to submit your iOS apps.