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_available
is printed as@available(platform, unavailable)
. - ClangImporter imports ObjC macros
SPI_AVAILABLE
and__SPI_AVAILABLE
to 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.