Skip to content

Generated struct operator== breaks std::vector<std::string>.map (CxxRandomAccessCollection) when the module also links a heavy C++ library #1376

Description

@chefmarvin

Summary

Since upgrading from nitro 0.31.10 → 0.35.9, our iOS build fails to compile a generated Swift struct because std::vector<std::string> no longer conforms to CxxRandomAccessCollection, so the nitrogen-generated .map(...) getter doesn't compile:

error: value of type 'std.__1.vector<std.__1.basic_string<CChar, std.__1.char_traits<CChar>,
std.__1.allocator<CChar>>, std.__1.allocator<...>>>' has no member 'map'

This only happens in a module that also links a heavy third-party C++ library (in our case Tencent WCDB, a large C++ database engine vendored as a Swift framework). The exact same generated .map code compiled fine on 0.31, and still compiles fine in our other nitro modules that don't link WCDB.

We traced the trigger to the friend bool operator==(...) = default; that nitrogen started emitting on structs since 0.32.0 (#1019, described in the release notes as a "DX/convenience feature" making structs Equatable). We'd love guidance on the recommended pattern for this scenario, and whether the generated operator== could be made opt-out.

Environment

  • react-native-nitro-modules: 0.35.9
  • nitrogen: 0.35.9
  • React Native: 0.85.0 (new arch / Fabric / bridgeless)
  • Xcode 26.2, Apple Swift 6.2.3 (swiftlang-6.2.3.3.21)
  • iOS, use_frameworks! :linkage => :static, CocoaPods
  • C++ interop flags (from add_nitrogen_files): -cxx-interoperability-mode=default, SWIFT_OBJC_INTEROP_MODE=objcxx, CLANG_CXX_LANGUAGE_STANDARD=c++20
  • The module links WCDB (WCDBSwift), whose public headers contain a large amount of WINQ/ORM C++ templates and which itself uses std::vector<std::string> extensively.

What we generate

A struct with a string[] field generates (shortened):

public extension HealthCheckResult {
  @inline(__always)
  var issues: [String] {
    return self.__issues.map({ __item in String(__item) })   // <- fails here
  }
}

Root cause analysis (our findings)

We diffed the 0.31 vs 0.35 generated C++ struct. The only relevant change is the defaulted equality operator added in 0.32.0 (#1019):

struct HealthCheckResult final {
  ...
  std::vector<std::string> issues;
  friend bool operator==(const HealthCheckResult& lhs, const HealthCheckResult& rhs) = default;  // <- new since 0.32
};

Our hypothesis: operator== = default forces the compiler to instantiate std::vector<std::string>::operator==, which instantiates the __wrap_iter iterator comparison. When another C++ component in the same module (WCDB) also instantiates std::vector<std::string>, this collides (cf. the multiple definitions of symbol ...__wrap_iter... reported in swiftlang/swift#67410), and Swift then drops the automatic CxxRandomAccessCollection conformance for std::vector<std::string> in that module — so .map is no longer available.

This is consistent with swiftlang/swift#67410 ("std::vector<std::string> fails to conform to CxxRandomAccessCollection").

Evidence

Scenario .map compiles?
nitro 0.31 (no generated operator==) + WCDB, same code ✅ yes
nitro 0.35 (generated operator==) + WCDB ❌ no
nitro 0.35, but remove the WCDB dependency from the module ✅ yes
Other 0.35 nitro modules (no heavy C++ lib) with the same string[].map ✅ yes

So: same toolchain, same WCDB — the only delta that breaks it is nitro 0.35's generated operator== co-existing with WCDB's C++ in one module.

(We did not hand-remove operator== from the generated output to fully isolate it, since generated files are regenerated and we didn't want to hand-edit them. We can patch nitrogen to confirm if that would help.)

On opt-out

Looking at nitrogen/src/syntax/c++/CppStruct.ts, the operator is emitted purely structurally:

const isEquatable = properties.every((p) => p.isEquatable);
if (isEquatable) { /* emit friend bool operator==(...) = default; */ }

There is currently no annotation or nitro.json option to opt out — any all-equatable struct always gets it. Since Equatable is documented as a DX/convenience feature (not a runtime requirement), it would be very helpful if it could be made opt-out for cases where it has build-breaking side effects.

Question / ask

What is the recommended way to use nitrogen-generated HybridObjects in a module that also links a heavy third-party C++ library?

Specifically:

  1. Could nitrogen offer a way to opt out of the generated struct operator== / Equatable — e.g. a per-struct annotation or a per-module nitro.json flag? Since it's a convenience feature, making it opt-out would let projects that link heavy C++ libraries avoid forcing the std::vector comparison instantiation that triggers std::vector<std::string> fails to conform to CxxRandomAccessCollection swiftlang/swift#67410.
  2. Is the recommended pattern instead to keep the generated specs/structs in a module that does NOT link the C++ library, and put the implementation (which uses the C++ lib) in a separate module with manual HybridObjectRegistry::registerHybridObjectConstructor(...)? Any caveats with that split?
  3. Is this considered a known Swift C++ interop limitation (std::vector<std::string> fails to conform to CxxRandomAccessCollection swiftlang/swift#67410) that nitro recommends working around on the user side?

Thanks a lot for nitro — happy to provide more details or a minimal repro if helpful.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions