Backporting new features with @_alwaysEmitIntoClient

Swift SwiftUI
27 July 2021

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!

⭑⭑⭑⭑⭑

Further Reading

Explore Swift

Browse all

Explore SwiftUI

Browse all