Skip to content

CxxInterop: Swift does not synthesize CxxConvertibleToContainer iterator operator== in some cases #77607

Open
@ADKaster

Description

@ADKaster

Description

In Ladybird, all iterators for our AK containers inherit from AK::SimpleIterator: https://github.com/LadybirdBrowser/ladybird/blob/3b04c983f14a3838870f202fe38fe081cb0771b4/AK/Iterator.h#L16

When I tried to use myVec.forEach { elem in ... } in my new code, I ran into two issues:

  • In code that was importing both AK and another module that uses AK, the compiler informed me that the forEach method on my container did not exist.

  • In code that only imports AK (a test file), the compiler confirmed that the exact type used in the other module is indeed a CxxConvertibleToContainer type, but failed with a bogus linker error.

This issue contains a repro for the second issue, since I suspect they are linked.

Reproduction

git clone https://github.com/LadybirdBrowser/ladybird.git
// install dependencies from https://github.com/LadybirdBrowser/ladybird/blob/master/Documentation/BuildInstructionsLadybird.md#build-prerequisites
// apply patch below
cmake -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -DENABLE_SWIFT=ON --preset default
ninja -C release TestAKBindings
./Build/release/bin/TestAKBindings

Apply this patch:

diff --git a/AK/Vector.h b/AK/Vector.h
index 41ac977fa13..5cbf6071544 100644
--- a/AK/Vector.h
+++ b/AK/Vector.h
@@ -857,6 +857,9 @@ private:
 template<class... Args>
 Vector(Args... args) -> Vector<CommonType<Args...>>;
 
+using U32Vector = Vector<u32>;
+using U32Vector2Capacity = Vector<u32, 2>;
+
 }
 
 #if USING_AK_GLOBALLY
diff --git a/Tests/AK/TestAKBindings.swift b/Tests/AK/TestAKBindings.swift
index 37211c021d4..22c0dc26d4b 100644
--- a/Tests/AK/TestAKBindings.swift
+++ b/Tests/AK/TestAKBindings.swift
@@ -14,6 +14,12 @@ private func isCxxSequenceType<T: ~Copyable>(_ type: borrowing T.Type) -> Bool {
     return CxxSequenceMarker<T>.self is ConformanceMarker.Type
 }
 
+enum CxxConvertibleToCollectionMarker<T: ~Copyable> {}
+extension CxxConvertibleToCollectionMarker: ConformanceMarker where T: CxxConvertibleToCollection {}
+private func isCxxConvertibleToCollectionType<T: ~Copyable>(_ type: borrowing T.Type) -> Bool {
+    return CxxConvertibleToCollectionMarker<T>.self is ConformanceMarker.Type
+}
+
 class StandardError: TextOutputStream {
     func write(_ string: Swift.String) {
         try! FileHandle.standardError.write(contentsOf: Data(string.utf8))
@@ -26,7 +32,7 @@ struct TestAKBindings {
         var standardError = StandardError()
         print("Testing CxxSequence types...", to: &standardError)
 
-        precondition(isCxxSequenceType(AK.StringView.self))
+        //precondition(isCxxSequenceType(AK.StringView.self))
         precondition(isCxxSequenceType(AK.Bytes.self))
         precondition(isCxxSequenceType(AK.ReadonlyBytes.self))
         precondition(isCxxSequenceType(AK.Utf16Data.self))
@@ -35,14 +41,33 @@ struct TestAKBindings {
         precondition(!isCxxSequenceType(AK.String.self))
 
         precondition(!isCxxSequenceType(AK.Error.self))
+        precondition(isCxxSequenceType(AK.U32Vector.self))
+        precondition(isCxxSequenceType(AK.U32Vector2Capacity.self))
 
         print("CxxSequence types pass", to: &standardError)
     }
 
+    static func testCollectionTypesAreBound() {
+        var standardError = StandardError()
+        print ("Testing CxxConvertibleToCollection types...", to: &standardError)
+
+        precondition(isCxxConvertibleToCollectionType(AK.U32Vector.self))
+        precondition(isCxxConvertibleToCollectionType(AK.U32Vector2Capacity.self))
+
+        let u32Vector = AK.U32Vector()
+        u32Vector.forEach { _ in }
+
+        let u32Vector2Capacity = AK.U32Vector2Capacity()
+        u32Vector2Capacity.forEach { _ in }
+
+        print("CxxConvertibleToCollection types pass", to: &standardError)
+    }
+
     static func main() {
         var standardError = StandardError()
         print("Starting test suite...", to: &standardError)
         testSequenceTypesAreBound()
+        testCollectionTypesAreBound()
 
         print("All tests pass", to: &standardError)
     }

Result:

ninja: Entering directory `Build/release/'
[0/2] Re-checking globbed directories...
[1/2] Linking Swift executable bin/TestAKBindings
FAILED: bin/TestAKBindings 
: && /home/andrew/.local/bin/swiftc -j 32 -num-threads 32 -emit-executable -o bin/TestAKBindings -O -g Lagom/Tests/AK/CMakeFiles/TestAKBindings.dir/TestAKBindings.swift.o /home/andrew/.local/share/swiftly/toolchains/main-snapshot-2024-10-30/usr/lib/swift/linux/x86_64/swiftrt.o -use-ld=lld -L /home/andrew/ladybird-org/ladybird-browser/Build/release/lib   -L /home/andrew/.local/share/swiftly/toolchains/main-snapshot-2024-10-30/usr/lib/swift/linux   -L /home/andrew/.local/share/swiftly/toolchains/main-snapshot-2024-10-30/usr/lib/swift/linux/x86_64 -Xlinker -rpath -Xlinker /home/andrew/ladybird-org/ladybird-browser/Build/release/vcpkg_installed/x64-linux/lib:/home/andrew/.local/share/swiftly/toolchains/main-snapshot-2024-10-30/usr/lib/swift/linux:/home/andrew/.local/share/swiftly/toolchains/main-snapshot-2024-10-30/usr/lib/swift/linux/x86_64:/home/andrew/ladybird-org/ladybird-browser/Build/release/lib  lib/liblagom-ak.so.0.0.0 && :
error: link command failed with exit code 1 (use -v to see invocation)
ld.lld: error: undefined symbol: AK::SimpleIterator<AK::Vector<unsigned int, 0ul> const, unsigned int const>::operator==(AK::SimpleIterator<AK::Vector<unsigned int, 0ul> const, unsigned int const> const&) const
>>> referenced by TestAKBindings.swift.o
>>>               Lagom/Tests/AK/CMakeFiles/TestAKBindings.dir/TestAKBindings.swift.o:($sSo2AKO0095SimpleIteratorVectorCUnsignedInt_CUnsignedLong_0_constCUnsignedInt_const_mrDAEawaAJfxaDAjgaCqAaVSQSCSQ2eeoiySbx_xtFZTW)
>>> referenced by <compiler-generated>:0 (/<compiler-generated>:0)
>>>               Lagom/Tests/AK/CMakeFiles/TestAKBindings.dir/TestAKBindings.swift.o:($sSTsE21_copySequenceContents12initializing8IteratorQz_SitSry7ElementQzG_tFSo2AKO0047VectorCUnsignedInt_CUnsignedLong_0_sfBJmlmajtndV_Tgq5)
>>> referenced by <stdin>:0
>>>               Lagom/Tests/AK/CMakeFiles/TestAKBindings.dir/TestAKBindings.swift.o:($s14TestAKBindingsAAV27testCollectionTypesAreBoundyyFZTf4d_n)
>>> referenced 1 more times

ld.lld: error: undefined symbol: AK::SimpleIterator<AK::Vector<unsigned int, 2ul> const, unsigned int const>::operator==(AK::SimpleIterator<AK::Vector<unsigned int, 2ul> const, unsigned int const> const&) const
>>> referenced by TestAKBindings.swift.o
>>>               Lagom/Tests/AK/CMakeFiles/TestAKBindings.dir/TestAKBindings.swift.o:($sSo2AKO0095SimpleIteratorVectorCUnsignedInt_CUnsignedLong_2_constCUnsignedInt_const_mrDAEawaAJfxaDAjgaCqAaVSQSCSQ2eeoiySbx_xtFZTW)
>>> referenced by <compiler-generated>:0 (/<compiler-generated>:0)
>>>               Lagom/Tests/AK/CMakeFiles/TestAKBindings.dir/TestAKBindings.swift.o:($sSTsE21_copySequenceContents12initializing8IteratorQz_SitSry7ElementQzG_tFSo2AKO0047VectorCUnsignedInt_CUnsignedLong_2_sfBJmlmajtndV_Tgq5)
>>> referenced by <stdin>:0
>>>               Lagom/Tests/AK/CMakeFiles/TestAKBindings.dir/TestAKBindings.swift.o:($s14TestAKBindingsAAV27testCollectionTypesAreBoundyyFZTf4d_n)
>>> referenced 1 more times
clang: error: linker command failed with exit code 1 (use -v to see invocation)
ninja: build stopped: subcommand failed.

Expected behavior

The test should compile, link, and run without issue.

Environment

$ swiftc -version
Swift version 6.1-dev (LLVM 9d655fdc6103926, Swift 0d16cbf)
Target: x86_64-unknown-linux-gnu

Distro: Ubuntu 24.04
uname -a: Linux Andrew-Workstation 6.8.0-48-generic #48-Ubuntu SMP PREEMPT_DYNAMIC Fri Sep 27 14:04:52 UTC 2024 x86_64 x86_64 x86_64 GNU/Linux

Additional information

operator== on the iterator type is defined as

    constexpr bool operator==(SimpleIterator const& other) const = default;
    constexpr auto operator<=>(SimpleIterator const& other) const = default;

Removing constexpr has no effect.

Adding a translation unit to force generation of the operator== (see details pane below) adds the operator== as LOCAL HIDDEN symbols. Clang seems to expect each TU to generate these symbols if it needs them.

Patch to liblagom-ak.so to contain the missing symbols
diff --git a/AK/AK+Swift.cpp b/AK/AK+Swift.cpp
new file mode 100644
index 00000000000..f9599211337
--- /dev/null
+++ b/AK/AK+Swift.cpp
@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) 2024, Andrew Kaster <andrew@ladybird.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <AK/Format.h>
+#include <AK/Vector.h>
+
+#pragma clang optimize off
+
+namespace AK {
+
+void __swift_use_vector();
+
+[[gnu::used, clang::optnone]] void __swift_use_vector() {
+    auto x = Vector<u32> {};
+    x.append(1);
+
+    for (auto i : x) {
+        dbgln("{}", i);
+    }
+
+    auto x2 = Vector<u32, 2> {};
+    x2.append(1);
+    x2.append(2);
+    for (auto i : x2) {
+        dbgln("{}", i);
+    }
+}
+
+}
diff --git a/AK/CMakeLists.txt b/AK/CMakeLists.txt
index 3fbdc65b90a..21335e70efa 100644
--- a/AK/CMakeLists.txt
+++ b/AK/CMakeLists.txt
@@ -80,7 +80,7 @@ if (ENABLE_SWIFT)
         "${CMAKE_CURRENT_BINARY_DIR}/Backtrace.h"
         "${CMAKE_CURRENT_BINARY_DIR}/Debug.h"
     )
-    target_sources(AK PRIVATE AK+Swift.swift)
+    target_sources(AK PRIVATE AK+Swift.swift AK+Swift.cpp)
     add_swift_target_properties(AK)
 endif()
 
diff --git a/AK/Vector.h b/AK/Vector.h
index 41ac977fa13..5cbf6071544 100644
--- a/AK/Vector.h
+++ b/AK/Vector.h
@@ -857,6 +857,9 @@ private:
 template<class... Args>
 Vector(Args... args) -> Vector<CommonType<Args...>>;
 
+using U32Vector = Vector<u32>;
+using U32Vector2Capacity = Vector<u32, 2>;
+
 }
 
 #if USING_AK_GLOBALLY
diff --git a/Tests/AK/TestAKBindings.swift b/Tests/AK/TestAKBindings.swift
index 37211c021d4..22c0dc26d4b 100644
--- a/Tests/AK/TestAKBindings.swift
+++ b/Tests/AK/TestAKBindings.swift
@@ -14,6 +14,12 @@ private func isCxxSequenceType<T: ~Copyable>(_ type: borrowing T.Type) -> Bool {
     return CxxSequenceMarker<T>.self is ConformanceMarker.Type
 }
 
+enum CxxConvertibleToCollectionMarker<T: ~Copyable> {}
+extension CxxConvertibleToCollectionMarker: ConformanceMarker where T: CxxConvertibleToCollection {}
+private func isCxxConvertibleToCollectionType<T: ~Copyable>(_ type: borrowing T.Type) -> Bool {
+    return CxxConvertibleToCollectionMarker<T>.self is ConformanceMarker.Type
+}
+
 class StandardError: TextOutputStream {
     func write(_ string: Swift.String) {
         try! FileHandle.standardError.write(contentsOf: Data(string.utf8))
@@ -26,7 +32,7 @@ struct TestAKBindings {
         var standardError = StandardError()
         print("Testing CxxSequence types...", to: &standardError)
 
-        precondition(isCxxSequenceType(AK.StringView.self))
+        //precondition(isCxxSequenceType(AK.StringView.self))
         precondition(isCxxSequenceType(AK.Bytes.self))
         precondition(isCxxSequenceType(AK.ReadonlyBytes.self))
         precondition(isCxxSequenceType(AK.Utf16Data.self))
@@ -35,14 +41,33 @@ struct TestAKBindings {
         precondition(!isCxxSequenceType(AK.String.self))
 
         precondition(!isCxxSequenceType(AK.Error.self))
+        precondition(isCxxSequenceType(AK.U32Vector.self))
+        precondition(isCxxSequenceType(AK.U32Vector2Capacity.self))
 
         print("CxxSequence types pass", to: &standardError)
     }
 
+    static func testCollectionTypesAreBound() {
+        var standardError = StandardError()
+        print ("Testing CxxConvertibleToCollection types...", to: &standardError)
+
+        precondition(isCxxConvertibleToCollectionType(AK.U32Vector.self))
+        precondition(isCxxConvertibleToCollectionType(AK.U32Vector2Capacity.self))
+
+        let u32Vector = AK.U32Vector()
+        u32Vector.forEach { _ in }
+
+        let u32Vector2Capacity = AK.U32Vector2Capacity()
+        u32Vector2Capacity.forEach { _ in }
+
+        print("CxxConvertibleToCollection types pass", to: &standardError)
+    }
+
     static func main() {
         var standardError = StandardError()
         print("Starting test suite...", to: &standardError)
         testSequenceTypesAreBound()
+        testCollectionTypesAreBound()
 
         print("All tests pass", to: &standardError)
     }

The symbols appear like so, with that patch applied:

readelf -Ws Build/release/lib/liblagom-ak.so.0.0.0 | llvm-cxxfilt-18 |  grep "operator==(AK::SimpleIterator"
  1253: 0000000000064c70    21 FUNC    LOCAL  HIDDEN    27 AK::SimpleIterator<AK::Vector<unsigned int, 0ul>, unsigned int>::operator==(AK::SimpleIterator<AK::Vector<unsigned int, 0ul>, unsigned int> const&) const
  1259: 0000000000064d50    21 FUNC    LOCAL  HIDDEN    27 AK::SimpleIterator<AK::Vector<unsigned int, 2ul>, unsigned int>::operator==(AK::SimpleIterator<AK::Vector<unsigned int, 2ul>, unsigned int> const&) const

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugA deviation from expected or documented behavior. Also: expected but undesirable behavior.triage neededThis issue needs more specific labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions