Swift supports bidirectional interoperability with C++. We can use many C++ entities (like functions or types) from Swift and many Swift entities from C++. There are two modes to expose Swift entities to C++. We either expose declarations that are annotated with @_expose(Cxx) or all publicly visible declarations. The latter is the default in the compiler.
We started to see a need for hiding a public Swift type or API from C++. The main motivation is to avoid name collisions when the C++ code already defines a type with the same name that exists on the Swift side too.
Before committing to this direction, we wanted to check if the community is on board with this solution or there are alternative suggestions for the spelling (or a different way to hide declarations).
I would seriously consider ~cxx as that mirrors the ~Copyable syntax that we already have for protocol negation.
I think that having some more details on the experience here would be helpful. Was it the norm that you wanted to prevent the interface from being visible or was it the exception? If it is the latter, I think that we should consider the inverted attribution (@swift or some other spelling) to indicate that the interface is limited to Swift. If it is that the norm, then it makes sense to attribute the interfaces that should be visible.
Another option to consider - if it is to deal with collisions, perhaps we should introduce the reverse mapping for expose to C++ with an alternate name. We already do this today for importing C/C++ types where they are renamed via APINotes or via bodyless declarators in Swift.
~ on generic requirements does not mean "is not", it means "is not required to be". A ~Copyable type may be conditionally copyable, or it could be a generic parameter bound to a Copyable implementation. That does not seem applicable here.
We do support that under the -clang-header-expose-decls=has-expose-attr frontend option. We currently do not have the corresponding driver flag but did not see much demand for this mode to be exposed more widely.
We actually support that under the syntax @_expose(Cxx, "CxxName"). That being said I did see cases where the users just preferred to hide the Swift declaration from C++ over renaming it. Another motivation for hiding is to temporarily work around bugs. E.g., if some declarations make the compiler generate a reverse interop header that would not compile. Fortunately, these are becoming more rare these days but it is still nice to be able to give people workarounds until a fix is released when this happens.
Oh, I just realized there are also some holes in the support here. Sometimes we generate multiple names for a single entity. E.g., a property is exposed to C++ via getProperty and setProperty methods. These cannot be renamed using @_expose at the moment because it only provides one name.
There is a long-documentedneed for official support to control how Swift exposes types to other languages, especially structs that are shared with C and C++. It would be unfortunate to formalize a C++-specific @expose keyword without considering these other long-standing use cases.
In the meantime, how about @available(cxx, unavailable)? (Edit: or does this already map to something like = deleted?)
Good point. Do you know use cases where someone wants to expose a type to C but not to C++? I wonder if we do need this granularity. Also, do we have this problem with C? The reason why we have this problem with C++ because by default we expose all the public Swift declarations we can to C++. On the other hand, @cdecl is opt-in so I am not 100% sure if we need the equivalent functionality in C.
This is also an interesting suggestion, but this would be a way bigger change. We might need to go through Swift evolution. @_expose is underscored, so we can use it to test an implementation out without having to go through the full evolution process.
I think this could work. The main concern here is the discoverability. But I think that could be mitigated via good documentation.
After some quick testing, _expose(Cxx, "") still tries to generate bridging code for said Swift class in the autogenerated -Swift.h header.
I was trying to hide a Swift class from C++ because it failed to build and I don't need it available from C++ anyways..
I like this and it sort of matches the macro strategy used in clang with marking decls as unavailable in Swift with [[unavailable]].
This is sort of unrelated and pedantic, but I don't think we should map anything to = deleted because it will trigger a compiler error if the overload is selected, and I doubt that's often the intent of an API author. I imagine a common use case for this is disabling overloads that don't export well to C++, and it might be common to have another overload that can also be selected in C++ (but is a worse match from overload resolution's perspective).
Iâm sure someone somewhere will have a use case for exposing something to C++ but not C (or vice versa). Personally, it will be more common that Iâll want to ensure the C++ projection of a struct is extern "C" so that I can guarantee its layout is consistent when imported in ObjC, C++, and Metal source files.
I mentioned = delete because itâs unclear to me how [[unavailable]] members affect overload resolution. Does Clang treat them as absent, implicitly deleted, explicitly deleted, or present but uncallable? All four of these options might be desirable for any given C++ projection, based on how they play with SFINAE and type deduction.
@available has many responsibilities, and I could see it gaining this one but I don't find it to be a very intuitive fit and I'd personally want to explore other solutions first. It's not a good fit because @available generally doesn't have the effect of hiding declarations entirely; instead, it constraints which regions of code can reference a declaration. If a syntax like @available(cxx, unavailable) already existed I would expect it to cause the declaration to be emitted with __attribute__((unavailable)) when it is printed in headers for C++ compatibility, not for it to be omitted from those headers entirely.
Interesting topic, and something we'll run into with Java interop as well eventually I guess, though we've not much clashes yet, it may be an useful thing to be able to not export some public decls to java Since java interop isn't part of the Swift source tree it's not really using @_expose, but I could totally see it using the same pattern here @_expose(Java) etc could be a nice way to expose a func that is for example internal but we're only extracting public funcs by default... So yeah, I'll keep an eye on how this proposal evolves for sure.
I get Allan's concern about available but it still seems quite tempting, but perhaps conflating too many things. At the same time, @_expose(!Cxx) feels a bit magical "what does not C++ mean?".
So one idea that came to mind is sticking to @_expose however use the unavailable word with it, perhaps @_expose(Cxx, unavailable) could be an interesting spelling for it.
Currently, it looks like we will go ahead with @_expose(!Cxx). Looks like this can be generalized to interop with other languages. While !Cxx might feel a bit magical, it looks like all the other spellings have potentially alternative confusing interpretations. @_expose(Cxx, unavailable) might give the impression we still generate the declaration but with an availability annotation. @_expose(Cxx, ââ) might not be clear for the reader unless they read the documentation for the attribute. ~ might be confusing because it has a different meaning in generic context. Reusing the availability annotations have similar concerns to using @_expose(Cxx, unavailable).
In case anyone has any strong objections please let us know. I plan to merge the PR next week if nothing comes up.
What about comparability? It feels a bit less composable if we want to mark it unavailable in multiple languages. Would you then use && to join the languages?
We currently need to do @_expose(Cxx) @_expose(wasm) at the moment. So I think it would work similarly for the hiding. One of the reasons why it is used like this for the positive case is the optional second argument. The same Swift function can be exported to different languages using different names this way.