Pitch: Swift Package Access Control

Hey there :wave:

I'd like to present a pitch for how the Swift language could be evolved to add a new access control keyword designed for use in Swift Packages.

I look forward to hearing your feedback!


Introduction

Swift currently has a handful of access control modifiers, namely: open public internal private fileprivate. These keywords change the visibility of parts of our code to change what other parts of our codebase have access to said code.

Swift Packages define targets which are groups of code. Targets can depend on other targets, and multiple targets can be combined to create a product. A product can be a dependency of another application or package.

I should state that I am massively oversimplifying for the sake of brevity here...

This pitch looks at access control keywords in the context of target to target communication.

Motivation

Package: "A"
  Product: "A"
    Target: "A"
    Target: "B"
      Dependency: Target "A"

Imagining the Swift package above, we have two targets one of which depends on the other. As a developer this allows me to separate concerns and test parts of my codebase in smaller chunks.

If I add a function (for example) to target A, and I want target B to be able to have visibility of it then I (currently) have no option but to use public or open. The other keywords would prevent package B from being able to see the function.

Now imagine that we have a separate application which depends on our package. This application now has access to all the code that was made public in target A - even though this function was only designed for use in target B. This is the issue this pitch aims to solve.

I believe it should be possible to write code which is public only within the package it is within.

Proposed Solution

My resolution to this problem follows safely on from the known and predictable access control methodology which Swift has had for a long time. We would add a new keyword: package.

Exact keyword name can obviously be changed...

The keyword would only be available for code which is within a Swift package. In essence, projects where #if SWIFT_PACKAGE is true. If you use it in a normal Xcode Project then you would get a warning.

Code marked with the package access control keyword would be the equivalent of open but only within the scope of the Swift package it sits within. It would be visible to all other targets which depend on said code.

If an application (not within the package) calls the function marked with the package keyword, an error would be shown explaining that they need to change the visibility. This is the same as if you try to access an internal function (etc) now.

Examples

// Target One
package func someFunction() -> String {
  return "Hey folks!"
}

public func someFunctionTwo() -> String {
  return "Hey folks!"
}

// Target Two
import TargetOne
someFunction() // = "Hey folks!"
someFunctionTwo() // = "Hey folks!"

// Application (Not in Package)
import TargetTwo
someFunction() // Error: Function is not available due to access control
someFunctionTwo() // = "Hey folks!"

Impact on Existing Code

This change should be entirely additive and as such would have no impact on existing code.

Users of a dependency using this feature would need to use the latest Xcode/Swift version in order for their code to compile.

Alternatives Considered

Private Target

It may be possible to add an enum to the PackageDescription listing for a target. This would allow us to define a target as "internal" and as such all code within the target (even stuff marked as "public") would only be available to other targets within the same package.

This vaguely builds on top of the pitch raised by Ankit here.

Other Notes

It may be worth noting the @_spi API which was introduced in Xcode 12 (Swift 5.3) as completed in this pull request. This feature is still experimental and while it puts a barrier up to importing the "hidden" package code, it's still possible and does not completely prevent users from calling those functions.

This attribute works by providing a label to definitions such as @_spi(Testing) class MyPackage { and then can be imported by using the same label: @_spi(Testing) import PackageName. This is similar (but more flexible) than the @testable flag which has long been part of Swift giving a test target access to internal code.

11 Likes

Thank you very much for the pitch!

I know of least a few projects that could from this functionality.

One is Tokamak, where TokamakCore which has API that is intended for consumption by the other targets in the package and not for consumers of the package. The project uses a naming scheme to define API which is only public due to the lack of the feature you’re describing.
I know that they are currently exploring using the @_spi attributes.

Another project is Firebase. It is currently mainly Objective-C. But a pattern here is for targets in the package to import private headers of the FirebaseCore module. So if this project were to move to a Swift implementation they would face a similar challenge.

I definitely support this idea even though some have said that the access modifier discussions ought to be over by now. :blush:

5 Likes

:heart: Appreciate the response!

I don't really disagree with them... but I also can't really think of a nice way to achieve this separation of access without the solution feeling like a massive after thought (which in turn hurts the simplicity and intuitive-ness of the language)

I am 100% open to alternative solutions which address the same underlying problem!

Apologies, I was just teasing a bit!
I think your pitch represents a new and different use case from many of the previous access modifier discussions - and I would definitely prefer a new modifier over using the _spi interface.

3 Likes

Thanks this definitely seems like something that would be very useful. I still would want @_spi as well. I have had applications that were built using an SDK that we wanted to provide an API to first parties that we did not want to expose to third parties. Our only option was to make it all public or open. This proposal looks like it would be helpful for calls internal to interdependent packages, which I think is a good thing to support.

I am wondering, out loud, if somehow we could combine the @_spi and package access controls. So we don't have an explosion of access modifiers that somehow seem similar. Can the "does not completely prevent users from calling those functions" be addressed by a change to the @_spi mechanism? Or are they conceptually different enough to have two different mechanisms?

For example, you could have a predefined package id @_spi(package) built-in that would ensure access only within a package. By the way, I like the package keyword better than @_spi keyword but if they were combined then package would not be appropriate.

2 Likes

I like the out of the box thinking on this one!

One downside of that change is that it'd be deemed a breaking chance since it's feasible a project is already using that identifier and this would then change the purpose of that. Definitely doesn't rule it out, but a consideration.

Not as much a downside but another consideration is also the discoverability of @spi and ultimately the special package keyword!

1 Like

Agreed the @_spi keyword would be good to come up with something better and perhaps not an attribute.

1 Like

Strong +1 here. As @Morten_Bek_Ditlevsen clarified above in this thread, this would clean up a lot of things in our multi-library package. We need to share some code between those library products, but it still has to stay unavailable for users. We work around that with _ underscore naming conventions, and somewhat convoluted re-export rules. package access modifier makes perfect sense here.

1 Like

I'm glad the need for such a capability is resonating with the few of you who have responded :smiley: Thanks!

A major concern of mine is around the technical feasibility - I'm not sure if anybody here has any actual experience with the compiler?

I wouldn't have the foggiest in terms of whether this is even possible, and most definitely don't have the ability to fulfil the requirement to have an actual implementation before taking this to a Swift proposal.

In general, there are very little changes to Swift based on it being a Swift Package Manager package (the only real one I can think of is the module bundle accessor) - I can't think of anything which fundamentally adds something to the language, and especially not one which only applies to multiple targets within the same package. This all feels like contextual information which Swift most likely does not have.

I'm thinking that perhaps access control could expand to cover both package and module-to-module cases. As I imagine the two features, the former would offer facilities that enable modularity within a given package without comprising on declarations' visibility. The latter would offer library authors even more control on complex protocols (such as SwiftUI's View with its _makeView(...) requirement) with fine-grained control over how individual modules β€” inside or outside the package β€” can interact with complex access-level semantics.

I'm wanted to add that I too am very excited to see discussion on this issue. I'm developing a library which has benefited tremendously from the introduction of SPI β€” which I first learned about here (thanks!).

On a more general note, I think it's important to start discussing Swift's access control limitations, which are highlighted by the standard library's relying on the leading-underscore rule for reducing visibility of ideally internal declarations (such as String._guts). Still, I think that the leading-underscore rule is appropriate for certain use cases; nonetheless, I think it's currently overused by Apple libraries and the standard library β€” the only places where said "rule" is available.