diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/BoxSpecialization.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/BoxSpecialization.swift index 31259a687..f6bf8fb7a 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/BoxSpecialization.swift +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/BoxSpecialization.swift @@ -12,7 +12,7 @@ // //===----------------------------------------------------------------------===// -public struct Box { +public struct Box: Hashable { public var count: Int64 public init(count: Int64) { @@ -20,7 +20,7 @@ public struct Box { } } -public struct Fish { +public struct Fish: Hashable { public var name: String public init(name: String) { diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/CollectionBoxable.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/CollectionBoxable.swift new file mode 100644 index 000000000..c2f9d374d --- /dev/null +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/CollectionBoxable.swift @@ -0,0 +1,97 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +public func makeIntToFishDictionary() -> [Int: Fish] { + [ + 1: Fish(name: "salmon"), + 2: Fish(name: "clownfish"), + ] +} + +public func intToFishDictionary(dict: [Int: Fish]) -> [Int: Fish] { + dict +} + +public func makeFishSet() -> Set { + [ + Fish(name: "salmon"), + Fish(name: "clownfish"), + ] +} + +public func fishSet(set: Set) -> Set { + set +} + +public func makeMyIDToFish() -> [MyID: Fish] { + [ + .init(0): Fish(name: "salmon"), + .init(1): Fish(name: "clownfish"), + ] +} + +public func makeSpecializedGenericTypeSet() -> Set { + [.init(count: 2), .init(count: 3)] +} + +public func makeSetInDictionary() -> [String: Set] { + [ + "even": [0, 2, 4], + "odd": [1, 3, 5], + ] +} + +public func makeIntArrayDictionary() -> [String: [Int32]] { + [ + "even": [0, 2, 4], + "odd": [1, 3, 5], + ] +} + +public func intArrayDictionary(dict: [String: [Int32]]) -> [String: [Int32]] { + dict +} + +public func makeFishArrayDictionary() -> [String: [Fish]] { + [ + "reef": [ + Fish(name: "clownfish"), + Fish(name: "blue tang"), + ], + "river": [ + Fish(name: "salmon") + ], + ] +} + +public func fishArrayDictionary(dict: [String: [Fish]]) -> [String: [Fish]] { + dict +} + +public func makeOptionalFishDictionary() -> [String: Fish?] { + [ + "reef": Fish(name: "clownfish"), + "empty": nil, + ] +} + +public func optionalFishDictionary(dict: [String: Fish?]) -> [String: Fish?] { + dict +} diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/GenericType.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/GenericType.swift index 420afda53..3f27d08eb 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/GenericType.swift +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/GenericType.swift @@ -12,7 +12,7 @@ // //===----------------------------------------------------------------------===// -public struct MyID { +public struct MyID: Hashable { public var rawValue: T public init(_ rawValue: T) { self.rawValue = rawValue diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/CollectionBoxableTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/CollectionBoxableTest.java new file mode 100644 index 000000000..63c874f9a --- /dev/null +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/CollectionBoxableTest.java @@ -0,0 +1,153 @@ +//===----------------------------------------------------------------------===// +// +// 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 org.swift.swiftkit.core.collections.SwiftDictionaryMap; +import org.swift.swiftkit.core.collections.SwiftSet; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.Arrays; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +public class CollectionBoxableTest { + @Test + void intToFishDictionaryRoundtrip() { + try (var arena = SwiftArena.ofConfined()) { + SwiftDictionaryMap original = MySwiftLibrary.makeIntToFishDictionary(arena); + assertEquals(2, original.size()); + assertEquals("salmon", original.get(1L).getName()); + assertEquals("clownfish", original.get(2L).getName()); + + SwiftDictionaryMap roundtripped = MySwiftLibrary.intToFishDictionary(original, arena); + assertEquals(2, roundtripped.size()); + assertEquals("salmon", roundtripped.get(1L).getName()); + assertEquals("clownfish", roundtripped.get(2L).getName()); + } + } + + @Test + void fishSetRoundtrip() { + try (var arena = SwiftArena.ofConfined()) { + SwiftSet original = MySwiftLibrary.makeFishSet(arena); + assertEquals(2, original.size()); + assertTrue(original.contains(Fish.init("salmon", arena))); + assertTrue(original.contains(Fish.init("clownfish", arena))); + + SwiftSet roundtripped = MySwiftLibrary.fishSet(original, arena); + assertEquals(2, roundtripped.size()); + assertTrue(roundtripped.contains(Fish.init("salmon", arena))); + assertTrue(roundtripped.contains(Fish.init("clownfish", arena))); + } + } + + @Test + void makeMyIDToFish() { + try (var arena = SwiftArena.ofConfined()) { + SwiftDictionaryMap, Fish> dict = MySwiftLibrary.makeMyIDToFish(arena); + assertEquals(2, dict.size()); + + MyID salmonId = MyIDs.makeIntID(0, arena); + MyID clownfishId = MyIDs.makeIntID(1, arena); + MyID unknownId = MyIDs.makeIntID(-100, arena); + + assertTrue(dict.containsKey(salmonId)); + assertTrue(dict.containsKey(clownfishId)); + assertFalse(dict.containsKey(unknownId)); + assertEquals("salmon", dict.get(salmonId).getName()); + assertEquals("clownfish", dict.get(clownfishId).getName()); + } + } + + @Test + void makeSpecializedGenericTypeSet() { + try (var arena = SwiftArena.ofConfined()) { + SwiftSet> set = MySwiftLibrary.makeSpecializedGenericTypeSet(arena); + + assertEquals( + set.stream() + .map(Box::getCount) + .collect(Collectors.toSet()), + Set.of(2L, 3L) + ); + } + } + + @Test + void makeSetInDictionary() { + try (var arena = SwiftArena.ofConfined()) { + SwiftDictionaryMap> dict = MySwiftLibrary.makeSetInDictionary(arena); + assertEquals(Set.of(0, 2, 4), dict.get("even").toJavaSet()); + assertNull(dict.get("unknown")); + } + } + + @Test + void intArrayDictionaryRoundtrip() { + try (var arena = SwiftArena.ofConfined()) { + SwiftDictionaryMap original = MySwiftLibrary.makeIntArrayDictionary(arena); + assertArrayEquals(new Integer[] {0, 2, 4}, original.get("even")); + + SwiftDictionaryMap roundtripped = MySwiftLibrary.intArrayDictionary(original, arena); + assertArrayEquals(new Integer[] {0, 2, 4}, roundtripped.get("even")); + } + } + + @Test + void fishArrayDictionaryRoundtrip() { + try (var arena = SwiftArena.ofConfined()) { + SwiftDictionaryMap original = MySwiftLibrary.makeFishArrayDictionary(arena); + assertArrayEquals(new String[] {"clownfish", "blue tang"}, fishNames(original.get("reef"))); + assertArrayEquals(new String[] {"salmon"}, fishNames(original.get("river"))); + + SwiftDictionaryMap roundtripped = MySwiftLibrary.fishArrayDictionary(original, arena); + assertArrayEquals(new String[] {"clownfish", "blue tang"}, fishNames(roundtripped.get("reef"))); + assertArrayEquals(new String[] {"salmon"}, fishNames(roundtripped.get("river"))); + } + } + + @Test + void optionalFishDictionaryRoundtrip() { + try (var arena = SwiftArena.ofConfined()) { + SwiftDictionaryMap> original = MySwiftLibrary.makeOptionalFishDictionary(arena); + assertDoesNotThrow(() -> { + var value = original.get("reef").orElseThrow(); + assertEquals("clownfish", value.getName()); + }); + assertDoesNotThrow(() -> { + assertTrue(original.get("empty").isEmpty()); + }); + + SwiftDictionaryMap> roundtripped = MySwiftLibrary.optionalFishDictionary(original, arena); + assertDoesNotThrow(() -> { + var value = roundtripped.get("reef").orElseThrow(); + assertEquals("clownfish", value.getName()); + }); + assertDoesNotThrow(() -> { + assertTrue(roundtripped.get("empty").isEmpty()); + }); + } + } + + private static String[] fishNames(Fish[] fish) { + return Arrays.stream(fish) + .map(Fish::getName) + .toArray(String[]::new); + } +} diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/DataTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/DataTest.java index a6024ca12..bc12a2322 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/DataTest.java +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/DataTest.java @@ -27,7 +27,7 @@ void data_echo() { var data = Data.fromByteArray(bytes, arena); var echoed = MySwiftLibrary.echoData(data, arena); - assertEquals(4, echoed.getCount()); + assertArrayEquals(bytes, echoed.toByteArray()); } } diff --git a/Sources/JExtractSwiftLib/JNI/JNICaching.swift b/Sources/JExtractSwiftLib/JNI/JNICaching.swift index f91d7b7d4..4ce16af9a 100644 --- a/Sources/JExtractSwiftLib/JNI/JNICaching.swift +++ b/Sources/JExtractSwiftLib/JNI/JNICaching.swift @@ -21,10 +21,22 @@ enum JNICaching { cacheName(for: type.nominalTypeDecl.qualifiedTypeName) } + static func bridgeName(for type: ImportedNominalType) -> String { + bridgeName(for: type.swiftNominal.qualifiedTypeName) + } + + static func bridgeName(for type: SwiftNominalType) -> String { + bridgeName(for: type.nominalTypeDecl.qualifiedTypeName) + } + private static func cacheName(for typeName: SwiftQualifiedTypeName) -> String { "_JNI_\(typeName.fullFlatName)" } + private static func bridgeName(for typeName: SwiftQualifiedTypeName) -> String { + "_JNIBridge_\(typeName.fullFlatName)" + } + static func cacheMemberName(for enumCase: ImportedEnumCase) -> String { "\(enumCase.enumType.nominalTypeDecl.name.firstCharacterLowercased)\(enumCase.name.firstCharacterUppercased)Cache" } diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift index 5335b81b1..1442b4158 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift @@ -995,11 +995,22 @@ extension JNISwift2JavaGenerator { valueType: SwiftType, parameterName: String ) throws -> NativeParameter { - NativeParameter( + let swiftDictionaryType = knownTypes.dictionarySugar(keyType, valueType) + let keyBridgeType = try bridgeTypeName(for: keyType) + let valueBridgeType = try bridgeTypeName(for: valueType) + return NativeParameter( parameters: [ JavaParameter(name: parameterName, type: .long) ], - conversion: .initFromJNI(.placeholder, swiftType: knownTypes.dictionarySugar(keyType, valueType)), + conversion: .constructor( + swiftDictionaryType, + arguments: [ + ("fromJNI", .placeholder), + ("in", .constant("environment")), + ("keyBridge", .constant("\(keyBridgeType).self")), + ("valueBridge", .constant("\(valueBridgeType).self")), + ] + ), indirectConversion: nil, conversionCheck: nil ) @@ -1010,12 +1021,18 @@ extension JNISwift2JavaGenerator { valueType: SwiftType, resultName: String ) throws -> NativeResult { - NativeResult( + let keyBridgeType = try bridgeTypeName(for: keyType) + let valueBridgeType = try bridgeTypeName(for: valueType) + return NativeResult( javaType: .long, conversion: .method( .placeholder, function: "dictionaryGetJNIValue", - arguments: [("in", .constant("environment"))] + arguments: [ + ("in", .constant("environment")), + ("keyBridge", .constant("\(keyBridgeType).self")), + ("valueBridge", .constant("\(valueBridgeType).self")), + ] ), outParameters: [] ) @@ -1025,11 +1042,20 @@ extension JNISwift2JavaGenerator { elementType: SwiftType, parameterName: String ) throws -> NativeParameter { - NativeParameter( + let swiftSetType = knownTypes.set(elementType) + let elementBridgeType = try bridgeTypeName(for: elementType) + return NativeParameter( parameters: [ JavaParameter(name: parameterName, type: .long) ], - conversion: .initFromJNI(.placeholder, swiftType: knownTypes.set(elementType)), + conversion: .constructor( + swiftSetType, + arguments: [ + ("fromJNI", .placeholder), + ("in", .constant("environment")), + ("elementBridge", .constant("\(elementBridgeType).self")), + ] + ), indirectConversion: nil, conversionCheck: nil ) @@ -1039,16 +1065,59 @@ extension JNISwift2JavaGenerator { elementType: SwiftType, resultName: String ) throws -> NativeResult { - NativeResult( + let elementBridgeType = try bridgeTypeName(for: elementType) + return NativeResult( javaType: .long, conversion: .method( .placeholder, function: "setGetJNIValue", - arguments: [("in", .constant("environment"))] + arguments: [ + ("in", .constant("environment")), + ("elementBridge", .constant("\(elementBridgeType).self")), + ] ), outParameters: [] ) } + + private func bridgeTypeName(for swiftType: SwiftType) throws -> String { + switch swiftType { + case .nominal(let nominalType): + if let knownType = nominalType.asKnownType { + switch knownType { + case .optional(let wrapped): + return "OptionalBridge<\(try bridgeTypeName(for: wrapped))>" + case .array(let element): + return "ArrayBridge<\(try bridgeTypeName(for: element))>" + case .dictionary(let key, let value): + return "DictionaryBridge<\(try bridgeTypeName(for: key)), \(try bridgeTypeName(for: value))>" + case .set(let element): + return "SetBridge<\(try bridgeTypeName(for: element))>" + case .bool, .int, .uint, .int8, .uint8, .int16, .uint16, .int32, .uint32, .int64, .uint64, .float, .double, .string: + return "JavaBoxableBridge<\(swiftType)>" + default: + throw JavaTranslationError.unsupportedSwiftType(swiftType) + } + } + + if nominalType.isSwiftJavaWrapper { + return "JavaObjectBridge<\(swiftType)>" + } + + let bridgeName = JNICaching.bridgeName(for: nominalType) + if nominalType.genericArguments.isEmpty { + return bridgeName + } else { + return "\(bridgeName)<\(nominalType.genericArguments.map(\.description).joined(separator: ", "))>" + } + + case .genericParameter: + throw JavaTranslationError.unsupportedSwiftType(swiftType) + + default: + throw JavaTranslationError.unsupportedSwiftType(swiftType) + } + } } struct NativeFunctionSignature { @@ -1177,6 +1246,11 @@ extension JNISwift2JavaGenerator { outArgumentName: String ) + indirect case constructor( + _ swiftType: SwiftType, + arguments: [(String?, NativeSwiftConversionStep)] = [] + ) + indirect case method( NativeSwiftConversionStep, function: String, @@ -1584,6 +1658,18 @@ extension JNISwift2JavaGenerator { } return "" + case .constructor(let swiftType, let arguments): + let args = arguments.map { name, value in + let value = value.render(&printer, placeholder) + if let name { + return "\(name): \(value)" + } else { + return value + } + } + let argsStr = args.joined(separator: ", ") + return "\(swiftType)(\(argsStr))" + case .method(let inner, let methodName, let arguments): let inner = inner.render(&printer, placeholder) let args = arguments.map { name, value in diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift index 9cae321d2..43391270c 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift @@ -344,6 +344,14 @@ extension JNISwift2JavaGenerator { printer.println() } + let isNeverLike = type.swiftNominal.kind == .enum && type.cases.isEmpty // Never types cannot be values, so ignore them + if !type.isSpecialization && !isNeverLike { + printJNICache(&printer, type) + printer.println() + printNominalJavaBridge(&printer, type) + printer.println() + } + printSpecificTypeThunks(&printer, type) printTypeMetadataAddressThunk(&printer, type) printer.println() @@ -761,6 +769,82 @@ extension JNISwift2JavaGenerator { } } + private func printJNICache(_ printer: inout CodePrinter, _ type: ImportedNominalType) { + let cacheName = JNICaching.cacheName(for: type) + let jniClassName = "\(javaPackagePath)/\(type.effectiveJavaTypeName.jniEscapedName)" + let isEffectivelyGeneric = type.swiftNominal.isGeneric && type.effectiveJavaTypeName == type.swiftNominal.qualifiedTypeName + let signature = + if isEffectivelyGeneric { + "(JJLorg/swift/swiftkit/core/SwiftArena;)L\(jniClassName);" + } else { + "(JLorg/swift/swiftkit/core/SwiftArena;)L\(jniClassName);" + } + + printer.printBraceBlock("private enum \(cacheName)") { printer in + printer.print( + """ + private static let wrapMemoryAddressUnsafeMethod = _JNIMethodIDCache.Method( + name: "wrapMemoryAddressUnsafe", + signature: "\(signature)", + isStatic: true + ) + + private static let cache = _JNIMethodIDCache( + className: "\(jniClassName)", + methods: [wrapMemoryAddressUnsafeMethod] + ) + + static var javaClass: jclass { + cache.javaClass + } + + static var wrapMemoryAddressUnsafe: jmethodID { + cache[wrapMemoryAddressUnsafeMethod]! + } + """ + ) + } + } + + private func printNominalJavaBridge(_ printer: inout CodePrinter, _ type: ImportedNominalType) { + let bridgeName = JNICaching.bridgeName(for: type) + let cacheName = JNICaching.cacheName(for: type) + let isEffectivelyGeneric = type.swiftNominal.isGeneric && !type.isSpecialization + let bridgeGenericClause = + if type.swiftNominal.genericParameters.isEmpty { + "" + } else { + "<\(type.swiftNominal.genericParameters.map { $0.syntax.trimmedDescription }.joined(separator: " "))>" + } + let bridgeWhereClause = type.swiftNominal.genericWhereClause?.trimmedDescription + let bridgedSwiftType = + if type.genericParameterNames.isEmpty { + type.effectiveSwiftTypeName + } else { + "\(type.baseTypeName)<\(type.genericParameterNames.joined(separator: ", "))>" + } + let parentProtocol = isEffectivelyGeneric ? "JextractedGenericTypeBridge" : "JextractedTypeBridge" + + let bridgeDeclaration = + if let bridgeWhereClause { + "enum \(bridgeName)\(bridgeGenericClause): \(parentProtocol) \(bridgeWhereClause)" + } else { + "enum \(bridgeName)\(bridgeGenericClause): \(parentProtocol)" + } + + printer.printBraceBlock(bridgeDeclaration) { printer in + printer.print("typealias SwiftType = \(bridgedSwiftType)") + printer.println() + printer.printBraceBlock("static var javaClass: jclass") { printer in + printer.print("\(cacheName).javaClass") + } + printer.println() + printer.printBraceBlock("static var wrapMemoryAddressUnsafe: jmethodID") { printer in + printer.print("\(cacheName).wrapMemoryAddressUnsafe") + } + } + } + private func printHeader(_ printer: inout CodePrinter) { // `public import` so the thunk file remains valid under // `InternalImportsByDefault` (SE-0409) diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftNominalTypeDeclaration.swift b/Sources/JExtractSwiftLib/SwiftTypes/SwiftNominalTypeDeclaration.swift index 6a9ab6cab..bbc5a97f4 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftNominalTypeDeclaration.swift +++ b/Sources/JExtractSwiftLib/SwiftTypes/SwiftNominalTypeDeclaration.swift @@ -121,6 +121,10 @@ package class SwiftNominalTypeDeclaration: SwiftTypeDeclaration { self.syntax.inheritanceClause?.inheritedTypes } + var genericWhereClause: GenericWhereClauseSyntax? { + self.syntax.asProtocol(WithGenericParametersSyntax.self)?.genericWhereClause + } + /// Returns true if this type conforms to `Sendable` and therefore is "threadsafe". private(set) lazy var isSendable: Bool = { // Check if Sendable is in the inheritance list diff --git a/Sources/JavaKit/Helpers/_JNIMethodIDCache.swift b/Sources/JavaKit/Helpers/_JNIMethodIDCache.swift index a54e547bb..266480786 100644 --- a/Sources/JavaKit/Helpers/_JNIMethodIDCache.swift +++ b/Sources/JavaKit/Helpers/_JNIMethodIDCache.swift @@ -17,7 +17,7 @@ /// This type is used internally in by the outputted JExtract wrappers /// to improve performance of any JNI lookups. public final class _JNIMethodIDCache: Sendable { - public struct Method: Hashable { + public struct Method: Hashable, Sendable { public let name: String public let signature: String diff --git a/Sources/SwiftJava/BridgedValues/JavaBoxing.swift b/Sources/SwiftJava/BridgedValues/JavaBoxing.swift index 5d37a778a..f26d3b5a5 100644 --- a/Sources/SwiftJava/BridgedValues/JavaBoxing.swift +++ b/Sources/SwiftJava/BridgedValues/JavaBoxing.swift @@ -329,10 +329,13 @@ class AnySwiftDictionaryBox { } /// Generic subclass that wraps a concrete `[K: V]` Swift dictionary. -final class SwiftDictionaryBox: AnySwiftDictionaryBox { - let dictionary: [K: V] +final class SwiftDictionaryBox: AnySwiftDictionaryBox +where KeyBridge.SwiftType: Hashable { + typealias Key = KeyBridge.SwiftType + typealias Value = ValueBridge.SwiftType + let dictionary: [Key: Value] - init(_ dictionary: [K: V]) { + init(_ dictionary: [Key: Value]) { self.dictionary = dictionary } @@ -345,19 +348,19 @@ final class SwiftDictionaryBox: AnySw } override func get(key: jobject?, environment: JNIEnvironment) -> jobject? { - guard environment.interface.IsInstanceOf(environment, key, K.javaBoxClass) == JNI_TRUE else { + guard KeyBridge.isJavaObject(key, in: environment) else { return nil } - let swiftKey = K.fromJavaObject(key, in: environment) + let swiftKey = KeyBridge.fromJavaObject(key, in: environment) guard let value = dictionary[swiftKey] else { return nil } - return value.toJavaObject(in: environment) + return ValueBridge.toJavaObject(value, in: environment) } override func containsKey(key: jobject?, environment: JNIEnvironment) -> Bool { - guard environment.interface.IsInstanceOf(environment, key, K.javaBoxClass) == JNI_TRUE else { + guard KeyBridge.isJavaObject(key, in: environment) else { return false } - let swiftKey = K.fromJavaObject(key, in: environment) + let swiftKey = KeyBridge.fromJavaObject(key, in: environment) return dictionary[swiftKey] != nil } @@ -366,7 +369,7 @@ final class SwiftDictionaryBox: AnySw let objectClass = environment.interface.FindClass(environment, "java/lang/Object") let result = environment.interface.NewObjectArray(environment, jsize(keysArray.count), objectClass, nil) for (i, key) in keysArray.enumerated() { - let javaKey = key.toJavaObject(in: environment) + let javaKey = KeyBridge.toJavaObject(key, in: environment) environment.interface.SetObjectArrayElement(environment, result, jsize(i), javaKey) } return result @@ -377,7 +380,7 @@ final class SwiftDictionaryBox: AnySw let objectClass = environment.interface.FindClass(environment, "java/lang/Object") let result = environment.interface.NewObjectArray(environment, jsize(valuesArray.count), objectClass, nil) for (i, value) in valuesArray.enumerated() { - let javaValue = value.toJavaObject(in: environment) + let javaValue = ValueBridge.toJavaObject(value, in: environment) environment.interface.SetObjectArrayElement(environment, result, jsize(i), javaValue) } return result @@ -400,10 +403,11 @@ class AnySwiftSetBox { } /// Generic subclass that wraps a concrete `Set` Swift set. -final class SwiftSetBox: AnySwiftSetBox { - let set: Set +final class SwiftSetBox: AnySwiftSetBox where ElementBridge.SwiftType: Hashable { + typealias Element = ElementBridge.SwiftType + let set: Set - init(_ set: Set) { + init(_ set: Set) { self.set = set } @@ -416,10 +420,10 @@ final class SwiftSetBox: AnySwiftSetBox { } override func contains(element: jobject?, environment: JNIEnvironment) -> Bool { - guard environment.interface.IsInstanceOf(environment, element, E.javaBoxClass) == JNI_TRUE else { + guard ElementBridge.isJavaObject(element, in: environment) else { return false } - let swiftElement = E.fromJavaObject(element, in: environment) + let swiftElement = ElementBridge.fromJavaObject(element, in: environment) return set.contains(swiftElement) } @@ -428,7 +432,7 @@ final class SwiftSetBox: AnySwiftSetBox { let objectClass = environment.interface.FindClass(environment, "java/lang/Object") let result = environment.interface.NewObjectArray(environment, jsize(elements.count), objectClass, nil) for (i, element) in elements.enumerated() { - let javaElement = element.toJavaObject(in: environment) + let javaElement = ElementBridge.toJavaObject(element, in: environment) environment.interface.SetObjectArrayElement(environment, result, jsize(i), javaElement) } return result diff --git a/Sources/SwiftJava/BridgedValues/JavaValue+Dictionary.swift b/Sources/SwiftJava/BridgedValues/JavaValue+Dictionary.swift index e9a80fac1..5ac2028d1 100644 --- a/Sources/SwiftJava/BridgedValues/JavaValue+Dictionary.swift +++ b/Sources/SwiftJava/BridgedValues/JavaValue+Dictionary.swift @@ -17,20 +17,29 @@ import SwiftJavaJNICore // ==== ----------------------------------------------------------------------- // MARK: Dictionary extension for JNI bridging -extension Dictionary where Key: JavaBoxable & Hashable, Value: JavaBoxable { +extension Dictionary { /// Box this dictionary and return a jlong pointer for passing across JNI. /// The dictionary is retained on the Swift heap; Java holds the pointer. - public func dictionaryGetJNIValue(in environment: JNIEnvironment) -> jlong { - let box = SwiftDictionaryBox(self) + public func dictionaryGetJNIValue( + in environment: JNIEnvironment, + keyBridge: KeyBridge.Type, + valueBridge: ValueBridge.Type + ) -> jlong where KeyBridge.SwiftType == Key, ValueBridge.SwiftType == Value { + let box = SwiftDictionaryBox(self) let unmanaged = Unmanaged.passRetained(box) let rawPointer = unmanaged.toOpaque() return jlong(Int(bitPattern: rawPointer)) } /// Reconstruct a Swift dictionary from a JNI jlong pointer to a SwiftDictionaryBox. - public init(fromJNI value: jlong, in environment: JNIEnvironment) { + public init( + fromJNI value: jlong, + in environment: JNIEnvironment, + keyBridge: KeyBridge.Type, + valueBridge: ValueBridge.Type + ) where KeyBridge.SwiftType == Key, ValueBridge.SwiftType == Value { let rawPointer = UnsafeRawPointer(bitPattern: Int(value))! - let box = Unmanaged>.fromOpaque(rawPointer).takeUnretainedValue() + let box = Unmanaged>.fromOpaque(rawPointer).takeUnretainedValue() self = box.dictionary } } diff --git a/Sources/SwiftJava/BridgedValues/JavaValue+Set.swift b/Sources/SwiftJava/BridgedValues/JavaValue+Set.swift index 2adb7d381..6b59d8d55 100644 --- a/Sources/SwiftJava/BridgedValues/JavaValue+Set.swift +++ b/Sources/SwiftJava/BridgedValues/JavaValue+Set.swift @@ -17,20 +17,27 @@ import SwiftJavaJNICore // ==== ----------------------------------------------------------------------- // MARK: Set extension for JNI bridging -extension Set where Element: JavaBoxable & Hashable { +extension Set { /// Box this set and return a jlong pointer for passing across JNI. /// The set is retained on the Swift heap; Java holds the pointer. - public func setGetJNIValue(in environment: JNIEnvironment) -> jlong { - let box = SwiftSetBox(self) + public func setGetJNIValue( + in environment: JNIEnvironment, + elementBridge: ElementBridge.Type + ) -> jlong where ElementBridge.SwiftType == Element { + let box = SwiftSetBox(self) let unmanaged = Unmanaged.passRetained(box) let rawPointer = unmanaged.toOpaque() return jlong(Int(bitPattern: rawPointer)) } /// Reconstruct a Swift set from a JNI jlong pointer to a SwiftSetBox. - public init(fromJNI value: jlong, in environment: JNIEnvironment) { + public init( + fromJNI value: jlong, + in environment: JNIEnvironment, + elementBridge: ElementBridge.Type + ) where ElementBridge.SwiftType == Element { let rawPointer = UnsafeRawPointer(bitPattern: Int(value))! - let box = Unmanaged>.fromOpaque(rawPointer).takeUnretainedValue() + let box = Unmanaged>.fromOpaque(rawPointer).takeUnretainedValue() self = box.set } } diff --git a/Sources/SwiftJava/BridgedValues/JobjectBridge.swift b/Sources/SwiftJava/BridgedValues/JobjectBridge.swift new file mode 100644 index 000000000..df0e5f18b --- /dev/null +++ b/Sources/SwiftJava/BridgedValues/JobjectBridge.swift @@ -0,0 +1,86 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +/// A strategy object that knows how to bridge a Swift type to and from Java. +/// +/// Unlike `JavaBoxable`, this is not attached to the bridged nominal type itself, +/// which avoids protocol-conformance conflicts for inheritable classes. +public protocol JobjectBridge { + associatedtype SwiftType + + /// Convert a Swift value to a Java object. + static func toJavaObject(_ value: SwiftType, in environment: JNIEnvironment) -> jobject? + + /// Convert a Java object back to a Swift value. + static func fromJavaObject(_ obj: jobject?, in environment: JNIEnvironment) -> SwiftType + + static func withJNIClass( + in environment: JNIEnvironment, + _ body: (jclass) throws -> Result + ) throws -> Result +} + +extension JobjectBridge { + public static func isJavaObject(_ obj: jobject?, in environment: JNIEnvironment) -> Bool { + guard let obj else { return false } + do { + return try withJNIClass(in: environment) { cls in + environment.interface.IsInstanceOf(environment, obj, cls) == JNI_TRUE + } + } catch { + return false + } + } +} + +public enum JavaBoxableBridge: JobjectBridge { + public typealias SwiftType = T + + public static func toJavaObject(_ value: T, in environment: JNIEnvironment) -> jobject? { + value.toJavaObject(in: environment) + } + + public static func fromJavaObject(_ obj: jobject?, in environment: JNIEnvironment) -> T { + T.fromJavaObject(obj, in: environment) + } + + public static func withJNIClass( + in environment: JNIEnvironment, + _ body: (jclass) throws -> Result + ) throws -> Result { + try body(T.javaBoxClass) + } +} + +public enum JavaObjectBridge: JobjectBridge { + public typealias SwiftType = T + + public static func toJavaObject(_ value: T, in environment: JNIEnvironment) -> jobject? { + value.javaThis + } + + public static func fromJavaObject(_ obj: jobject?, in environment: JNIEnvironment) -> T { + guard let obj else { + fatalError("\(T.self).fromJavaObject received a null Java object") + } + return T(javaThis: obj, environment: environment) + } + + public static func withJNIClass( + in environment: JNIEnvironment, + _ body: (jclass) throws -> Result + ) throws -> Result { + try T.withJNIClass(in: environment, body) + } +} diff --git a/Sources/SwiftJavaRuntimeSupport/CollectionBridge.swift b/Sources/SwiftJavaRuntimeSupport/CollectionBridge.swift new file mode 100644 index 000000000..baad3cd07 --- /dev/null +++ b/Sources/SwiftJavaRuntimeSupport/CollectionBridge.swift @@ -0,0 +1,210 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +import SwiftJava + +public enum DictionaryBridge: JobjectBridge where KeyBridge.SwiftType: Hashable { + public typealias SwiftType = [KeyBridge.SwiftType: ValueBridge.SwiftType] + + public static func toJavaObject(_ value: SwiftType, in environment: JNIEnvironment) -> jobject? { + let selfPointer = value.dictionaryGetJNIValue(in: environment, keyBridge: KeyBridge.self, valueBridge: ValueBridge.self) + var args = [jvalue(), jvalue()] + args[0].j = selfPointer + args[1].l = JavaSwiftArena.defaultAutoArena.javaThis + return environment.interface.CallStaticObjectMethodA( + environment, + _JNIMethodIDCache.SwiftDictionaryMap.class, + _JNIMethodIDCache.SwiftDictionaryMap.wrapMemoryAddressUnsafe, + &args + ) + } + + public static func fromJavaObject(_ obj: jobject?, in environment: JNIEnvironment) -> SwiftType { + guard let obj else { + fatalError("Dictionary.fromJavaObject received a null Java object") + } + let selfPointer = environment.interface.CallLongMethodA( + environment, + obj, + _JNIMethodIDCache.JNISwiftInstance.memoryAddress, + nil + ) + return SwiftType(fromJNI: selfPointer, in: environment, keyBridge: KeyBridge.self, valueBridge: ValueBridge.self) + } + + public static func withJNIClass( + in environment: JNIEnvironment, + _ body: (jclass) throws -> Result + ) throws -> Result { + try body(_JNIMethodIDCache.SwiftDictionaryMap.class) + } +} + +public enum SetBridge: JobjectBridge where ElementBridge.SwiftType: Hashable { + public typealias SwiftType = Set + + public static func toJavaObject(_ value: SwiftType, in environment: JNIEnvironment) -> jobject? { + let selfPointer = value.setGetJNIValue(in: environment, elementBridge: ElementBridge.self) + var args = [jvalue(), jvalue()] + args[0].j = selfPointer + args[1].l = JavaSwiftArena.defaultAutoArena.javaThis + return environment.interface.CallStaticObjectMethodA( + environment, + _JNIMethodIDCache.SwiftSet.class, + _JNIMethodIDCache.SwiftSet.wrapMemoryAddressUnsafe, + &args + ) + } + + public static func fromJavaObject(_ obj: jobject?, in environment: JNIEnvironment) -> SwiftType { + guard let obj else { + fatalError("Set.fromJavaObject received a null Java object") + } + let selfPointer = environment.interface.CallLongMethodA( + environment, + obj, + _JNIMethodIDCache.JNISwiftInstance.memoryAddress, + nil + ) + return SwiftType(fromJNI: selfPointer, in: environment, elementBridge: ElementBridge.self) + } + + public static func withJNIClass( + in environment: JNIEnvironment, + _ body: (jclass) throws -> Result + ) throws -> Result { + try body(_JNIMethodIDCache.SwiftSet.class) + } +} + +public enum OptionalBridge: JobjectBridge { + public typealias SwiftType = WrappedBridge.SwiftType? + + public static func toJavaObject(_ value: SwiftType, in environment: JNIEnvironment) -> jobject? { + if let value { + var args = [jvalue()] + args[0].l = WrappedBridge.toJavaObject(value, in: environment) + return environment.interface.CallStaticObjectMethodA( + environment, + _JNIMethodIDCache.JavaOptional.class, + _JNIMethodIDCache.JavaOptional.of, + &args + ) + } else { + return environment.interface.CallStaticObjectMethodA( + environment, + _JNIMethodIDCache.JavaOptional.class, + _JNIMethodIDCache.JavaOptional.empty, + nil + ) + } + } + + public static func fromJavaObject(_ obj: jobject?, in environment: JNIEnvironment) -> SwiftType { + guard let obj else { + fatalError("Optional.fromJavaObject received a null Java object") + } + + let isPresent = environment.interface.CallBooleanMethodA( + environment, + obj, + _JNIMethodIDCache.JavaOptional.isPresent, + nil + ) + guard isPresent == JNI_TRUE else { + return nil + } + + let wrapped = environment.interface.CallObjectMethodA( + environment, + obj, + _JNIMethodIDCache.JavaOptional.get, + nil + ) + return WrappedBridge.fromJavaObject(wrapped, in: environment) + } + + public static func withJNIClass( + in environment: JNIEnvironment, + _ body: (jclass) throws -> Result + ) throws -> Result { + try body(_JNIMethodIDCache.JavaOptional.class) + } +} + +public enum ArrayBridge: JobjectBridge { + public typealias SwiftType = [ElementBridge.SwiftType] + + public static func toJavaObject(_ value: SwiftType, in environment: JNIEnvironment) -> jobject? { + try! ElementBridge.withJNIClass(in: environment) { elementClass in + guard + let array = environment.interface.NewObjectArray( + environment, + jsize(value.count), + elementClass, + nil + ) + else { + fatalError("Array.toJavaObject failed to allocate a Java array") + } + + for (i, element) in value.enumerated() { + let javaElement = ElementBridge.toJavaObject(element, in: environment) + environment.interface.SetObjectArrayElement(environment, array, jsize(i), javaElement) + } + return array + } + } + + public static func fromJavaObject(_ obj: jobject?, in environment: JNIEnvironment) -> SwiftType { + guard let obj else { + fatalError("Array.fromJavaObject received a null Java object") + } + + let array = unsafeBitCast(obj, to: jobjectArray?.self) + let count = Int(environment.interface.GetArrayLength(environment, array)) + var result: SwiftType = [] + result.reserveCapacity(count) + + for i in 0..( + in environment: JNIEnvironment, + _ body: (jclass) throws -> Result + ) throws -> Result { + try ElementBridge.withJNIClass(in: environment) { elementClass in + guard let array = environment.interface.NewObjectArray(environment, 0, elementClass, nil) else { + fatalError("Array.withJNIClass failed to allocate a Java array") + } + defer { + environment.interface.DeleteLocalRef(environment, array) + } + + guard let arrayClass = environment.interface.GetObjectClass(environment, array) else { + fatalError("Array.withJNIClass could not load the Java array class") + } + defer { + environment.interface.DeleteLocalRef(environment, arrayClass) + } + + return try body(arrayClass) + } + } +} diff --git a/Sources/SwiftJavaRuntimeSupport/JNIMethodIDCaches.swift b/Sources/SwiftJavaRuntimeSupport/JNIMethodIDCaches.swift index 7c851e8f6..b938def03 100644 --- a/Sources/SwiftJavaRuntimeSupport/JNIMethodIDCaches.swift +++ b/Sources/SwiftJavaRuntimeSupport/JNIMethodIDCaches.swift @@ -121,6 +121,97 @@ extension _JNIMethodIDCache { } } + public enum SwiftDictionaryMap { + private static let wrapMemoryAddressUnsafeMethod = Method( + name: "wrapMemoryAddressUnsafe", + signature: "(JLorg/swift/swiftkit/core/SwiftArena;)Lorg/swift/swiftkit/core/collections/SwiftDictionaryMap;", + isStatic: true + ) + + private static let cache = _JNIMethodIDCache( + className: "org/swift/swiftkit/core/collections/SwiftDictionaryMap", + methods: [wrapMemoryAddressUnsafeMethod] + ) + + public static var `class`: jclass { + cache.javaClass + } + + public static var wrapMemoryAddressUnsafe: jmethodID { + cache.methods[wrapMemoryAddressUnsafeMethod]! + } + } + + public enum SwiftSet { + private static let wrapMemoryAddressUnsafeMethod = Method( + name: "wrapMemoryAddressUnsafe", + signature: "(JLorg/swift/swiftkit/core/SwiftArena;)Lorg/swift/swiftkit/core/collections/SwiftSet;", + isStatic: true + ) + + private static let cache = _JNIMethodIDCache( + className: "org/swift/swiftkit/core/collections/SwiftSet", + methods: [wrapMemoryAddressUnsafeMethod] + ) + + public static var `class`: jclass { + cache.javaClass + } + + public static var wrapMemoryAddressUnsafe: jmethodID { + cache.methods[wrapMemoryAddressUnsafeMethod]! + } + } + + public enum JavaOptional { + private static let emptyMethod = Method( + name: "empty", + signature: "()Ljava/util/Optional;", + isStatic: true + ) + + private static let ofMethod = Method( + name: "of", + signature: "(Ljava/lang/Object;)Ljava/util/Optional;", + isStatic: true + ) + + private static let isPresentMethod = Method( + name: "isPresent", + signature: "()Z" + ) + + private static let getMethod = Method( + name: "get", + signature: "()Ljava/lang/Object;" + ) + + private static let cache = _JNIMethodIDCache( + className: "java/util/Optional", + methods: [emptyMethod, ofMethod, isPresentMethod, getMethod] + ) + + public static var `class`: jclass { + cache.javaClass + } + + public static var empty: jmethodID { + cache.methods[emptyMethod]! + } + + public static var of: jmethodID { + cache.methods[ofMethod]! + } + + public static var isPresent: jmethodID { + cache.methods[isPresentMethod]! + } + + public static var get: jmethodID { + cache.methods[getMethod]! + } + } + public enum _OutSwiftGenericInstance { private static let selfPointerField = Field( name: "selfPointer", diff --git a/Sources/SwiftJavaRuntimeSupport/JextractedTypeBridge.swift b/Sources/SwiftJavaRuntimeSupport/JextractedTypeBridge.swift new file mode 100644 index 000000000..b9a41ebbd --- /dev/null +++ b/Sources/SwiftJavaRuntimeSupport/JextractedTypeBridge.swift @@ -0,0 +1,87 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +import SwiftJava + +/// A bridge for Swift types whose Java wrapper classes are generated by jextract. +/// +/// This protocol exists so individual generated bridges can stay concise and +/// avoid repeating the same boilerplate. +public protocol JextractedTypeBridge: JobjectBridge { + static var javaClass: jclass { get } + static var wrapMemoryAddressUnsafe: jmethodID { get } +} + +extension JextractedTypeBridge { + public static func toJavaObject(_ value: SwiftType, in environment: JNIEnvironment) -> jobject? { + let selfPointer$ = UnsafeMutablePointer.allocate(capacity: 1) + selfPointer$.initialize(to: value) + let selfPointerBits$ = Int64(Int(bitPattern: selfPointer$)) + var args = [jvalue(), jvalue()] + args[0].j = selfPointerBits$.getJNIValue(in: environment) + args[1].l = JavaSwiftArena.defaultAutoArena.javaThis + return environment.interface.CallStaticObjectMethodA( + environment, + Self.javaClass, + Self.wrapMemoryAddressUnsafe, + &args + ) + } + + public static func fromJavaObject(_ obj: jobject?, in environment: JNIEnvironment) -> SwiftType { + guard let obj else { + fatalError("\(Self.self).fromJavaObject received a null Java object") + } + let selfPointer$ = environment.interface.CallLongMethodA( + environment, + obj, + _JNIMethodIDCache.JNISwiftInstance.memoryAddress, + nil + ) + let selfPointerBits$ = Int(Int64(fromJNI: selfPointer$, in: environment)) + guard let valuePointer$ = UnsafeMutablePointer(bitPattern: selfPointerBits$) else { + fatalError("\(Self.self).fromJavaObject received a null Swift memory address") + } + return valuePointer$.pointee + } + + public static func withJNIClass( + in environment: JNIEnvironment, + _ body: (jclass) throws -> Result + ) throws -> Result { + try body(javaClass) + } +} + +public protocol JextractedGenericTypeBridge: JextractedTypeBridge {} + +extension JextractedGenericTypeBridge { + public static func toJavaObject(_ value: SwiftType, in environment: JNIEnvironment) -> jobject? { + let selfPointer$ = UnsafeMutablePointer.allocate(capacity: 1) + selfPointer$.initialize(to: value) + let selfPointerBits$ = Int64(Int(bitPattern: selfPointer$)) + let selfTypePointer$ = unsafeBitCast(SwiftType.self, to: UnsafeRawPointer.self) + let selfTypePointerBits$ = Int64(Int(bitPattern: selfTypePointer$)) + var args = [jvalue(), jvalue(), jvalue()] + args[0].j = selfPointerBits$.getJNIValue(in: environment) + args[1].j = selfTypePointerBits$.getJNIValue(in: environment) + args[2].l = JavaSwiftArena.defaultAutoArena.javaThis + return environment.interface.CallStaticObjectMethodA( + environment, + Self.javaClass, + Self.wrapMemoryAddressUnsafe, + &args + ) + } +} diff --git a/Sources/SwiftJavaRuntimeSupport/_JNIMethodIDCache.swift b/Sources/SwiftJavaRuntimeSupport/_JNIMethodIDCache.swift index 4ba4c5419..87eb661a7 100644 --- a/Sources/SwiftJavaRuntimeSupport/_JNIMethodIDCache.swift +++ b/Sources/SwiftJavaRuntimeSupport/_JNIMethodIDCache.swift @@ -20,7 +20,7 @@ import SwiftJavaJNICore /// This type is used internally in by the outputted JExtract wrappers /// to improve performance of any JNI lookups. public final class _JNIMethodIDCache: Sendable { - public struct Method: Hashable { + public struct Method: Hashable, Sendable { public let name: String public let signature: String public let isStatic: Bool @@ -32,7 +32,7 @@ public final class _JNIMethodIDCache: Sendable { } } - public struct Field: Hashable { + public struct Field: Hashable, Sendable { public let name: String public let signature: String public let isStatic: Bool diff --git a/Tests/JExtractSwiftTests/JNI/JNIDictionaryTest.swift b/Tests/JExtractSwiftTests/JNI/JNIDictionaryTest.swift index ebb8ed163..88ae2397a 100644 --- a/Tests/JExtractSwiftTests/JNI/JNIDictionaryTest.swift +++ b/Tests/JExtractSwiftTests/JNI/JNIDictionaryTest.swift @@ -49,7 +49,7 @@ struct JNIDictionaryTest { """ @_cdecl("Java_com_example_swift_SwiftModule__00024f__") public func Java_com_example_swift_SwiftModule__00024f__(environment: UnsafeMutablePointer!, thisClass: jclass) -> jlong { - return SwiftModule.f().dictionaryGetJNIValue(in: environment) + return SwiftModule.f().dictionaryGetJNIValue(in: environment, keyBridge: JavaBoxableBridge.self, valueBridge: JavaBoxableBridge.self) } """ ] @@ -87,7 +87,7 @@ struct JNIDictionaryTest { """ @_cdecl("Java_com_example_swift_SwiftModule__00024f__J") public func Java_com_example_swift_SwiftModule__00024f__J(environment: UnsafeMutablePointer!, thisClass: jclass, dict: jlong) { - SwiftModule.f(dict: [String: Int64](fromJNI: dict, in: environment)) + SwiftModule.f(dict: [String: Int64](fromJNI: dict, in: environment, keyBridge: JavaBoxableBridge.self, valueBridge: JavaBoxableBridge.self)) } """ ] @@ -125,7 +125,7 @@ struct JNIDictionaryTest { """ @_cdecl("Java_com_example_swift_SwiftModule__00024f__J") public func Java_com_example_swift_SwiftModule__00024f__J(environment: UnsafeMutablePointer!, thisClass: jclass, dict: jlong) -> jlong { - return SwiftModule.f(dict: [String: Int64](fromJNI: dict, in: environment)).dictionaryGetJNIValue(in: environment) + return SwiftModule.f(dict: [String: Int64](fromJNI: dict, in: environment, keyBridge: JavaBoxableBridge.self, valueBridge: JavaBoxableBridge.self)).dictionaryGetJNIValue(in: environment, keyBridge: JavaBoxableBridge.self, valueBridge: JavaBoxableBridge.self) } """ ] @@ -163,7 +163,7 @@ struct JNIDictionaryTest { """ @_cdecl("Java_com_example_swift_SwiftModule__00024f__J") public func Java_com_example_swift_SwiftModule__00024f__J(environment: UnsafeMutablePointer!, thisClass: jclass, dict: jlong) -> jlong { - return SwiftModule.f(dict: [String: Int64](fromJNI: dict, in: environment)).dictionaryGetJNIValue(in: environment) + return SwiftModule.f(dict: [String: Int64](fromJNI: dict, in: environment, keyBridge: JavaBoxableBridge.self, valueBridge: JavaBoxableBridge.self)).dictionaryGetJNIValue(in: environment, keyBridge: JavaBoxableBridge.self, valueBridge: JavaBoxableBridge.self) } """ ] @@ -275,7 +275,7 @@ struct JNIDictionaryTest { """ @_cdecl("Java_com_example_swift_SwiftModule__00024f__JJ") public func Java_com_example_swift_SwiftModule__00024f__JJ(environment: UnsafeMutablePointer!, thisClass: jclass, a: jlong, b: jlong) { - SwiftModule.f(a: [String: Int64](fromJNI: a, in: environment), b: [String: Bool](fromJNI: b, in: environment)) + SwiftModule.f(a: [String: Int64](fromJNI: a, in: environment, keyBridge: JavaBoxableBridge.self, valueBridge: JavaBoxableBridge.self), b: [String: Bool](fromJNI: b, in: environment, keyBridge: JavaBoxableBridge.self, valueBridge: JavaBoxableBridge.self)) } """ ] @@ -316,7 +316,7 @@ struct JNIDictionaryTest { """ @_cdecl("Java_com_example_swift_SwiftModule__00024f__JLjava_lang_String_2J") public func Java_com_example_swift_SwiftModule__00024f__JLjava_lang_String_2J(environment: UnsafeMutablePointer!, thisClass: jclass, dict: jlong, key: jstring?, value: jlong) -> jlong { - return SwiftModule.f(dict: [String: Int64](fromJNI: dict, in: environment), key: String(fromJNI: key, in: environment), value: Int64(fromJNI: value, in: environment)).dictionaryGetJNIValue(in: environment) + return SwiftModule.f(dict: [String: Int64](fromJNI: dict, in: environment, keyBridge: JavaBoxableBridge.self, valueBridge: JavaBoxableBridge.self), key: String(fromJNI: key, in: environment), value: Int64(fromJNI: value, in: environment)).dictionaryGetJNIValue(in: environment, keyBridge: JavaBoxableBridge.self, valueBridge: JavaBoxableBridge.self) } """ ] diff --git a/Tests/JExtractSwiftTests/JNI/JNIEnumTests.swift b/Tests/JExtractSwiftTests/JNI/JNIEnumTests.swift index ddb22bae8..3c4a41add 100644 --- a/Tests/JExtractSwiftTests/JNI/JNIEnumTests.swift +++ b/Tests/JExtractSwiftTests/JNI/JNIEnumTests.swift @@ -349,9 +349,6 @@ struct JNIEnumTests { detectChunkByInitialLines: 1, expectedChunks: [], notExpectedChunks: [ - """ - enum _JNI_MyEnum - """, """ public func Java_com_example_swift_MyEnum__00024getAsFirst__J(" """, diff --git a/Tests/JExtractSwiftTests/JNI/JNIJobjectBridgeTests.swift b/Tests/JExtractSwiftTests/JNI/JNIJobjectBridgeTests.swift new file mode 100644 index 000000000..9653503f4 --- /dev/null +++ b/Tests/JExtractSwiftTests/JNI/JNIJobjectBridgeTests.swift @@ -0,0 +1,98 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +import JExtractSwiftLib +import Testing + +@Suite +struct JNIJobjectBridgeTests { + @Test("JNI generates explicit bridges for dictionary element types") + func generatesBridgeDeclaration() throws { + try assertOutput( + input: """ + public struct ReefFish {} + public func f(dict: [Int: ReefFish]) -> [Int: ReefFish] {} + """, + .jni, + .swift, + detectChunkByInitialLines: 2, + expectedChunks: [ + """ + private enum _JNI_ReefFish { + private static let wrapMemoryAddressUnsafeMethod = _JNIMethodIDCache.Method( + name: "wrapMemoryAddressUnsafe", + signature: "(JLorg/swift/swiftkit/core/SwiftArena;)Lcom/example/swift/ReefFish;", + isStatic: true + ) + + private static let cache = _JNIMethodIDCache( + className: "com/example/swift/ReefFish", + methods: [wrapMemoryAddressUnsafeMethod] + ) + static var javaClass: jclass { + cache.javaClass + } + static var wrapMemoryAddressUnsafe: jmethodID { + cache[wrapMemoryAddressUnsafeMethod]! + } + """, + """ + enum _JNIBridge_ReefFish: JextractedTypeBridge { + typealias SwiftType = ReefFish + + static var javaClass: jclass { + _JNI_ReefFish.javaClass + } + + static var wrapMemoryAddressUnsafe: jmethodID { + _JNI_ReefFish.wrapMemoryAddressUnsafe + } + } + """, + """ + return SwiftModule.f(dict: [Int: ReefFish](fromJNI: dict, in: environment, keyBridge: JavaBoxableBridge.self, valueBridge: _JNIBridge_ReefFish.self)).dictionaryGetJNIValue(in: environment, keyBridge: JavaBoxableBridge.self, valueBridge: _JNIBridge_ReefFish.self) + """, + ] + ) + } + + + @Test("JNI generates explicit bridges for generic dictionary keys and values") + func generatesBridgeDeclarationForGenericType() throws { + try assertOutput( + input: """ + public struct MyID: Hashable {} + public struct MyValue where O : Swift.Comparable, E : Swift.Error {} + public enum Never: Error {} + public func f() -> [MyID: MyValue] {} + """, + .jni, + .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + enum _JNIBridge_MyID: JextractedGenericTypeBridge { + typealias SwiftType = MyID + """, + """ + enum _JNIBridge_MyValue: JextractedGenericTypeBridge where O : Swift.Comparable, E : Swift.Error { + typealias SwiftType = MyValue + """, + """ + return SwiftModule.f().dictionaryGetJNIValue(in: environment, keyBridge: _JNIBridge_MyID.self, valueBridge: _JNIBridge_MyValue.self) + """, + ] + ) + } +} diff --git a/Tests/JExtractSwiftTests/JNI/JNISetTest.swift b/Tests/JExtractSwiftTests/JNI/JNISetTest.swift index 8adec8d58..6a79c4c92 100644 --- a/Tests/JExtractSwiftTests/JNI/JNISetTest.swift +++ b/Tests/JExtractSwiftTests/JNI/JNISetTest.swift @@ -49,7 +49,7 @@ struct JNISetTest { """ @_cdecl("Java_com_example_swift_SwiftModule__00024f__") public func Java_com_example_swift_SwiftModule__00024f__(environment: UnsafeMutablePointer!, thisClass: jclass) -> jlong { - return SwiftModule.f().setGetJNIValue(in: environment) + return SwiftModule.f().setGetJNIValue(in: environment, elementBridge: JavaBoxableBridge.self) } """ ] @@ -87,7 +87,7 @@ struct JNISetTest { """ @_cdecl("Java_com_example_swift_SwiftModule__00024f__J") public func Java_com_example_swift_SwiftModule__00024f__J(environment: UnsafeMutablePointer!, thisClass: jclass, set: jlong) { - SwiftModule.f(set: Set(fromJNI: set, in: environment)) + SwiftModule.f(set: Set(fromJNI: set, in: environment, elementBridge: JavaBoxableBridge.self)) } """ ] @@ -125,7 +125,7 @@ struct JNISetTest { """ @_cdecl("Java_com_example_swift_SwiftModule__00024f__J") public func Java_com_example_swift_SwiftModule__00024f__J(environment: UnsafeMutablePointer!, thisClass: jclass, set: jlong) -> jlong { - return SwiftModule.f(set: Set(fromJNI: set, in: environment)).setGetJNIValue(in: environment) + return SwiftModule.f(set: Set(fromJNI: set, in: environment, elementBridge: JavaBoxableBridge.self)).setGetJNIValue(in: environment, elementBridge: JavaBoxableBridge.self) } """ ] @@ -220,7 +220,7 @@ struct JNISetTest { """ @_cdecl("Java_com_example_swift_SwiftModule__00024f__JJ") public func Java_com_example_swift_SwiftModule__00024f__JJ(environment: UnsafeMutablePointer!, thisClass: jclass, a: jlong, b: jlong) { - SwiftModule.f(a: Set(fromJNI: a, in: environment), b: Set(fromJNI: b, in: environment)) + SwiftModule.f(a: Set(fromJNI: a, in: environment, elementBridge: JavaBoxableBridge.self), b: Set(fromJNI: b, in: environment, elementBridge: JavaBoxableBridge.self)) } """ ] @@ -261,7 +261,7 @@ struct JNISetTest { """ @_cdecl("Java_com_example_swift_SwiftModule__00024f__JLjava_lang_String_2") public func Java_com_example_swift_SwiftModule__00024f__JLjava_lang_String_2(environment: UnsafeMutablePointer!, thisClass: jclass, set: jlong, element: jstring?) -> jlong { - return SwiftModule.f(set: Set(fromJNI: set, in: environment), element: String(fromJNI: element, in: environment)).setGetJNIValue(in: environment) + return SwiftModule.f(set: Set(fromJNI: set, in: environment, elementBridge: JavaBoxableBridge.self), element: String(fromJNI: element, in: environment)).setGetJNIValue(in: environment, elementBridge: JavaBoxableBridge.self) } """ ]