Exploring ABI stability, @frozen, and library evolution mode

Exploring ABI stability, @frozen, and library evolution mode
Photo by diego / Unsplash

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.
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:

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.

// SwiftUI
@frozen public struct ViewThatFits<Content> : View where Content : View {

@frozen is especially useful for public structs / enums within libraries or frameworks.

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.

@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.

Ace The iOS Interview
The best investment for landing your dream iOS jobHey there! My name is Aryaman Sharda and I started making iOS apps way back in 2015. Since then, I’ve worked for a variety of companies like Porsche, Turo, and Scoop Technologies just to name a few. Over the years, I’ve mentored junior engineers, bui…
Indie Watch
Indie Watch is an exclusive weekly hand-curated newsletter showcasing the best iOS, macOS, watchOS, and tvOS apps from developers worldwide.

Subscribe to Digital Bunker

Don’t miss out on the latest issues. Sign up now to get access to the library of members-only issues.
[email protected]