Backporting new features with @_alwaysEmitIntoClient
In SwiftUI patterns evolution: view builders, we've covered a new pattern introduced in Xcode 13. At the end of the article, we've seen how relatively simple it was to backport such patterns via extensions.
New in Xcode 13 beta 3, most of those extensions are no longer needed: the SwiftUI team back-ported this pattern for us. How is it possible? Let's find out.
The change
Prior to Xcode 13 beta 3, the new pattern was limited to the newly announced OSes, for example:
@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) // 👈🏻
extension Section where Parent: View, Content: View, Footer: View {
public init(
@ViewBuilder content: () -> Content,
@ViewBuilder header: () -> Parent,
@ViewBuilder footer: () -> Footer
)
}
New in Xcode 13 beta 3, those definitions are now available to all OSes supporting SwiftUI:
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) // 👈🏻
extension Section where Parent: View, Content: View, Footer: View {
public init(
@ViewBuilder content: () -> Content,
@ViewBuilder header: () -> Parent,
@ViewBuilder footer: () -> Footer
)
}
Older OSes did not magically learn the new pattern: once the OS is shipped, the system framework capabilities for that OS are sealed and cannot be changed (strictly speaking, this is mostly true).
This is all we get from SwiftUI's headers. Let's dig deeper.
Swift Interfaces and ABI stability
A core part of Swift's ABI stability is Module Stability, which lets apps use libraries/frameworks even when compiled with different Swift versions.
On top of Module Stability, we have Library Evolution, which lets libraries change while remaining binary-compatible with previous versions.
For app developers, all of this comes down to library Swift interfaces, also known as .swiftinterface
file(s), and what's declared on them.
System frameworks Swift interfaces
Xcode always comes with Apple's OSes framework binaries, including SwiftUI's, which also contains the .swiftinterface
files mentioned above.
The location might vary, currently it's
Xcode.app/Contents/Developer/Platforms/YOUR_PLATFORM_HERE.platform/Developer/SDKs/YOUR_PLATFORM_HERE.sdk/System/Library/Frameworks/
Continuing with SwiftUI's Section
example, prior to Xcode 13 beta 3, the Swift interface declared:
@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
extension Section where Parent: View, Content: View, Footer: View {
public init(
@ViewBuilder content: () -> Content,
@ViewBuilder header: () -> Parent,
@ViewBuilder footer: () -> Footer
)
}
Which translates 1-1 to what we see in SwiftUI's headers.
From Xcode 13 beta 3, the previous definition becomes:
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
extension Section where Parent: View, Content: View, Footer: View {
@_alwaysEmitIntoClient
public init(
@ViewBuilder content: () -> Content,
@ViewBuilder header: () -> Parent,
@ViewBuilder footer: () -> Footer
) {
self.init(header: header(), footer: footer(), content: content)
}
}
We have two significant changes, the initializer...
- ... comes with its complete implementation
- ... is associated with the
@_alwaysEmitIntoClient
attribute
Going back to Swift's Module Stability and Library Evolution, this definition is possible because it's entirely additive:
- the new initializer is a convenience initializer, using another
public
initializer, which has been available since iOS 13 (and equivalent in other platforms) - since the interface exposes the complete implementation, we can now use this new API even in older OSes
All of this is possible thanks to the @_alwaysEmitIntoClient
attribute.
@_alwaysEmitIntoClient
The underscore prefix means that this attribute hasn't gone through Swift Evolution just yet.
From what we know so far, it looks like @_alwaysEmitIntoClient
is similar to @inlinable
, which also exposes the associated implementation as part of the module’s public interface (regardless of whether the declaration is public
or internal
).
There are two major differences between @_alwaysEmitIntoClient
and @inlinable
:
- an
@inlinable
declaration is part of the library binary. Removing an@inlinable
symbol is both a binary-breaking and a source-breaking change - the compiler is allowed to replace calls to an
@inlinable
symbol with a copy of the symbol’s implementation at call site
On the contrary:
- an
@_alwaysEmitIntoClient
declaration is never part of a library binary. Removing a@_alwaysEmitIntoClient
symbol is a binary-compatible and source-breaking change - an
@_alwaysEmitIntoClient
implementation will always get copied into the client (e.g., an app) binary
This last point explains why using @_alwaysEmitIntoClient
allows back-porting new definitions to older OSes:
it's not the framework that has the new capabilities. It's the app that brings them within its binary.
There are a few drawbacks with this approach:
@_alwaysEmitIntoClient
code that’s compiled into apps cannot be improved/fixed via OS updates. The only way to update those is for the app to be rebuilt with a newer@_alwaysEmitIntoClient
implementation@_alwaysEmitIntoClient
brings unnecessary code size bloat, as these implementations, again, are never part of the library binary and must come within the app binaries using them, even on future OSes@_alwaysEmitIntoClient
does not work with new types
It's up to the library maintainers to decide whether the trade-off is worth it.
Beside for view builders, among others, SwiftUI also uses
@_alwaysEmitIntoClient
in all the new styles declaration, allowing us to use.toggleStyle(.switch)
instead of the legacy.toggleStyle(SwitchToggleStyle())
even when targeting iOS 13 (and equivalent in other platforms).
Conclusions
We've seen how binary frameworks can make additive changes to their API while remaining binary-compatible with previous versions:
SwiftUI uses this pattern quite a bit (there are 282 @_alwaysEmitIntoClient
matches in Xcode 13b3 SwiftUI interface), have you spotted @_alwaysEmitIntoClient
elsewhere? Please let me know via email or Twitter!