Feature proposal: Accessing non-public C++ members from Swift

Hey folks, I'm working on a feature that will allow Swift to access non-public C++ members, and I'm interested in hearing your feedback.

You can find the full pitch here.

The tl;dr is to introduce a C++ class annotation, SWIFT_PRIVATE_FILEID, which will specify where Swift extensions of that class will be allowed to access its non-public members:

class SWIFT_PRIVATE_FILEID("MyModule/MyFile.swift") Foo { ... };

The goal of this feature is to help C++ developers incrementally migrate the implementation of their C++ classes to Swift, without breaking encapsulation and indiscriminately exposing those classes' private and protected fields.

Note that the specific feature I am proposing here is not @implementation for C++: it does not allow Swift extensions to directly implement C++ methods declared in a header file. That is still unsupported today (no matter the access level), but something like @implementation for C++ would certainly require non-public member access in order to be useful.

9 Likes

Would the implementation as you've spelled it out allow removal of attributes like SWIFT_CONFORMS_TO_PROTOCOL?

It sounds like what you're planning to do involves telling the Swift compiler that the synthesized "Clang module" for the module from the modulemap is the "same" file as MyModule/MyFile.swift, in the context of class Foo.

Would that allow asserting protocol conformance in a more direct way than an annotation on the C++ source file? If we are claiming that MyFile.swift is the "same" file as the one that introduced Foo, it seems natural to me to extend MyFile.swift all of the special rights given to the Clang module as far as protocol conformance goes.

I think this is a great approach, extending Swift's notion of private/fileprivate out to specific C++ classes.

Doug

1 Like

Hey all, I'd like to add some critiques and voice some concerns I had about this (and the general direction of how similar features are approached).

First, strictly about this proposal, I believe this is the first of these SWIFT_ macros that directly mention a Swift file/module by name (questions of how these paths should be handled aside):

class SWIFT_PRIVATE_FILEID("MyModule/MyFile.swift") Foo { ... };

While this is absolutely the nicest of the alternatives considered, I do not like how it "leaks" Swift into the C++ source - for lack of a better description on my part; as a stark contrast to most other SWIFT_ macros that only make explicit some behaviour or constraint that stems from C++ itself (e.g. "returns independent value")

To offer a couple other alternatives for consideration:

  • A more explicit @cxxPrivate (something like @cxxPrivate(fileprivate Foo)); this doesn't address the "access is authoritatively specified at the site where an entity is defined" point in the proposal, but I'd argue that importing something external into swift is its declaration site within swift (disregarding the modulemap)
  • speaking of which, the modulemap is the point of introduction for a C++ header/object into swift, further syntax there or a supplemental module.access.modulemap file, perhaps?

That said, some general feedback on the "direction" as it were (this isn't directly relevant to this proposal, but I feel it's necessary to communicate across why I don't like "leaking" one language into the other when doing interop):

It seems to me that c++interop is very one-sided (not in the sense that only one side can call the other, but rather in the sense that the C++ source is forced to supply any information that cannot be inferred automatically); that's not what interopability should be like.
The Ideal :tm: interop model would have both sources completely oblivious of the other (which would also make it simple to straight up replace foo.cpp with Foo.swift, or the other way around). This is (of course) unreasonable, but once (what I think is) the mistake of shoving a bunch of information that cannot be communicated cleanly across the interop bridge only into one side is made, replacing either side becomes increasingly difficult.
This is true for other attributes like SWIFT_CONFORMS_TO_PROTOCOL as well.

I think Swift has done a stellar job of keeping the C++ from leaking into the Swift source so far, I'd love to see it not leak Swift into C++ as well.

Sorry for the overly long post.

2 Likes

The feature I've proposed is not directly related to protocol conformances. The SWIFT_PRIVATE_FILEID annotation only affects non-public member visibility in Swift, and does not grant any additional privileges to the blessed Swift file.

However, SWIFT_PRIVATE_FILEID will allow protocol conformances that require access to non-public members to be implemented in Swift files, as a positive side effect for those who wish to keep SWIFT_CONFORMS_TO_PROTOCOL annotations out of their header files.

As you point out, extending this feature to allow the blessed Swift file to be home to more protocol conformances is natural extension of the feature I'm proposing—I'm interested in exploring that as a follow up to this feature. However, I think we are still far from removing the need for SWIFT_CONFORMS_TO_PROTOCOL entirely. In particular, that is still strictly necessary to add conformances for all instantiations of a templated C++ class, because there isn't any way to refer to an un-instantiated template in Swift.

Hey Ali, thanks for your feedback! Your point about interop being rather one-sided is well taken, and I'm definitely onboard with your general sentiment of keeping Swift out of C++ source code.

However, for this specific feature, I think adding this annotation to C++ is justified. This annotation is not designed for Swift developers using a C++ library, but for C++ library developers who wish to implement part of their library in Swift (and have ownership of the C++ source). Like C++'s native access controls, SWIFT_PRIVATE_FILEID annotations are there to help C++ developers encapsulate their classes' implementations (in Swift or in C++) from their users. They serve a similar role to C++ friend declarations, except that they bless non-public access by Swift files rather than C++ declarations. (In an earlier draft of my proposal, I had actually named the annotation SWIFT_FRIEND_FILEID; I ended up changing my mind but I am very open to bike shedding this.)

To respond to the alternatives you suggest:

A more explicit @cxxPrivate (something like @cxxPrivate(fileprivate Foo) ) ... I'd argue that importing something external into swift is its declaration site within swift

This approach is still prone to multiple Swift files/modules proclaiming @cxxPrivate in order to gain access to a class's internals, making it difficult to discern "legitimate" uses of the attribute from abuses of it. That is, it would allow arbitrary Swift adopters to access private C++ members, against the intentions of the C++ developers that declared those members.

the modulemap is the point of introduction for a C++ header/object into swift, further syntax there or a supplemental module.access.modulemap file, perhaps?

This is a good idea, and I'll include it in my next draft of the proposal. However, I still think it has two downsides compared to C++ annotations. One is that it requires changes to both Clang (which handles .modulemap files) and Swift, whereas __swift_attr__ annotations are already ignored by Clang. The second downside is that it decouples the access controls of each class from the class definition itself.