How do package access and @_spi differ from public and internal, and what do they look like in a .swiftinterface?
Predict first
What do the interfaces and the clients say?
// === Lib.swift ===
public struct Api {
public init() {}
public func open() -> Int { 1 }
package func shared() -> Int { 2 }
@_spi(Internal) public func secret() -> Int { 3 }
}
// === A.swift ===
import Lib
print(Api().open(), Api().shared())
// === B.swift ===
@_spi(Internal) import Lib
print(Api().secret())
// === C.swift ===
import Lib
print(Api().secret())
// === run.sh ===
swiftc -swift-version 6 -parse-as-library -package-name Core -emit-module -module-name Lib -emit-module-interface-path Lib.swiftinterface -enable-library-evolution -emit-private-module-interface-path Lib.private.swiftinterface -o Lib.swiftmodule Lib.swift
echo "public interface lists: $(grep -E 'func ' Lib.swiftinterface | sed 's/.*func \([a-z]*\).*/\1/' | tr '\n' ' ')"
echo "private interface lists: $(grep -E 'func ' Lib.private.swiftinterface | sed 's/.*func \([a-z]*\).*/\1/' | tr '\n' ' ')"
for f in A B C; do
echo "$f same package: $(swiftc -swift-version 6 -typecheck -I . -package-name Core $f.swift 2>&1 | grep -q 'error:' && echo error || echo ok)"
echo "$f other package: $(swiftc -swift-version 6 -typecheck -I . -package-name App $f.swift 2>&1 | grep -q 'error:' && echo error || echo ok)"
done
- public lists open; private lists open and secret; A: ok same package, 1 other package; B: 0 and 0; C: 1 and 1
- both list all three functions
- package members appear in the public interface
- SPI members compile without the import
Mental model. Public is the front door, package is the staff entrance for one company, and SPI is a side door you can only find if somebody told you its name.
package access is visible to every module compiled with the same package name (-package-name, set by SwiftPM from the package manifest) and to nobody else. @_spi(Name) public is a different tool: the declaration is public in the binary but hidden from clients unless they write @_spi(Name) import Lib; it is how Apple frameworks expose “System Programming Interface” to friendly callers while keeping it out of the public interface. The run below shows both: the public .swiftinterface lists only open; the private interface adds secret (with its SPI tag); package members appear in neither. A client in the same package sees shared(), a client with another package name gets “inaccessible due to ‘package’ protection level”, and secret() fails without the SPI import and compiles with it.
Use package to share helpers between the targets of one repository without making them part of the public API (and without the pattern of making everything public for convenience). Use SPI for an interface that must be public in the binary for a specific partner but is not promised to everyone. SPI is a convention, not security: anyone who writes the import can call it, and it is an underscored attribute, so the spelling is not an official stability promise. Both keep ABI considerations: an SPI declaration is part of the binary’s ABI like any public one.
Package members are checked by package name; SPI members need the SPI import and live only in the private interface.
Common traps
- Using
packageand expecting binary frameworks to honour it. The check relies on the package name, set consistently by the build system. - Treating SPI as private. It is hidden, not protected.
They might follow up with
- Does package access change linkage? The symbol is not hidden the way internal ones are; see the linkage question.
- Is there a third interface? A package interface can be emitted for package-level declarations; I did not run that flag.
Related questions
- 1.7.3 What does package access solve?
- 5.4.2 What is a .swiftinterface file, and what does it show about @inlinable and @frozen?
- 1.10.4 What is ABI stability vs source compatibility vs module stability?
- 5.8.2 How do access levels become linkage in the object file, and why can an unused internal function disappear in…
What does internal import (SE-0409) change, and why does it matter… →