What does -enable-library-evolution change, and why would you not turn it on for an app?
Predict first
Which module should enable library evolution?
// A: the app target
// B: a closed-source framework shipped as an XCFramework
// C: a helper module built together with the app
- A
- B
- C
Mental model. Without it, the library is a sealed set of blueprints that the client copies in. With it, the library is a service the client calls and asks, “how big are you today?”
By default Swift compiles a module for its own build: it freely bakes in struct layouts, enum case numbers and function bodies, and a client must be rebuilt against the exact same version. -enable-library-evolution makes the module resilient: public structs and enums are treated as layouts that may change. Clients no longer hard-code a struct’s size or field offsets; they ask the runtime through metadata and value witnesses. Adding a stored property, reordering fields, adding a method, or adding an enum case (to a non-@frozen enum, which forces clients to write @unknown default) is then ABI-compatible.
The cost is performance and flexibility. Access to a resilient type’s fields goes through indirection, values of unknown size are passed in memory, switches over enums cannot assume the case list, and nothing is inlined or specialised across the boundary unless you mark it @inlinable (and then you have promised that body forever). So you enable it for binary frameworks that ship separately from their clients (XCFrameworks, SDKs) and leave it off for an app or for modules built together, where the extra indirection buys nothing. Mark types that will never change @frozen.
Only a module whose clients are compiled separately, against a version you will later replace, benefits from the promise.
| Default module | With library evolution | |
|---|---|---|
| Struct layout | baked into clients | looked up at runtime |
| Add a stored property | breaks clients | compatible |
| Add an enum case | clients must rebuild | compatible if not @frozen |
| Cross-module inlining | bodies visible to clients | only for @inlinable |
| Clients rebuild for each version | yes | no |
Common traps
- Enabling it for an internal module. You pay the indirection cost for a compatibility promise nobody needs.
- Marking a type
@frozen“for speed” and then adding a field. You break every client already compiled against it.
They might follow up with
- What stays source-compatible but not ABI-compatible without it? Almost every layout change: adding a stored property or reordering fields.
- How do clients get an unfrozen enum right? With
@unknown default, so a new case produces a warning at build time but still runs.
Related questions
- 5.4.2 What is a .swiftinterface file, and what does it show about @inlinable and @frozen?
- 1.10.4 What is ABI stability vs source compatibility vs module stability?
- 1.10.5 What does @frozen on an enum mean for you as an API author?
- 1.10.6 You need to add a field to a public struct in a binary SPM library used by five apps. What do you consider?
What is a .swiftinterface file, and what does it show about… →