System Programming Interfaces (SPI) in Swift Explained

I am a Software Engineer working on open source and enterprise mobile SDKs for iOS and MacOS developers written in Swift. From 🇩🇪 and happily living in 🇺🇸
In this blog post, I will explain the experimental Swift feature named System Programming interfaces and why this should interest library developers.
Defintion
Apple gives the following definition.
| API | SPI |
|---|---|
An entity in a library that a client may use, or the collection of all such entities in a library. Marked public or open in Swift. Stands for Application Programming Interface. |
A subset of API that is only available to certain clients. Stands for System Programming Interface. |
I rather like to think that SPI stands for Secret Programming Interface ;)
As a library developer, you can ship experimental features to dedicated clients (e.g. in-house teams) while hiding those features to other clients and all within the same build artifact.
Or maybe you (ab)use @testable and you are looking for an alternative...
SPI is experimental
Warning: Apple discourages using underscored attributes because those semantics are subject to change and most likely need to go through the Swift evolution process before being stabilized.
Nevertheless, plenty of projects do use underscored attributes.
Declare SPI
Swift 5.3 introduced an experimental attribute @_spi(spiName) to mark a declaration as SPI.
// Module "Shopping"
public struct ShoppingCart {
public init() {}
public func payCash() {}
@_spi(PayPal) public func payWithPayPal() {}
}
Consume SPI
Clients that import Shopping will have access to ShoppingCart, its initializer, and ShoppingCart.payCash, but they won't see payWithPayPal function.
Clients can access SPI by declaring the import as @_spi(spiName) import Module.
Therefore library developers have to share the spiName with those clients.
@_spi(PayPal) import Shopping
let s = Shopping()
s.payWithPayPal()
Multiple SPI Names
As a library developer, you can use whatever SPI name you'd like and use as many as you'd like within the module.
// Module "Shopping"
public struct ShoppingCart {
public init() {}
public func payCash() {}
@_spi(PayPal) public func payWithPayPal() {}
@_spi(Bitcoin) public func payWithBitcoin() {}
}
@_spi(PayPal) @_spi(Bitcoin) import Shopping
let s = ShoppingCart()
s.payWithPayPal()
s.payWithBitcoin()
Impact to .swiftinterface
Modules exposing SPI and using library evolution generate an additional .private.swiftinterface file in addition to the usual .swiftinterface file. This private interface exposes both API and SPI.
Shopping.swiftinterface
// swift-interface-format-version: 1.0
// swift-compiler-version: Apple Swift version 5.7 (swiftlang-5.7.0.127.4 clang-1400.0.29.50)
// swift-module-flags: -target arm64-apple-macosx10.13 -enable-objc-interop -enable-library-evolution -swift-version 5 -Onone -module-name Shopping
import Swift
import _Concurrency
public struct ShoppingCartItem {
public init()
}
public struct ShoppingCart {
public init()
public func payCash()
}
Shopping.private.swiftinterface
// swift-interface-format-version: 1.0
// swift-compiler-version: Apple Swift version 5.7 (swiftlang-5.7.0.127.4 clang-1400.0.29.50)
// swift-module-flags: -target arm64-apple-macosx10.13 -enable-objc-interop -enable-library-evolution -swift-version 5 -Onone -module-name Shopping
import Swift
import _Concurrency
public struct ShoppingCartItem {
public init()
}
public struct ShoppingCart {
public init()
public func payCash()
@_spi(PayPal) public func payWithPayPal()
@_spi(Bitcoin) public func payWithBitcoin()
}
@_spi(PayPal) public struct PayPalProvider {
@_spi(PayPal) public init()
}
SPI specific to a platform and/or version
The original implementation ...
... was enhanced in Swift 5.7 by an additional, experimental attribute @_spi_available(platform, version)
Like @available, this attribute indicates a declaration is available only as an SPI. This implies several behavioral changes compared to regular @available:
- Type checker diagnoses when a client accidentally exposes such a symbol in library APIs.
- When emitting public interfaces,
@_spi_availableis printed as@available(platform, unavailable). - ClangImporter imports ObjC macros
SPI_AVAILABLEand__SPI_AVAILABLEto this attribute.
// Module "Shopping"
public struct ShoppingCart {
public init() {}
@_spi_available(watchOS 9, *)
@available(tvOS, unavailable)
public private(set) var items = [ShoppingCartItem]()
public func payCash() {}
@_spi(PayPal) public func payWithPayPal() {}
@_spi(Bitcoin) public func payWithBitcoin() {}
}
Shopping.swiftinterface
// swift-interface-format-version: 1.0
// swift-compiler-version: Apple Swift version 5.7 (swiftlang-5.7.0.127.4 clang-1400.0.29.50)
// swift-module-flags: -target arm64-apple-macosx10.13 -enable-objc-interop -enable-library-evolution -swift-version 5 -Onone -module-name Shopping
import Swift
import _Concurrency
public struct ShoppingCartItem {
public init()
}
public struct ShoppingCart {
public init()
@available(watchOS, unavailable)
@available(tvOS, unavailable)
public var items: [Shopping.ShoppingCartItem] {
get
}
public func payCash()
}
@_spiOnly
And support for SPI gets even more enhanced in the upcoming Swift 5.8.
In the development snapshot, you can see a new, experimental attribute @_spiOnly.
It marks an import to be used in SPI and implementation details only.

To use @_spiOnly you are required to set the frontend flag -experimental-spi-only-imports. Below is an example of how to specify it in a Package.swift file.
targets: [
.target(
name: "Shopping",
dependencies: [],
swiftSettings: [
.unsafeFlags(["-enable-library-evolution"]),
.unsafeFlags(["-Xfrontend", "-experimental-spi-only-imports"]) // requires Swift 5.8
]
),
]
The import statement will only be printed in the private .swiftinterface and skipped in the public .swiftinterface. Any use of imported types and declarations in API will be diagnosed.
So when you are using @_spiOnly import CryptoKit in module Shopping then Shopping.swiftinterface won't include that information.
// swift-interface-format-version: 1.0
// swift-compiler-version: Apple Swift version 5.8-dev (LLVM 338153f292479fa, Swift b752d7883c26cea)
// swift-module-flags: -target arm64-apple-macosx10.13 -enable-objc-interop -enable-library-evolution -swift-version 5 -Onone -module-name Shopping
import Swift
import _Concurrency
import _StringProcessing
But Shopping.private.swiftinterface will do.
// swift-interface-format-version: 1.0
// swift-compiler-version: Apple Swift version 5.8-dev (LLVM 338153f292479fa, Swift b752d7883c26cea)
// swift-module-flags: -target arm64-apple-macosx10.13 -enable-objc-interop -enable-library-evolution -swift-version 5 -Onone -module-name Shopping
/*@_spiOnly*/ import CryptoKit
import Swift
import _Concurrency
import _StringProcessing
public struct ShoppingCartItem {
Summary
Swift, starting with 5.3, allows library developers to hide declarations and make them available for specific clients only. Those experimental capabilities, known as System Programming Interfaces, were and do currently get enhanced in later Swift versions.




