System Programming Interfaces (SPI) in Swift Explained

System Programming Interfaces (SPI) in Swift Explained

Β·

5 min read

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.

Screen Shot 2022-10-01 at 5.13.57 PM.png

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.

SPI

Did you find this article valuable?

Support Swifty Tech by Marco Eidinger by becoming a sponsor. Any amount is appreciated!