@_spi(Experimental) in Swift

System Programming Interfaces (SPI) in Swift


I’ve learned about this feature from Twitter. It’s still experimental and not publicly supported, but it’s there since Xcode 12 and as far as I can tell, works well1.

@_spi huh?

So, what is this all about?

If you ever wrote unit tests for Swift code, you’ve probably used @testable import. It allows access to internal declarations of the module under test as if these were public (more details in docs).

I understand @_spi feature as a generalization of @testable. The logic is a bit different though. You mark a public declaration with @_spi(Identifier) (where Identifier is arbitrary) and this declaration is not visible outside the module.

If some code uses @_spi(Identifier) import Module with matching identifier, the declarations marked with @_spi become visible as if they were regular public declarations.

Consider an example from the implementation PR:

In the following example, MyLib defines a function under the SPI named Experimental.

// MyLib
@_spi(Experimental) public func newExperimentalService() {}

The SPI function newExperimentalService is hidden from clients that imports the module normally.

import MyLib

newExperimentalService() // Error: use of unresolved identifier

However, clients that imports MyLib and its SPI Experimental have access to all decls with the attribute ` @_spi(Experimental) declared in MyLib`. This is a way for the library clients to opt-in using the SPI.

@_spi(Experimental) import MyLib

newExperimentalService() // Ok

What for

This can be really useful in Swift package setups where a package is split into multiple targets. Targets importing each other have to make declarations public, but not all of these declarations should be used by the package clients. For example, I may have the following package:

let package = Package(
    name: "MyPackage",
    ...
    products: [
        .library(
            name: "MyLibrary",
            type: .dynamic,
            targets: [
                "Database",
            ]
        ),
    ],
    targets: [
        .target(name: "Utils"),
        .target(
            name: "Database",
            dependencies: ["Utils"],
        )
    ]
)

Clients using MyPackage would

import Database

and use its public API, but since Database depends on Utils target, clients could also

import Utils

and use some of the internal APIs which are not suitable for general use.

These APIs still need to be public because Database target should be able to access them, and using a special @_spi for them could prevent their usage by general public (at least unless they’re not using @_spi import).

Support

According to Doug Gregor’s tweet, the feature is there since Xcode 12. You can use it in production at your own risk.

As I remember trying this feature some time ago, it worked, but Xcode behaved a bit strange with @_spi marked declarations – like declarations not appearing in code completion suggestions etc. But I’ve checked in Xcode 13.2.1, and it looks like the code completion works as expected. Maybe this was an indexing issue that time.


Notes:

  1. See the implementation on GitHub for more info. 


Project maintained by wanderwaltz Hosted on GitHub Pages — Based on theme by mattgraham