From b2f527e7ee74e517e46024da4e6a6223bfbdc171 Mon Sep 17 00:00:00 2001 From: Iceman Date: Tue, 2 Jun 2026 14:53:54 +0900 Subject: [PATCH 1/3] Add generic equals method --- .../MySwiftLibrary/EquatableClass.swift | 30 +++++++++++ .../java/com/example/swift/EquatableTest.java | 52 +++++++++++++++++++ ...t2JavaGenerator+JavaBindingsPrinting.swift | 7 +++ Sources/SwiftJava/SwiftObjects.swift | 37 +++++++++++++ .../org/swift/swiftkit/core/SwiftObjects.java | 1 + 5 files changed, 127 insertions(+) create mode 100644 Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/EquatableClass.swift create mode 100644 Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/EquatableTest.java diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/EquatableClass.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/EquatableClass.swift new file mode 100644 index 000000000..ccecc6872 --- /dev/null +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/EquatableClass.swift @@ -0,0 +1,30 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2026 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +public class EquatableClass: Equatable { + public let value: Int + public init(value: Int) { + self.value = value + } + + public static func == (lhs: EquatableClass, rhs: EquatableClass) -> Bool { + lhs.value == rhs.value + } +} + +public class EquatableSubclass: EquatableClass { + public override init(value: Int) { + super.init(value: value) + } +} diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/EquatableTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/EquatableTest.java new file mode 100644 index 000000000..04ea3ea7b --- /dev/null +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/EquatableTest.java @@ -0,0 +1,52 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2026 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +package com.example.swift; + +import org.junit.jupiter.api.Test; +import org.swift.swiftkit.core.SwiftArena; + +import static org.junit.jupiter.api.Assertions.*; + +@SuppressWarnings({"AssertBetweenInconvertibleTypes", "EqualsWithItself"}) +public class EquatableTest { + @Test + void genericStructType() { + try (var arena = SwiftArena.ofConfined()) { + var a = MyIDs.makeIntID(42, arena); + var b = MyIDs.makeIntID(42, arena); + var c = MyIDs.makeIntID(0, arena); + var d = MyIDs.makeStringID("42", arena); + assertEquals(a, a); + assertEquals(a, b); + assertNotEquals(a, c); + assertNotEquals(a, d); + assertNotEquals("foo", a); + } + } + + @Test + void classType() { + try (var arena = SwiftArena.ofConfined()) { + var a = EquatableClass.init(42, arena); + var b = EquatableSubclass.init(42, arena); + var c = EquatableSubclass.init(0, arena); + assertEquals(a, b); + assertEquals(b, a); + assertEquals(b, b); + assertNotEquals(a, c); + assertNotEquals(b, c); + } + } +} diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift index ec7b1448c..b6211ae60 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift @@ -373,6 +373,13 @@ extension JNISwift2JavaGenerator { printer.print( """ + public boolean equals(Object obj) { + if (obj instanceof JNISwiftInstance rhs) { + return SwiftObjects.equals(this.$memoryAddress(), this.$typeMetadataAddress(), rhs.$memoryAddress(), rhs.$typeMetadataAddress()); + } + return false; + } + public java.lang.String toString() { return SwiftObjects.toString(this.$memoryAddress(), this.$typeMetadataAddress()); } diff --git a/Sources/SwiftJava/SwiftObjects.swift b/Sources/SwiftJava/SwiftObjects.swift index cf5e02c9d..d13b43a04 100644 --- a/Sources/SwiftJava/SwiftObjects.swift +++ b/Sources/SwiftJava/SwiftObjects.swift @@ -98,4 +98,41 @@ extension SwiftObjects { let typeMetadata = unsafeBitCast(selfType$, to: Any.Type.self) return String(describing: typeMetadata) } + + @JavaMethod + public static func equals(environment: UnsafeMutablePointer!, lhsPointer: Int64, lhsTypePointer: Int64, rhsPointer: Int64, rhsTypePointer: Int64) -> Bool { + guard let lhsType$ = UnsafeRawPointer(bitPattern: Int(lhsTypePointer)) else { + fatalError("lhsType metadata address was null") + } + let lhsMetatype = unsafeBitCast(lhsType$, to: Any.Type.self) + guard let lhsMetatype = lhsMetatype as? (any Equatable.Type) else { + return false + } + + guard let rhsType$ = UnsafeRawPointer(bitPattern: Int(rhsTypePointer)) else { + fatalError("rhsType metadata address was null") + } + let rhsMetatype = unsafeBitCast(rhsType$, to: Any.Type.self) + guard let rhsMetatype = rhsMetatype as? (any Equatable.Type) else { + return false + } + + func perform(lhsType: L.Type, rhsType: R.Type) -> Bool { + guard let lhs$ = UnsafeMutablePointer(bitPattern: Int(lhsPointer)) else { + fatalError("lhs memory address was null") + } + guard let rhs$ = UnsafeMutablePointer(bitPattern: Int(rhsPointer)) else { + fatalError("rhs memory address was null") + } + if lhsType == rhsType { + return lhs$.pointee == rhs$.pointee as! L + } else if let lhs = lhs$.pointee as? R { + return lhs == rhs$.pointee + } else if let rhs = rhs$.pointee as? L { + return lhs$.pointee == rhs + } + return false + } + return perform(lhsType: lhsMetatype, rhsType: rhsMetatype) + } } diff --git a/SwiftKitCore/src/main/java/org/swift/swiftkit/core/SwiftObjects.java b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/SwiftObjects.java index d98f722d0..29c7593e9 100644 --- a/SwiftKitCore/src/main/java/org/swift/swiftkit/core/SwiftObjects.java +++ b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/SwiftObjects.java @@ -29,4 +29,5 @@ public static void requireNonZero(long number, String name) { public static native String toDebugString(long selfPointer, long selfTypePointer); public static native void destroy(long selfPointer, long selfTypePointer); public static native String typeDescription(long selfTypePointer); + public static native boolean equals(long lhsPointer, long lhsTypePointer, long rhsPointer, long rhsTypePointer); } From de1c0d42f43fab668301542e31ab8fce656284bb Mon Sep 17 00:00:00 2001 From: Iceman Date: Tue, 2 Jun 2026 15:57:53 +0900 Subject: [PATCH 2/3] implement hashCode() --- ...t2JavaGenerator+JavaBindingsPrinting.swift | 4 ++++ Sources/SwiftJava/SwiftObjects.swift | 20 +++++++++++++++++++ .../org/swift/swiftkit/core/SwiftObjects.java | 1 + 3 files changed, 25 insertions(+) diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift index b6211ae60..d6191a475 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift @@ -380,6 +380,10 @@ extension JNISwift2JavaGenerator { return false; } + public int hashCode() { + return SwiftObjects.hashCode(this.$memoryAddress(), this.$typeMetadataAddress()); + } + public java.lang.String toString() { return SwiftObjects.toString(this.$memoryAddress(), this.$typeMetadataAddress()); } diff --git a/Sources/SwiftJava/SwiftObjects.swift b/Sources/SwiftJava/SwiftObjects.swift index d13b43a04..742a4da6d 100644 --- a/Sources/SwiftJava/SwiftObjects.swift +++ b/Sources/SwiftJava/SwiftObjects.swift @@ -135,4 +135,24 @@ extension SwiftObjects { } return perform(lhsType: lhsMetatype, rhsType: rhsMetatype) } + + @JavaMethod + public static func hashCode(environment: UnsafeMutablePointer!, selfPointer: Int64, selfTypePointer: Int64) -> Int32 { + guard let selfType$ = UnsafeRawPointer(bitPattern: Int(selfTypePointer)) else { + fatalError("selfType metadata address was null") + } + let typeMetadata = unsafeBitCast(selfType$, to: Any.Type.self) + guard let typeMetadata = typeMetadata as? (any Hashable.Type) else { + // For value types, different instances may return different hash codes even if the values are same. + return Int32(truncatingIfNeeded: selfPointer.hashValue) + } + + func perform(as type: T.Type) -> Int32 { + guard let self$ = UnsafeMutablePointer(bitPattern: Int(selfPointer)) else { + fatalError("self memory address was null") + } + return Int32(truncatingIfNeeded: self$.pointee.hashValue) + } + return perform(as: typeMetadata) + } } diff --git a/SwiftKitCore/src/main/java/org/swift/swiftkit/core/SwiftObjects.java b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/SwiftObjects.java index 29c7593e9..a8cce9166 100644 --- a/SwiftKitCore/src/main/java/org/swift/swiftkit/core/SwiftObjects.java +++ b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/SwiftObjects.java @@ -30,4 +30,5 @@ public static void requireNonZero(long number, String name) { public static native void destroy(long selfPointer, long selfTypePointer); public static native String typeDescription(long selfTypePointer); public static native boolean equals(long lhsPointer, long lhsTypePointer, long rhsPointer, long rhsTypePointer); + public static native int hashCode(long selfPointer, long selfTypePointer); } From 2c64c6f7f8fdf37e301c42224faa5b5e5e406203 Mon Sep 17 00:00:00 2001 From: Iceman Date: Tue, 2 Jun 2026 17:43:31 +0900 Subject: [PATCH 3/3] Add HashSet test case --- ...uatableClass.swift => HashableClass.swift} | 10 ++-- .../{EquatableTest.java => HashableTest.java} | 49 ++++++++++++++++--- Sources/SwiftJava/SwiftObjects.swift | 15 ++++++ 3 files changed, 65 insertions(+), 9 deletions(-) rename Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/{EquatableClass.swift => HashableClass.swift} (73%) rename Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/{EquatableTest.java => HashableTest.java} (50%) diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/EquatableClass.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/HashableClass.swift similarity index 73% rename from Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/EquatableClass.swift rename to Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/HashableClass.swift index ccecc6872..896a945d1 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/EquatableClass.swift +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/HashableClass.swift @@ -12,18 +12,22 @@ // //===----------------------------------------------------------------------===// -public class EquatableClass: Equatable { +public class HashableClass: Hashable { public let value: Int public init(value: Int) { self.value = value } - public static func == (lhs: EquatableClass, rhs: EquatableClass) -> Bool { + public static func == (lhs: HashableClass, rhs: HashableClass) -> Bool { lhs.value == rhs.value } + + public func hash(into hasher: inout Hasher) { + hasher.combine(value) + } } -public class EquatableSubclass: EquatableClass { +public class HashableSubclass: HashableClass { public override init(value: Int) { super.init(value: value) } diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/EquatableTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/HashableTest.java similarity index 50% rename from Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/EquatableTest.java rename to Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/HashableTest.java index 04ea3ea7b..03bedd448 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/EquatableTest.java +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/HashableTest.java @@ -19,10 +19,13 @@ import static org.junit.jupiter.api.Assertions.*; +import java.util.HashSet; +import java.util.List; + @SuppressWarnings({"AssertBetweenInconvertibleTypes", "EqualsWithItself"}) -public class EquatableTest { +public class HashableTest { @Test - void genericStructType() { + void valueTypeEquals() { try (var arena = SwiftArena.ofConfined()) { var a = MyIDs.makeIntID(42, arena); var b = MyIDs.makeIntID(42, arena); @@ -37,11 +40,11 @@ void genericStructType() { } @Test - void classType() { + void referenceTypeEquals() { try (var arena = SwiftArena.ofConfined()) { - var a = EquatableClass.init(42, arena); - var b = EquatableSubclass.init(42, arena); - var c = EquatableSubclass.init(0, arena); + var a = HashableClass.init(42, arena); + var b = HashableSubclass.init(42, arena); + var c = HashableSubclass.init(0, arena); assertEquals(a, b); assertEquals(b, a); assertEquals(b, b); @@ -49,4 +52,38 @@ void classType() { assertNotEquals(b, c); } } + + @Test + void hashSetValueType() { + try (var arena = SwiftArena.ofConfined()) { + var a = MyIDs.makeIntID(42, arena); + var b = MyIDs.makeIntID(42, arena); + var c = MyIDs.makeIntID(0, arena); + var set = new HashSet<>(List.of( + a, b + )); + assertTrue(set.contains(a)); + assertTrue(set.contains(b)); + assertFalse(set.contains(c)); + assertEquals(1, set.size()); + } + } + + @Test + void hashSetReferenceType() { + try (var arena = SwiftArena.ofConfined()) { + var a = HashableClass.init(42, arena); + var b = HashableClass.init(42, arena); + var c = HashableSubclass.init(42, arena); + var d = HashableSubclass.init(0, arena); + var set = new HashSet<>(List.of( + a, b, c + )); + assertTrue(set.contains(a)); + assertTrue(set.contains(b)); + assertTrue(set.contains(c)); + assertFalse(set.contains(d)); + assertEquals(1, set.size()); + } + } } diff --git a/Sources/SwiftJava/SwiftObjects.swift b/Sources/SwiftJava/SwiftObjects.swift index 742a4da6d..a3f510d94 100644 --- a/Sources/SwiftJava/SwiftObjects.swift +++ b/Sources/SwiftJava/SwiftObjects.swift @@ -156,3 +156,18 @@ extension SwiftObjects { return perform(as: typeMetadata) } } + +public class HashableClass: Hashable { + public let value: Int + public init(value: Int) { + self.value = value + } + + public static func == (lhs: HashableClass, rhs: HashableClass) -> Bool { + lhs.value == rhs.value + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(value) + } +}