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 namedExperimental
.// 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 SPIExperimental
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
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
).
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:
See the implementation on GitHub for more info. ↩