[cxx-interop] Using a C++ enum in Swift, cannot be used in C++ anymore (bi-directional Swift <-> C++ code)
Description
I'm creating a C++ class that has a Swift implementation.
Since Swift doesn't support inheriting from C++ classes (yet), I have to write the methods twice, and just add the Swift instance as a member to the C++ instance:
#include "MyLibrary-Swift.h"
// C++ enum
enum class SomeEnum {
first,
second
};
// C++ class that holds the Swift class
class SomethingCxx {
public:
SomethingCxx(MyLibrary::SomethingSwift swiftPart): _swiftPart(swiftPart) { }
SomeEnum getSomeEnum() { return _swiftPart.getSomeEnum(); }
private:
MyLibrary::SomethingSwift _swiftPart;
};
public class SomethingSwift {
public var someEnum: SomeEnum { get { return .first } }
}
The problem here now is that Swift depends on the C++ code (SomeEnum), and C++ depends on the Swift code (SomethingSwift) - so it is a cyclic include.
I tried separating this out into multiple files, but no luck - Swift generates a single C++ header (MyLibrary-Swift.h), and if at any point I try to depend on C++ <-> Swift, it breaks the build with errors like these:
- No member named 'getSomething' in 'MyLibrary::SomethingSwift'
If I just return an Int instead of SomeEnum it works fine, so it is definitely the SomeEnum part that breaks.
Reproduction
public class SomethingSwift {
public var someEnum: SomeEnum { get { return .first } }
}
Expected behavior
I expect Swift to properly resolve the C++ types and break up cyclic includes by either forward declaring it or just splitting the headers.
Environment
swift-driver version: 1.90.11.1 Apple Swift version 5.10 (swiftlang-5.10.0.13 clang-1500.3.9.4) Target: arm64-apple-macosx14.0
Additional information
No response
After reading "Exposing Swift APIs to C++", it seems like this should be supported - their example calls a Swift function from C++, which uses a C++ type:
So maybe there is something broken with my setup. It's probably worth mentioning that I am using this in a CocoaPods project, so the modulemap is generated automatically.
As of right now, the modulemap just exports everything:
module react_native_nitro_image {
umbrella header "react-native-nitro-image-umbrella.h"
export *
module * { export * }
}
I am not sure why this is not working in this case.
I tried to reproduce your issue and got the exact same error.
First, I tried looking in the build folder, located at ~/Library/Developer/Xcode/DerivedData/. I found that MyLibrary-Swift.h does not contain any mention of SomeEnum, which is strange. I therefore tried with some other types, and to my surprise it works fine with both Swift's Int type and C++ structs!
public class SomethingSwift {
public var someEnum: SomeEnum { get { return SomeEnum.first } }
public var someInt: Int { get { return 0 } }
public var someStruct: MyStruct { get { return MyStruct(myEnum: .first) } }
}
Hence, the issue seems to be happening when using C++ enums in Swift. Very interesting indeed.
After some fiddling around, I actually got your code to (sort of) work. My temporary solution is to encapsulate the enum within a struct as below.
struct MyStruct {
SomeEnum myEnum;
};
Along with:
SomeEnum getSomeEnum() { return _swiftPart.getSomeStruct().myEnum; }
In SomethingCxx.
Could you try this and verify that it works for you as well?
Hey @ludwwwig - thank you for your reply & thank you for reproducing.
You are right - wrapping it in a struct works! 🤯 That's great- I thought I would have to do much more effort to separate the Swift and C++ codebases into individual clang modules as I thought this was some cyclic header include issue.
I'll use that workaround for now, I'll try to fiddle with the Swift Compiler here to see if I can find a fix for this - but I'm far from a compiler engineer so I'd have no idea what I'm doing. 🙈
Glad to hear that the workaround worked for you, @mrousavy!
Btw.; another workaround (maybe more efficient? idk) is to just use the enum's .rawValue (Int32 <-> int)
@mrousavy I find it hard to believe that wrapping of the enum inside a struct would lead to much overhead, if any at all. That being said, your solution is more clear than mine, so I would definitely go with that! 👍🏻
I think circular dependencies like this might not be supported but this particular case can be easily factored out so we no longer have circular dependencies. I added a test in this PR: https://github.com/swiftlang/swift/pull/75685 It works as expected for me on main. Can you take a look if this is what you wanted or if I am missing something?
@Xazax-hun Hi, I cherry-picked your test and tried running it with the release/5.10 branch. It returned an error:
error: no member named 'getSomeEnum' in 'UseCxx::SomethingSwift'
SomeEnum getSomeEnum() { return _swiftPart.getSomeEnum(); }
~~~~~~~~~~ ^
1 error generated.
--
********************
********************
Failed Tests (1):
Swift(macosx-arm64) :: Interop/CxxToSwiftToCxx/bridge-cxx-enum-back-to-cxx-execution.cpp
I am currently building the main branch to test. But I guess if you just tested it then there shouldn't be any issues?
Update: The test passes on main but not on release/6.0.