I told @Jon_Shier I'd write something up on @_exported
and visibility in general, so here it is. It's a bit of a brain dump, but I know if I wait until I have time to edit it it won't get posted for months. (That's already true, in fact; I've had this model kicking around my head for probably close to a year now.) So, thanks for the prompt, Jon.
Today, if your Swift library "Foo" uses a library "Bar", the headers (or swiftmodule files) of "Bar" must be available to all clients of "Foo". This is clearly undesirable, especially when "Bar" is just an implementation detail. (It may even be linked statically.)
At the same time, if part of Foo's public interface uses a type from Bar, the interface for "Bar" must be available. Otherwise, a client of Foo wouldn't be able to use that particular API from Foo.
(Technically, we could make this more fine-grained: a client of Foo can't use that particular API without Bar being available, but they can use other APIs. But I'm not sure if that level of control is worth it at the moment.)
Note that Swift already has a fairly robust (and well-understood) model for access control. However, just because an entity is public does not actually mean it is visible; you may not have imported its module yet. This proposal is only concerned with modifying the rules for visibility.
On the flip side, there is no supported way to get the effect of C headers in Swift. With C, you can make something like Cocoa.h, whose only purpose is to make the Foundation, AppKit, and CoreData modules available to clients all in one go. Or you can have UIKit re-export Foundation, so that import UIKit
is enough to get Foundation as well. In practice, developers for Apple platforms have gotten very used to this model in both Objective-C and Swift; most people don't care that some of the APIs made available through import UIKit
are actually from the MobileCoreServices module.
Today's Swift is designed more like Java or C# or Python, in that if you import Bar in the implementation of Foo it doesn't affect clients who import Foo. Or, well, it doesn't make the top-level names of Bar visible to clients who import Foo.
-
You still need Bar around, because the compiler doesn't track whether you've used one of its types in Foo's public interface. (That's the previous section.)
-
Extensions in Bar are still made visible to clients who import Foo, because the compiler doesn't distinguish where extensions come from today.
-
Operator declarations in Bar are still made visible to clients who import Foo, because the compiler finds operators in a different way than it finds everything else at the top level.
(2) and (3) are closer to "bugs" than "features", but we may have no chance of fixing them at this point, because there'd be so much code depending on them, and we don't actually have an implementation of the "right" model for either.
But we're supposed to be looking on the "feature" side now, too. What if I want to make the top-level names of Bar visible to clients who import Foo, the way UIKit exposes the types in Foundation?
(There's a non-supported attribute for this, @_exported
. It pretty much works, as far as I know.)
Bonus complication: on Apple platforms, we generate headers for the parts of a framework that are exposed to Objective-C, and those headers have (Objective-C) imports of their own. The net effect is that some modules end up getting re-exported when you import a library...but it's only those that are needed to define the Objective-C interface. What a mess!
While I think the Java / C# / Python model is a good one, I don't think there's a good path to get there for Swift, particularly on Apple platforms where its visibility model has to play with C's. So the way I think we should go is something like this: Formalize @_exported
, then require that all types exposed in a module's public API come from @_exported
modules.
Then we have a simple rule: any time you can see a type in a particular API, you can also see the (public) contents of the module that defines that type, regardless of whether you imported it directly. This is consistent with the behavior of the generated Objective-C header on Apple platforms today.
I would implement this requirement with a warning, not an error. That is, if a type is not available from the current file's set of re-exported imports, the compiler would emit a warning and then implicitly re-export the module containing the type.
This doesn't directly solve any of the "too much is visible" problems I mentioned above, but I think it will help us get to a point where we no longer need to have Bar available to import Foo. We're not there yet because the compiler currently expects to have full knowledge of every type it deals with, even internal
and private
members, but I think it's a step in the right direction. It fixes the semantics to be something sensible and consistent with C, giving us space to work towards dropping the non-re-exported dependencies altogether.
To cap it off, I would suggest that the natural spelling for a proper supported @_exported
is public
, as in public import Foundation
.