Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[cxx-interop] Using a C++ enum in Swift, cannot be used in C++ anymore (bi-directional Swift <-> C++ code) #75330

Open
mrousavy opened this issue Jul 18, 2024 · 8 comments
Labels
bug A deviation from expected or documented behavior. Also: expected but undesirable behavior. c++ interop Feature: Interoperability with C++ clang importer Area → compiler: The clang importer compiler The Swift compiler itself enum Feature → type declarations: Swift enumeration declarations swift 5.10 unexpected error Bug: Unexpected error

Comments

@mrousavy
Copy link

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

@mrousavy mrousavy added bug A deviation from expected or documented behavior. Also: expected but undesirable behavior. triage needed This issue needs more specific labels labels Jul 18, 2024
@mrousavy
Copy link
Author

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:

image

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.

@ludwwwig
Copy link
Contributor

ludwwwig commented Jul 18, 2024

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?

@hborla hborla added c++ interop Feature: Interoperability with C++ and removed triage needed This issue needs more specific labels labels Jul 19, 2024
@mrousavy
Copy link
Author

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. 🙈

@mrousavy mrousavy changed the title [cxx-interop] Cyclic includes cannot be broken (bi-directional Swift <-> C++ code) [cxx-interop] Using a C++ enum in Swift, cannot be used in C++ anymore (bi-directional Swift <-> C++ code) Jul 19, 2024
@ludwwwig
Copy link
Contributor

Glad to hear that the workaround worked for you, @mrousavy!

@mrousavy
Copy link
Author

Btw.; another workaround (maybe more efficient? idk) is to just use the enum's .rawValue (Int32 <-> int)

@AnthonyLatsis AnthonyLatsis added compiler The Swift compiler itself swift 5.10 clang importer Area → compiler: The clang importer enum Feature → type declarations: Swift enumeration declarations unexpected error Bug: Unexpected error labels Jul 22, 2024
@ludwwwig
Copy link
Contributor

@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! 👍🏻

@Xazax-hun
Copy link
Contributor

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: #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?

@ludwwwig
Copy link
Contributor

ludwwwig commented Aug 5, 2024

@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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug A deviation from expected or documented behavior. Also: expected but undesirable behavior. c++ interop Feature: Interoperability with C++ clang importer Area → compiler: The clang importer compiler The Swift compiler itself enum Feature → type declarations: Swift enumeration declarations swift 5.10 unexpected error Bug: Unexpected error
Projects
None yet
Development

No branches or pull requests

5 participants