Motivation
Packages are often composed of multiple modules; packages exist as a way to organize modules in Swift, and organizing often involves splitting a module into smaller modules. For example, a module containing internal helper APIs can be split into a utility module only with the helper APIs and the other module(s) containing the rest. In order to access the helper APIs, however, the helper APIs need to be made public. The side effect of this is that they can “leak” to a client that should not have access to those symbols. Besides the scope of visibility, making them public also has an implication on the code size and performance.
For example, here’s a scenario where App
depends on modules from package gamePkg
.
App (Xcode project or appPkg)
|— Game (gamePkg)
|— Engine (gamePkg)
Here are source code examples.
[Engine]
public struct MainEngine {
public init() { ... }
// Intended as public
public var stats: String { ... }
// A helper function made public to be accessed by Game
public func run() { ... }
}
[Game]
import Engine
public func play() {
MainEngine().run() // Access a helper API within the same package
}
[App]
import Game
import Engine
let engine = MainEngine()
engine.run() // `run` is a helper API and should not be accessed here
Game.play()
print(engine.stats) // `stats` is intended as public so can be accessed here
In the above scenario, App
can import Engine
(a utility module in gamePkg
) and access its helper API directly, even though the API is not intended to be used outside of its package.
Proposal
A current workaround for the above scenario is to use @_spi
, @_implemenationOnly
, or @testable
. However, they have caveats. The @_spi
requires a group name, which makes it verbose to use and harder to keep track of, and @_implementationOnly
can be too limiting as we want to be able to restrict access to only portions of APIs. The @testable
elevates all internal symbols to public, which leads to an increase of the binary size and the shared cache size. If there are multiple symbols with the same name from different modules, they will clash and require module qualifiers everywhere. It is hacky and is strongly discouraged for use.
We propose to introduce a new access modifier package
. This would limit the visibility of the symbols to only modules within a package.
Declaration Site
Using the scenario above, the helper API run
can now be declared package
public struct MainEngine {
public init() { ... }
public var stats: String { ... }
package func run() { ... }
}
The package
access modifier can be added to any types that an existing access modifier can be added to, e.g. class, struct, enum, func, var, protocol
, etc. The access level of package
will be between internal
and public
. It will allow access as well as subclassing (unless final
) cross-modules within a package, and does not allow the symbol visibility outside of a package. The parallels to internal
are strong. With internal
, every potential client is in the same module. With package
, every potential client is in the same package. Since a package is developed as a unit, package-visible declarations can be updated with their clients. The exportability rule will be similar to the the existing behavior; for example, a public
class is not allowed to inherit a package
class, and a public
func is not allowed to have a package
type in its signature.
Use site
A package name will be stored in a .swiftmodule
and looked up during type check to dis/allow access to package APIs. Swift Package Manager knows the package boundary and will pass it down to the compiler. Other build systems such as Xcode and Basel will need to pass a new command-line argument -package-name
to the build command to let the compiler know what package the compiled module is in, per below.
[Engine] swiftc -module-name Engine -package-name gamePkg ...
[Game] swiftc -module-name Game -package-name gamePkg ...
[App] swiftc App -package-name appPkg ...
The input to -package-name
is a package identity, which, besides alphanumeric characters, can contain a hyphen, a dot, and other characters valid in URL; such characters will be transposed into a c99 identifier.
When building the Engine
module, it will store the package name gamePkg
to Engine.swiftmodule
. When building Game
, it will store and compare the package name of Game
and that of Engine
and allow access to package APIs if they are the same. When building App
, it will detect that its package name is different from the package name of Game
or Engine
that it imports, so it will disallow access to package APIs and throw an error if it attempts to do so. With the helper API run
in MainEngine
now declared package
, App
can now only access its public API stats
, which is the intended behavior.
Future
Limiting the scope of visibility per package can open up a whole lot of optimization opportunities. A package containing several modules can be considered as a unit for applying size and performance optimizations, which could yield notable improvements in the future.