diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/HashableClass.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/HashableClass.swift new file mode 100644 index 000000000..896a945d1 --- /dev/null +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/HashableClass.swift @@ -0,0 +1,34 @@ +//===----------------------------------------------------------------------===// +// +// 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 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) + } +} + +public class HashableSubclass: HashableClass { + public override init(value: Int) { + super.init(value: value) + } +} diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/HashableTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/HashableTest.java new file mode 100644 index 000000000..03bedd448 --- /dev/null +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/HashableTest.java @@ -0,0 +1,89 @@ +//===----------------------------------------------------------------------===// +// +// 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.*; + +import java.util.HashSet; +import java.util.List; + +@SuppressWarnings({"AssertBetweenInconvertibleTypes", "EqualsWithItself"}) +public class HashableTest { + @Test + void valueTypeEquals() { + 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 referenceTypeEquals() { + try (var arena = SwiftArena.ofConfined()) { + 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); + assertNotEquals(a, c); + 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/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift index ec7b1448c..d6191a475 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift @@ -373,6 +373,17 @@ 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 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 cf5e02c9d..a3f510d94 100644 --- a/Sources/SwiftJava/SwiftObjects.swift +++ b/Sources/SwiftJava/SwiftObjects.swift @@ -98,4 +98,76 @@ 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) + } + + @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) + } +} + +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) + } } 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..a8cce9166 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,6 @@ 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); + public static native int hashCode(long selfPointer, long selfTypePointer); }