From 6e02a688c4759dded97f2b99896feaacec0fd972 Mon Sep 17 00:00:00 2001 From: Iceman Date: Thu, 7 May 2026 16:26:03 +0900 Subject: [PATCH 01/25] initial implementation --- .../MySwiftLibrary/CollectionBoxable.swift | 59 ++ .../example/swift/CollectionBoxableTest.java | 85 +++ ...ift2JavaGenerator+SwiftThunkPrinting.swift | 616 ++++++++++++++++++ .../JNI/JUICollectionBoxableTests.swift | 158 +++++ 4 files changed, 918 insertions(+) create mode 100644 Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/CollectionBoxable.swift create mode 100644 Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/CollectionBoxableTest.java create mode 100644 Tests/JExtractSwiftTests/JNI/JUICollectionBoxableTests.swift diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/CollectionBoxable.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/CollectionBoxable.swift new file mode 100644 index 000000000..88942319a --- /dev/null +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/CollectionBoxable.swift @@ -0,0 +1,59 @@ +//===----------------------------------------------------------------------===// +// +// 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 struct ReefFish: Hashable { + private let rawName: String + + public init(name: String) { + self.rawName = name + } + + public func getName() -> String { + rawName + } +} + +public func makeIntToFishDictionary() -> [Int: ReefFish] { + [ + 1: ReefFish(name: "salmon"), + 2: ReefFish(name: "clownfish"), + ] +} + +public func intToFishDictionary(dict: [Int: ReefFish]) -> [Int: ReefFish] { + dict +} + +public func insertIntoIntToFishDictionary(dict: [Int: ReefFish], key: Int, value: ReefFish) -> [Int: ReefFish] { + var copy = dict + copy[key] = value + return copy +} + +public func makeFishSet() -> Set { + [ + ReefFish(name: "salmon"), + ReefFish(name: "clownfish"), + ] +} + +public func fishSet(set: Set) -> Set { + set +} + +public func insertIntoFishSet(set: Set, fish: ReefFish) -> Set { + var copy = set + copy.insert(fish) + return copy +} 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..b1e5bc00c --- /dev/null +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/CollectionBoxableTest.java @@ -0,0 +1,85 @@ +//===----------------------------------------------------------------------===// +// +// 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.*; + +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 insertIntoIntToFishDictionary() { + try (var arena = SwiftArena.ofConfined()) { + SwiftDictionaryMap original = MySwiftLibrary.makeIntToFishDictionary(arena); + ReefFish tuna = ReefFish.init("tuna", arena); + + SwiftDictionaryMap modified = + MySwiftLibrary.insertIntoIntToFishDictionary(original, 3L, tuna, arena); + + assertEquals(3, modified.size()); + assertEquals("tuna", modified.get(3L).getName()); + assertEquals(2, original.size()); + assertNull(original.get(3L)); + } + } + + @Test + void fishSetRoundtrip() { + try (var arena = SwiftArena.ofConfined()) { + SwiftSet original = MySwiftLibrary.makeFishSet(arena); + assertEquals(2, original.size()); + assertTrue(original.contains(ReefFish.init("salmon", arena))); + assertTrue(original.contains(ReefFish.init("clownfish", arena))); + + SwiftSet roundtripped = MySwiftLibrary.fishSet(original, arena); + assertEquals(2, roundtripped.size()); + assertTrue(roundtripped.contains(ReefFish.init("salmon", arena))); + assertTrue(roundtripped.contains(ReefFish.init("clownfish", arena))); + } + } + + @Test + void insertIntoFishSet() { + try (var arena = SwiftArena.ofConfined()) { + SwiftSet original = MySwiftLibrary.makeFishSet(arena); + ReefFish tuna = ReefFish.init("tuna", arena); + + SwiftSet modified = MySwiftLibrary.insertIntoFishSet(original, tuna, arena); + + assertEquals(3, modified.size()); + assertTrue(modified.contains(tuna)); + assertEquals(2, original.size()); + assertFalse(original.contains(tuna)); + } + } +} diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift index 3ad676be2..d667932b5 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift @@ -303,6 +303,8 @@ extension JNISwift2JavaGenerator { printSwiftFunctionThunk(&printer, decl) printer.println() } + + printCollectionJavaBoxableExtensions(&printer) } private func printNominalTypeThunks(_ printer: inout CodePrinter, _ type: ImportedNominalType) throws { @@ -828,6 +830,620 @@ extension JNISwift2JavaGenerator { } } + private struct ConcreteDictionaryJavaBoxable: Hashable { + let swiftTypeName: String + } + + private struct ConcreteSetJavaBoxable: Hashable { + let swiftTypeName: String + } + + private struct CollectionJavaBoxableTypes { + let nominalTypes: [ImportedNominalType] + let dictionaryTypes: [ConcreteDictionaryJavaBoxable] + let setTypes: [ConcreteSetJavaBoxable] + + var isEmpty: Bool { + nominalTypes.isEmpty && dictionaryTypes.isEmpty && setTypes.isEmpty + } + } + + private func printCollectionJavaBoxableExtensions(_ printer: inout CodePrinter) { + let boxableTypes = collectCollectionJavaBoxableTypes() + guard !boxableTypes.isEmpty else { + return + } + + printer.printSeparator("JavaBoxable conformances for Dictionary/Set element types") + + if !boxableTypes.dictionaryTypes.isEmpty || !boxableTypes.setTypes.isEmpty { + printCollectionJavaBoxableCaches(&printer, boxableTypes) + printer.println() + } + + for nominalType in boxableTypes.nominalTypes { + printNominalJavaBoxableCache(&printer, nominalType) + printer.println() + printNominalJavaBoxableExtension(&printer, nominalType) + printer.println() + } + + for dictionaryType in boxableTypes.dictionaryTypes { + printConcreteDictionaryJavaBoxableExtension(&printer, dictionaryType) + printer.println() + } + + for setType in boxableTypes.setTypes { + printConcreteSetJavaBoxableExtension(&printer, setType) + printer.println() + } + } + + private func collectCollectionJavaBoxableTypes() -> CollectionJavaBoxableTypes { + var nominalTypes: Set = [] + var dictionaryTypes: Set = [] + var setTypes: Set = [] + + func collect(_ type: SwiftType, insideCollectionElement: Bool = false) { + switch type { + case .nominal(let nominalType): + if let knownType = nominalType.asKnownType { + switch knownType { + case .dictionary(let keyType, let valueType): + if insideCollectionElement { + dictionaryTypes.insert(.init(swiftTypeName: type.description)) + } + collect(keyType, insideCollectionElement: true) + collect(valueType, insideCollectionElement: true) + return + + case .set(let elementType): + if insideCollectionElement { + setTypes.insert(.init(swiftTypeName: type.description)) + } + collect(elementType, insideCollectionElement: true) + return + + case .optional(let wrappedType): + collect(wrappedType, insideCollectionElement: insideCollectionElement) + return + + case .array(let elementType): + collect(elementType, insideCollectionElement: false) + return + + default: + break + } + } + + guard insideCollectionElement else { + return + } + guard !nominalType.isSwiftJavaWrapper else { + return + } + guard nominalType.nominalTypeDecl.knownTypeKind == nil else { + return + } + guard let importedType = analysis.importedTypes[nominalType.nominalTypeDecl.qualifiedName] else { + return + } + guard !importedType.swiftNominal.isGeneric else { + return + } + guard !importedType.isSpecialization else { + return + } + nominalTypes.insert(importedType) + + case .tuple(let elements): + for element in elements { + collect(element.type, insideCollectionElement: false) + } + + case .existential(let innerType), .opaque(let innerType), .metatype(let innerType): + collect(innerType, insideCollectionElement: false) + + case .function(let functionType): + for parameter in functionType.parameters { + collect(parameter.type, insideCollectionElement: false) + } + collect(functionType.resultType, insideCollectionElement: false) + + case .composite(let innerTypes): + for innerType in innerTypes { + collect(innerType, insideCollectionElement: false) + } + + case .genericParameter: + break + } + } + + for decl in analysis.importedGlobalFuncs { + for parameter in decl.functionSignature.parameters { + collect(parameter.type) + } + collect(decl.functionSignature.result.type) + } + + for decl in analysis.importedGlobalVariables { + for parameter in decl.functionSignature.parameters { + collect(parameter.type) + } + collect(decl.functionSignature.result.type) + } + + for importedType in analysis.importedTypes.values { + for decl in importedType.initializers + importedType.methods + importedType.variables { + for parameter in decl.functionSignature.parameters { + collect(parameter.type) + } + collect(decl.functionSignature.result.type) + } + } + + return CollectionJavaBoxableTypes( + nominalTypes: nominalTypes.sorted { $0.effectiveSwiftTypeName < $1.effectiveSwiftTypeName }, + dictionaryTypes: dictionaryTypes.sorted { $0.swiftTypeName < $1.swiftTypeName }, + setTypes: setTypes.sorted { $0.swiftTypeName < $1.swiftTypeName } + ) + } + + private func printCollectionJavaBoxableCaches( + _ printer: inout CodePrinter, + _ boxableTypes: CollectionJavaBoxableTypes + ) { + printer.printBraceBlock("private enum _SwiftJavaCollectionJavaBoxingCache") { printer in + if !boxableTypes.dictionaryTypes.isEmpty { + printer.print( + """ + private static let swiftDictionaryMapWrapMemoryAddressUnsafeMethod = _JNIMethodIDCache.Method( + name: "wrapMemoryAddressUnsafe", + signature: "(JLorg/swift/swiftkit/core/SwiftArena;)Lorg/swift/swiftkit/core/collections/SwiftDictionaryMap;", + isStatic: true + ) + private static let swiftDictionaryMapCache = _JNIMethodIDCache( + className: "org/swift/swiftkit/core/collections/SwiftDictionaryMap", + methods: [swiftDictionaryMapWrapMemoryAddressUnsafeMethod] + ) + + static var swiftDictionaryMapClass: jclass { + swiftDictionaryMapCache.javaClass + } + + static var swiftDictionaryMapWrapMemoryAddressUnsafe: jmethodID { + swiftDictionaryMapCache[swiftDictionaryMapWrapMemoryAddressUnsafeMethod]! + } + """ + ) + } + + if !boxableTypes.dictionaryTypes.isEmpty && !boxableTypes.setTypes.isEmpty { + printer.println() + } + + if !boxableTypes.setTypes.isEmpty { + printer.print( + """ + private static let swiftSetWrapMemoryAddressUnsafeMethod = _JNIMethodIDCache.Method( + name: "wrapMemoryAddressUnsafe", + signature: "(JLorg/swift/swiftkit/core/SwiftArena;)Lorg/swift/swiftkit/core/collections/SwiftSet;", + isStatic: true + ) + private static let swiftSetCache = _JNIMethodIDCache( + className: "org/swift/swiftkit/core/collections/SwiftSet", + methods: [swiftSetWrapMemoryAddressUnsafeMethod] + ) + + static var swiftSetClass: jclass { + swiftSetCache.javaClass + } + + static var swiftSetWrapMemoryAddressUnsafe: jmethodID { + swiftSetCache[swiftSetWrapMemoryAddressUnsafeMethod]! + } + """ + ) + } + } + } + + private func printNominalJavaBoxableCache(_ printer: inout CodePrinter, _ type: ImportedNominalType) { + let cacheName = "_SwiftJavaBoxing_\(type.effectiveJavaTypeName.fullFlatName)" + let jniClassName = "\(javaPackagePath)/\(type.effectiveJavaTypeName.jniEscapedName)" + let signature = "(J)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 printNominalJavaBoxableExtension(_ printer: inout CodePrinter, _ type: ImportedNominalType) { + let swiftTypeName = type.effectiveSwiftTypeName + let cacheName = "_SwiftJavaBoxing_\(type.effectiveJavaTypeName.fullFlatName)" + let javaClassName = "\(javaPackage).\(type.effectiveJavaTypeName.jniEscapedName)" + + printer.printBraceBlock("extension \(swiftTypeName): JavaValue, JavaBoxable") { printer in + printer.print("public typealias JNIType = jobject?") + printer.print("public static var jvalueKeyPath: WritableKeyPath { \\.l }") + printer.print(#"public static var javaType: JavaType { JavaType(className: "\#(javaClassName)") }"#) + printer.println() + printer.printBraceBlock( + "public func getJNIValue(in environment: JNIEnvironment) -> JNIType" + ) { printer in + printer.print("toJavaObject(in: environment)") + } + printer.println() + printer.printBraceBlock("public func toJavaObject(in environment: JNIEnvironment) -> jobject?") { printer in + printer.print( + """ + let selfPointer$ = UnsafeMutablePointer<\(swiftTypeName)>.allocate(capacity: 1) + selfPointer$.initialize(to: self) + let selfPointerBits$ = Int64(Int(bitPattern: selfPointer$)) + var args = [jvalue()] + args[0].j = selfPointerBits$.getJNIValue(in: environment) + return environment.interface.CallStaticObjectMethodA( + environment, + \(cacheName).javaClass, + \(cacheName).wrapMemoryAddressUnsafe, + &args + ) + """ + ) + } + printer.println() + printer.printBraceBlock( + "public init(fromJNI value: JNIType, in environment: JNIEnvironment)" + ) { printer in + printer.print("self = Self.fromJavaObject(value, in: environment)") + } + printer.println() + printer.printBraceBlock("public static func fromJavaObject(_ obj: jobject?, in environment: JNIEnvironment) -> \(swiftTypeName)") { printer in + printer.print( + """ + guard let obj else { + fatalError("\(swiftTypeName).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<\(swiftTypeName)>(bitPattern: selfPointerBits$) else { + fatalError("\(swiftTypeName).fromJavaObject received a null Swift memory address") + } + return valuePointer$.pointee + """ + ) + } + printer.println() + printer.printBraceBlock( + "public static func jniMethodCall(in environment: JNIEnvironment) -> JNIMethodCall" + ) { printer in + printer.print("environment.interface.CallObjectMethodA") + } + printer.println() + printer.printBraceBlock( + "public static func jniFieldGet(in environment: JNIEnvironment) -> JNIFieldGet" + ) { printer in + printer.print("environment.interface.GetObjectField") + } + printer.println() + printer.printBraceBlock( + "public static func jniFieldSet(in environment: JNIEnvironment) -> JNIFieldSet" + ) { printer in + printer.print("environment.interface.SetObjectField") + } + printer.println() + printer.printBraceBlock( + "public static func jniStaticMethodCall(in environment: JNIEnvironment) -> JNIStaticMethodCall" + ) { printer in + printer.print("environment.interface.CallStaticObjectMethodA") + } + printer.println() + printer.printBraceBlock( + "public static func jniStaticFieldGet(in environment: JNIEnvironment) -> JNIStaticFieldGet" + ) { printer in + printer.print("environment.interface.GetStaticObjectField") + } + printer.println() + printer.printBraceBlock( + "public static func jniStaticFieldSet(in environment: JNIEnvironment) -> JNIStaticFieldSet" + ) { printer in + printer.print("environment.interface.SetStaticObjectField") + } + printer.println() + printer.printBraceBlock( + "public static func jniNewArray(in environment: JNIEnvironment) -> JNINewArray" + ) { printer in + printer.print("{ environment, size in environment.interface.NewObjectArray(environment, size, \(cacheName).javaClass, nil) }") + } + printer.println() + printer.printBraceBlock( + "public static func jniGetArrayRegion(in environment: JNIEnvironment) -> JNIGetArrayRegion" + ) { printer in + printer.print( + """ + { environment, array, start, length, outPointer in + let buffer = UnsafeMutableBufferPointer(start: outPointer, count: Int(length)) + for i in start.. JNISetArrayRegion" + ) { printer in + printer.print( + """ + { environment, array, start, length, outPointer in + let buffer = UnsafeBufferPointer(start: outPointer, count: Int(length)) + for i in start.. { \\.l }") + printer.print(#"public static var javaType: JavaType { JavaType(className: "org.swift.swiftkit.core.collections.SwiftDictionaryMap") }"#) + printer.println() + printer.printBraceBlock("public func getJNIValue(in environment: JNIEnvironment) -> JNIType") { printer in + printer.print("toJavaObject(in: environment)") + } + printer.println() + printer.printBraceBlock("public func toJavaObject(in environment: JNIEnvironment) -> jobject?") { printer in + printer.print( + """ + let selfPointer$ = self.dictionaryGetJNIValue(in: environment) + var args = [jvalue(), jvalue()] + args[0].j = selfPointer$ + args[1].l = JavaSwiftArena.defaultAutoArena.javaThis + return environment.interface.CallStaticObjectMethodA( + environment, + _SwiftJavaCollectionJavaBoxingCache.swiftDictionaryMapClass, + _SwiftJavaCollectionJavaBoxingCache.swiftDictionaryMapWrapMemoryAddressUnsafe, + &args + ) + """ + ) + } + printer.println() + printer.printBraceBlock("public init(fromJNI value: JNIType, in environment: JNIEnvironment)") { printer in + printer.print("self = Self.fromJavaObject(value, in: environment)") + } + printer.println() + printer.printBraceBlock("public static func fromJavaObject(_ obj: jobject?, in environment: JNIEnvironment) -> Self") { printer in + printer.print( + """ + guard let obj else { + fatalError("\(type.swiftTypeName).fromJavaObject received a null Java object") + } + let selfPointer$ = environment.interface.CallLongMethodA( + environment, + obj, + _JNIMethodIDCache.JNISwiftInstance.memoryAddress, + nil + ) + return Self(fromJNI: selfPointer$, in: environment) + """ + ) + } + printer.println() + printer.printBraceBlock("public static func jniMethodCall(in environment: JNIEnvironment) -> JNIMethodCall") { printer in + printer.print("environment.interface.CallObjectMethodA") + } + printer.println() + printer.printBraceBlock("public static func jniFieldGet(in environment: JNIEnvironment) -> JNIFieldGet") { printer in + printer.print("environment.interface.GetObjectField") + } + printer.println() + printer.printBraceBlock("public static func jniFieldSet(in environment: JNIEnvironment) -> JNIFieldSet") { printer in + printer.print("environment.interface.SetObjectField") + } + printer.println() + printer.printBraceBlock("public static func jniStaticMethodCall(in environment: JNIEnvironment) -> JNIStaticMethodCall") { printer in + printer.print("environment.interface.CallStaticObjectMethodA") + } + printer.println() + printer.printBraceBlock("public static func jniStaticFieldGet(in environment: JNIEnvironment) -> JNIStaticFieldGet") { printer in + printer.print("environment.interface.GetStaticObjectField") + } + printer.println() + printer.printBraceBlock("public static func jniStaticFieldSet(in environment: JNIEnvironment) -> JNIStaticFieldSet") { printer in + printer.print("environment.interface.SetStaticObjectField") + } + printer.println() + printer.printBraceBlock("public static func jniNewArray(in environment: JNIEnvironment) -> JNINewArray") { printer in + printer.print("{ environment, size in environment.interface.NewObjectArray(environment, size, _SwiftJavaCollectionJavaBoxingCache.swiftDictionaryMapClass, nil) }") + } + printer.println() + printer.printBraceBlock("public static func jniGetArrayRegion(in environment: JNIEnvironment) -> JNIGetArrayRegion") { printer in + printer.print( + """ + { environment, array, start, length, outPointer in + let buffer = UnsafeMutableBufferPointer(start: outPointer, count: Int(length)) + for i in start.. JNISetArrayRegion") { printer in + printer.print( + """ + { environment, array, start, length, outPointer in + let buffer = UnsafeBufferPointer(start: outPointer, count: Int(length)) + for i in start.. { \\.l }") + printer.print(#"public static var javaType: JavaType { JavaType(className: "org.swift.swiftkit.core.collections.SwiftSet") }"#) + printer.println() + printer.printBraceBlock("public func getJNIValue(in environment: JNIEnvironment) -> JNIType") { printer in + printer.print("toJavaObject(in: environment)") + } + printer.println() + printer.printBraceBlock("public func toJavaObject(in environment: JNIEnvironment) -> jobject?") { printer in + printer.print( + """ + let selfPointer$ = self.setGetJNIValue(in: environment) + var args = [jvalue(), jvalue()] + args[0].j = selfPointer$ + args[1].l = JavaSwiftArena.defaultAutoArena.javaThis + return environment.interface.CallStaticObjectMethodA( + environment, + _SwiftJavaCollectionJavaBoxingCache.swiftSetClass, + _SwiftJavaCollectionJavaBoxingCache.swiftSetWrapMemoryAddressUnsafe, + &args + ) + """ + ) + } + printer.println() + printer.printBraceBlock("public init(fromJNI value: JNIType, in environment: JNIEnvironment)") { printer in + printer.print("self = Self.fromJavaObject(value, in: environment)") + } + printer.println() + printer.printBraceBlock("public static func fromJavaObject(_ obj: jobject?, in environment: JNIEnvironment) -> Self") { printer in + printer.print( + """ + guard let obj else { + fatalError("\(type.swiftTypeName).fromJavaObject received a null Java object") + } + let selfPointer$ = environment.interface.CallLongMethodA( + environment, + obj, + _JNIMethodIDCache.JNISwiftInstance.memoryAddress, + nil + ) + return Self(fromJNI: selfPointer$, in: environment) + """ + ) + } + printer.println() + printer.printBraceBlock("public static func jniMethodCall(in environment: JNIEnvironment) -> JNIMethodCall") { printer in + printer.print("environment.interface.CallObjectMethodA") + } + printer.println() + printer.printBraceBlock("public static func jniFieldGet(in environment: JNIEnvironment) -> JNIFieldGet") { printer in + printer.print("environment.interface.GetObjectField") + } + printer.println() + printer.printBraceBlock("public static func jniFieldSet(in environment: JNIEnvironment) -> JNIFieldSet") { printer in + printer.print("environment.interface.SetObjectField") + } + printer.println() + printer.printBraceBlock("public static func jniStaticMethodCall(in environment: JNIEnvironment) -> JNIStaticMethodCall") { printer in + printer.print("environment.interface.CallStaticObjectMethodA") + } + printer.println() + printer.printBraceBlock("public static func jniStaticFieldGet(in environment: JNIEnvironment) -> JNIStaticFieldGet") { printer in + printer.print("environment.interface.GetStaticObjectField") + } + printer.println() + printer.printBraceBlock("public static func jniStaticFieldSet(in environment: JNIEnvironment) -> JNIStaticFieldSet") { printer in + printer.print("environment.interface.SetStaticObjectField") + } + printer.println() + printer.printBraceBlock("public static func jniNewArray(in environment: JNIEnvironment) -> JNINewArray") { printer in + printer.print("{ environment, size in environment.interface.NewObjectArray(environment, size, _SwiftJavaCollectionJavaBoxingCache.swiftSetClass, nil) }") + } + printer.println() + printer.printBraceBlock("public static func jniGetArrayRegion(in environment: JNIEnvironment) -> JNIGetArrayRegion") { printer in + printer.print( + """ + { environment, array, start, length, outPointer in + let buffer = UnsafeMutableBufferPointer(start: outPointer, count: Int(length)) + for i in start.. JNISetArrayRegion") { printer in + printer.print( + """ + { environment, array, start, length, outPointer in + let buffer = UnsafeBufferPointer(start: outPointer, count: Int(length)) + for i in start.. [Int: ReefFish] {} + """, + .jni, + .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + @_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: [Int: ReefFish](fromJNI: dict, in: environment)).dictionaryGetJNIValue(in: environment) + } + """, + """ + private enum _SwiftJavaBoxing_ReefFish { + private static let wrapMemoryAddressUnsafeMethod = _JNIMethodIDCache.Method( + name: "wrapMemoryAddressUnsafe", + signature: "(J)Lcom/example/swift/ReefFish;", + isStatic: true + ) + ... + } + """, + """ + extension ReefFish: JavaValue, JavaBoxable { + public typealias JNIType = jobject? + public static var jvalueKeyPath: WritableKeyPath { \\.l } + public static var javaType: JavaType { JavaType(className: "com.example.swift.ReefFish") } + public func getJNIValue(in environment: JNIEnvironment) -> JNIType { + toJavaObject(in: environment) + } + public func toJavaObject(in environment: JNIEnvironment) -> jobject? { + let selfPointer$ = UnsafeMutablePointer.allocate(capacity: 1) + selfPointer$.initialize(to: self) + let selfPointerBits$ = Int64(Int(bitPattern: selfPointer$)) + var args = [jvalue()] + args[0].j = selfPointerBits$.getJNIValue(in: environment) + return environment.interface.CallStaticObjectMethodA( + environment, + _SwiftJavaBoxing_ReefFish.javaClass, + _SwiftJavaBoxing_ReefFish.wrapMemoryAddressUnsafe, + &args + ) + } + ... + public static func fromJavaObject(_ obj: jobject?, in environment: JNIEnvironment) -> ReefFish { + guard let obj else { + fatalError("ReefFish.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("ReefFish.fromJavaObject received a null Swift memory address") + } + return valuePointer$.pointee + } + ... + public static var jniPlaceholderValue: JNIType { nil } + } + """, + ] + ) + } + + @Test("JNI generates JavaValue and JavaBoxable for set element types") + func setCustomElementGeneratesJavaBoxingConformance() throws { + try assertOutput( + input: """ + public struct ReefFish: Hashable {} + public func f(set: Set) -> Set {} + """, + .jni, + .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + @_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) + } + """, + """ + extension ReefFish: JavaValue, JavaBoxable { + public typealias JNIType = jobject? + public static var jvalueKeyPath: WritableKeyPath { \\.l } + public static var javaType: JavaType { JavaType(className: "com.example.swift.ReefFish") } + public func getJNIValue(in environment: JNIEnvironment) -> JNIType { + toJavaObject(in: environment) + } + public func toJavaObject(in environment: JNIEnvironment) -> jobject? { + let selfPointer$ = UnsafeMutablePointer.allocate(capacity: 1) + selfPointer$.initialize(to: self) + let selfPointerBits$ = Int64(Int(bitPattern: selfPointer$)) + var args = [jvalue()] + args[0].j = selfPointerBits$.getJNIValue(in: environment) + return environment.interface.CallStaticObjectMethodA( + environment, + _SwiftJavaBoxing_ReefFish.javaClass, + _SwiftJavaBoxing_ReefFish.wrapMemoryAddressUnsafe, + &args + ) + } + ... + public static func fromJavaObject(_ obj: jobject?, in environment: JNIEnvironment) -> ReefFish { + guard let obj else { + fatalError("ReefFish.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("ReefFish.fromJavaObject received a null Swift memory address") + } + return valuePointer$.pointee + } + ... + public static func jniNewArray(in environment: JNIEnvironment) -> JNINewArray { + { environment, size in environment.interface.NewObjectArray(environment, size, _SwiftJavaBoxing_ReefFish.javaClass, nil) } + } + ... + } + """, + ] + ) + } +} From 090e4fa8e17c23fa7db45fbb7ecfb4b4bc667551 Mon Sep 17 00:00:00 2001 From: Iceman Date: Thu, 7 May 2026 16:47:17 +0900 Subject: [PATCH 02/25] Add support generic types --- .../MySwiftLibrary/CollectionBoxable.swift | 8 +- .../Sources/MySwiftLibrary/Dictionary.swift | 7 ++ .../Sources/MySwiftLibrary/GenericType.swift | 2 +- .../example/swift/SwiftDictionaryMapTest.java | 16 ++++ ...ift2JavaGenerator+SwiftThunkPrinting.swift | 46 +++++++---- .../JNI/JUICollectionBoxableTests.swift | 82 +++++++++++++++++-- 6 files changed, 131 insertions(+), 30 deletions(-) diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/CollectionBoxable.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/CollectionBoxable.swift index 88942319a..015b7bd72 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/CollectionBoxable.swift +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/CollectionBoxable.swift @@ -13,14 +13,10 @@ //===----------------------------------------------------------------------===// public struct ReefFish: Hashable { - private let rawName: String + public var name: String public init(name: String) { - self.rawName = name - } - - public func getName() -> String { - rawName + self.name = name } } diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Dictionary.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Dictionary.swift index 78164e7e2..b447b858a 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Dictionary.swift +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Dictionary.swift @@ -28,3 +28,10 @@ public func insertIntoStringToLongDictionary(dict: [String: Int64], key: String, copy[key] = value return copy } + +public func makeMyIDToFish() -> [MyID: Fish] { + [ + .init(0): Fish(name: "salmon"), + .init(1): Fish(name: "clownfish"), + ] +} diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/GenericType.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/GenericType.swift index 0968489eb..d3c107d40 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/SwiftDictionaryMapTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/SwiftDictionaryMapTest.java index 635d46f21..1a5f63fd0 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/SwiftDictionaryMapTest.java +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/SwiftDictionaryMapTest.java @@ -92,4 +92,20 @@ void toJavaMap() { assertEquals(1L, javaMap.get("hello")); assertEquals(2L, javaMap.get("world")); } + + @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); + + assertTrue(dict.containsKey(salmonId)); + assertTrue(dict.containsKey(clownfishId)); + assertEquals("salmon", dict.get(salmonId).getName()); + assertEquals("clownfish", dict.get(clownfishId).getName()); + } + } } diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift index d667932b5..b3665e210 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift @@ -926,13 +926,7 @@ extension JNISwift2JavaGenerator { guard nominalType.nominalTypeDecl.knownTypeKind == nil else { return } - guard let importedType = analysis.importedTypes[nominalType.nominalTypeDecl.qualifiedName] else { - return - } - guard !importedType.swiftNominal.isGeneric else { - return - } - guard !importedType.isSpecialization else { + guard let importedType = importedNominalCollectionBoxingType(from: nominalType) else { return } nominalTypes.insert(importedType) @@ -1050,10 +1044,19 @@ extension JNISwift2JavaGenerator { } } + private func importedNominalCollectionBoxingType(from nominalType: SwiftNominalType) -> ImportedNominalType? { + analysis.importedTypes[nominalType.nominalTypeDecl.qualifiedName] + } + + private func nominalJavaBoxingCacheName(for type: ImportedNominalType) -> String { + "_SwiftJavaBoxing_\(type.effectiveJavaTypeName.fullFlatName)" + } + private func printNominalJavaBoxableCache(_ printer: inout CodePrinter, _ type: ImportedNominalType) { - let cacheName = "_SwiftJavaBoxing_\(type.effectiveJavaTypeName.fullFlatName)" + let cacheName = nominalJavaBoxingCacheName(for: type) let jniClassName = "\(javaPackagePath)/\(type.effectiveJavaTypeName.jniEscapedName)" - let signature = "(J)L\(jniClassName);" + let isEffectivelyGeneric = type.swiftNominal.isGeneric && type.effectiveJavaTypeName == type.swiftNominal.qualifiedTypeName + let signature = isEffectivelyGeneric ? "(JJ)L\(jniClassName);" : "(J)L\(jniClassName);" printer.printBraceBlock("private enum \(cacheName)") { printer in printer.print( @@ -1082,8 +1085,9 @@ extension JNISwift2JavaGenerator { private func printNominalJavaBoxableExtension(_ printer: inout CodePrinter, _ type: ImportedNominalType) { let swiftTypeName = type.effectiveSwiftTypeName - let cacheName = "_SwiftJavaBoxing_\(type.effectiveJavaTypeName.fullFlatName)" + let cacheName = nominalJavaBoxingCacheName(for: type) let javaClassName = "\(javaPackage).\(type.effectiveJavaTypeName.jniEscapedName)" + let isEffectivelyGeneric = type.swiftNominal.isGeneric && type.effectiveJavaTypeName == type.swiftNominal.qualifiedTypeName printer.printBraceBlock("extension \(swiftTypeName): JavaValue, JavaBoxable") { printer in printer.print("public typealias JNIType = jobject?") @@ -1097,13 +1101,21 @@ extension JNISwift2JavaGenerator { } printer.println() printer.printBraceBlock("public func toJavaObject(in environment: JNIEnvironment) -> jobject?") { printer in + printer.print("let selfPointer$ = UnsafeMutablePointer.allocate(capacity: 1)") + printer.print("selfPointer$.initialize(to: self)") + printer.print("let selfPointerBits$ = Int64(Int(bitPattern: selfPointer$))") + if isEffectivelyGeneric { + printer.print("let selfTypePointer$ = unsafeBitCast(Self.self, to: UnsafeRawPointer.self)") + printer.print("let selfTypePointerBits$ = Int64(Int(bitPattern: selfTypePointer$))") + printer.print("var args = [jvalue(), jvalue()]") + printer.print("args[0].j = selfPointerBits$.getJNIValue(in: environment)") + printer.print("args[1].j = selfTypePointerBits$.getJNIValue(in: environment)") + } else { + printer.print("var args = [jvalue()]") + printer.print("args[0].j = selfPointerBits$.getJNIValue(in: environment)") + } printer.print( """ - let selfPointer$ = UnsafeMutablePointer<\(swiftTypeName)>.allocate(capacity: 1) - selfPointer$.initialize(to: self) - let selfPointerBits$ = Int64(Int(bitPattern: selfPointer$)) - var args = [jvalue()] - args[0].j = selfPointerBits$.getJNIValue(in: environment) return environment.interface.CallStaticObjectMethodA( environment, \(cacheName).javaClass, @@ -1120,7 +1132,7 @@ extension JNISwift2JavaGenerator { printer.print("self = Self.fromJavaObject(value, in: environment)") } printer.println() - printer.printBraceBlock("public static func fromJavaObject(_ obj: jobject?, in environment: JNIEnvironment) -> \(swiftTypeName)") { printer in + printer.printBraceBlock("public static func fromJavaObject(_ obj: jobject?, in environment: JNIEnvironment) -> Self") { printer in printer.print( """ guard let obj else { @@ -1133,7 +1145,7 @@ extension JNISwift2JavaGenerator { nil ) let selfPointerBits$ = Int(Int64(fromJNI: selfPointer$, in: environment)) - guard let valuePointer$ = UnsafeMutablePointer<\(swiftTypeName)>(bitPattern: selfPointerBits$) else { + guard let valuePointer$ = UnsafeMutablePointer(bitPattern: selfPointerBits$) else { fatalError("\(swiftTypeName).fromJavaObject received a null Swift memory address") } return valuePointer$.pointee diff --git a/Tests/JExtractSwiftTests/JNI/JUICollectionBoxableTests.swift b/Tests/JExtractSwiftTests/JNI/JUICollectionBoxableTests.swift index 7ff0c54be..d95c24cd4 100644 --- a/Tests/JExtractSwiftTests/JNI/JUICollectionBoxableTests.swift +++ b/Tests/JExtractSwiftTests/JNI/JUICollectionBoxableTests.swift @@ -53,7 +53,7 @@ struct JUICollectionBoxableTests { toJavaObject(in: environment) } public func toJavaObject(in environment: JNIEnvironment) -> jobject? { - let selfPointer$ = UnsafeMutablePointer.allocate(capacity: 1) + let selfPointer$ = UnsafeMutablePointer.allocate(capacity: 1) selfPointer$.initialize(to: self) let selfPointerBits$ = Int64(Int(bitPattern: selfPointer$)) var args = [jvalue()] @@ -66,7 +66,7 @@ struct JUICollectionBoxableTests { ) } ... - public static func fromJavaObject(_ obj: jobject?, in environment: JNIEnvironment) -> ReefFish { + public static func fromJavaObject(_ obj: jobject?, in environment: JNIEnvironment) -> Self { guard let obj else { fatalError("ReefFish.fromJavaObject received a null Java object") } @@ -77,7 +77,7 @@ struct JUICollectionBoxableTests { nil ) let selfPointerBits$ = Int(Int64(fromJNI: selfPointer$, in: environment)) - guard let valuePointer$ = UnsafeMutablePointer(bitPattern: selfPointerBits$) else { + guard let valuePointer$ = UnsafeMutablePointer(bitPattern: selfPointerBits$) else { fatalError("ReefFish.fromJavaObject received a null Swift memory address") } return valuePointer$.pointee @@ -116,7 +116,7 @@ struct JUICollectionBoxableTests { toJavaObject(in: environment) } public func toJavaObject(in environment: JNIEnvironment) -> jobject? { - let selfPointer$ = UnsafeMutablePointer.allocate(capacity: 1) + let selfPointer$ = UnsafeMutablePointer.allocate(capacity: 1) selfPointer$.initialize(to: self) let selfPointerBits$ = Int64(Int(bitPattern: selfPointer$)) var args = [jvalue()] @@ -129,7 +129,7 @@ struct JUICollectionBoxableTests { ) } ... - public static func fromJavaObject(_ obj: jobject?, in environment: JNIEnvironment) -> ReefFish { + public static func fromJavaObject(_ obj: jobject?, in environment: JNIEnvironment) -> Self { guard let obj else { fatalError("ReefFish.fromJavaObject received a null Java object") } @@ -140,7 +140,7 @@ struct JUICollectionBoxableTests { nil ) let selfPointerBits$ = Int(Int64(fromJNI: selfPointer$, in: environment)) - guard let valuePointer$ = UnsafeMutablePointer(bitPattern: selfPointerBits$) else { + guard let valuePointer$ = UnsafeMutablePointer(bitPattern: selfPointerBits$) else { fatalError("ReefFish.fromJavaObject received a null Swift memory address") } return valuePointer$.pointee @@ -155,4 +155,74 @@ struct JUICollectionBoxableTests { ] ) } + + @Test("JNI generates JavaBoxable for generic dictionary keys") + func genericDictionaryKeyGeneratesJavaBoxingConformance() throws { + try assertOutput( + input: """ + public struct Fish: Hashable {} + public struct MyID: Hashable {} + public func f() -> [MyID: Fish] {} + """, + .jni, + .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + private enum _SwiftJavaBoxing_MyID { + private static let wrapMemoryAddressUnsafeMethod = _JNIMethodIDCache.Method( + name: "wrapMemoryAddressUnsafe", + signature: "(JJ)Lcom/example/swift/MyID;", + isStatic: true + ) + ... + } + """, + """ + extension MyID: JavaValue, JavaBoxable { + public typealias JNIType = jobject? + public static var jvalueKeyPath: WritableKeyPath { \\.l } + public static var javaType: JavaType { JavaType(className: "com.example.swift.MyID") } + public func getJNIValue(in environment: JNIEnvironment) -> JNIType { + toJavaObject(in: environment) + } + public func toJavaObject(in environment: JNIEnvironment) -> jobject? { + let selfPointer$ = UnsafeMutablePointer.allocate(capacity: 1) + selfPointer$.initialize(to: self) + let selfPointerBits$ = Int64(Int(bitPattern: selfPointer$)) + let selfTypePointer$ = unsafeBitCast(Self.self, to: UnsafeRawPointer.self) + let selfTypePointerBits$ = Int64(Int(bitPattern: selfTypePointer$)) + var args = [jvalue(), jvalue()] + args[0].j = selfPointerBits$.getJNIValue(in: environment) + args[1].j = selfTypePointerBits$.getJNIValue(in: environment) + return environment.interface.CallStaticObjectMethodA( + environment, + _SwiftJavaBoxing_MyID.javaClass, + _SwiftJavaBoxing_MyID.wrapMemoryAddressUnsafe, + &args + ) + } + ... + public static func fromJavaObject(_ obj: jobject?, in environment: JNIEnvironment) -> Self { + guard let obj else { + fatalError("MyID.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("MyID.fromJavaObject received a null Swift memory address") + } + return valuePointer$.pointee + } + ... + } + """, + ] + ) + } } From 98ce3018282bababfdeff58d6da52e47ada5c1de Mon Sep 17 00:00:00 2001 From: Iceman Date: Thu, 7 May 2026 16:56:04 +0900 Subject: [PATCH 03/25] Skip to print method cache for Core classes --- ...ift2JavaGenerator+SwiftThunkPrinting.swift | 76 ++----------------- .../JNIMethodIDCaches.swift | 42 ++++++++++ ....swift => JNICollectionBoxableTests.swift} | 72 +++++++++++++++++- 3 files changed, 117 insertions(+), 73 deletions(-) rename Tests/JExtractSwiftTests/JNI/{JUICollectionBoxableTests.swift => JNICollectionBoxableTests.swift} (80%) diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift index b3665e210..17ec896cf 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift @@ -856,11 +856,6 @@ extension JNISwift2JavaGenerator { printer.printSeparator("JavaBoxable conformances for Dictionary/Set element types") - if !boxableTypes.dictionaryTypes.isEmpty || !boxableTypes.setTypes.isEmpty { - printCollectionJavaBoxableCaches(&printer, boxableTypes) - printer.println() - } - for nominalType in boxableTypes.nominalTypes { printNominalJavaBoxableCache(&printer, nominalType) printer.println() @@ -985,65 +980,6 @@ extension JNISwift2JavaGenerator { ) } - private func printCollectionJavaBoxableCaches( - _ printer: inout CodePrinter, - _ boxableTypes: CollectionJavaBoxableTypes - ) { - printer.printBraceBlock("private enum _SwiftJavaCollectionJavaBoxingCache") { printer in - if !boxableTypes.dictionaryTypes.isEmpty { - printer.print( - """ - private static let swiftDictionaryMapWrapMemoryAddressUnsafeMethod = _JNIMethodIDCache.Method( - name: "wrapMemoryAddressUnsafe", - signature: "(JLorg/swift/swiftkit/core/SwiftArena;)Lorg/swift/swiftkit/core/collections/SwiftDictionaryMap;", - isStatic: true - ) - private static let swiftDictionaryMapCache = _JNIMethodIDCache( - className: "org/swift/swiftkit/core/collections/SwiftDictionaryMap", - methods: [swiftDictionaryMapWrapMemoryAddressUnsafeMethod] - ) - - static var swiftDictionaryMapClass: jclass { - swiftDictionaryMapCache.javaClass - } - - static var swiftDictionaryMapWrapMemoryAddressUnsafe: jmethodID { - swiftDictionaryMapCache[swiftDictionaryMapWrapMemoryAddressUnsafeMethod]! - } - """ - ) - } - - if !boxableTypes.dictionaryTypes.isEmpty && !boxableTypes.setTypes.isEmpty { - printer.println() - } - - if !boxableTypes.setTypes.isEmpty { - printer.print( - """ - private static let swiftSetWrapMemoryAddressUnsafeMethod = _JNIMethodIDCache.Method( - name: "wrapMemoryAddressUnsafe", - signature: "(JLorg/swift/swiftkit/core/SwiftArena;)Lorg/swift/swiftkit/core/collections/SwiftSet;", - isStatic: true - ) - private static let swiftSetCache = _JNIMethodIDCache( - className: "org/swift/swiftkit/core/collections/SwiftSet", - methods: [swiftSetWrapMemoryAddressUnsafeMethod] - ) - - static var swiftSetClass: jclass { - swiftSetCache.javaClass - } - - static var swiftSetWrapMemoryAddressUnsafe: jmethodID { - swiftSetCache[swiftSetWrapMemoryAddressUnsafeMethod]! - } - """ - ) - } - } - } - private func importedNominalCollectionBoxingType(from nominalType: SwiftNominalType) -> ImportedNominalType? { analysis.importedTypes[nominalType.nominalTypeDecl.qualifiedName] } @@ -1254,8 +1190,8 @@ extension JNISwift2JavaGenerator { args[1].l = JavaSwiftArena.defaultAutoArena.javaThis return environment.interface.CallStaticObjectMethodA( environment, - _SwiftJavaCollectionJavaBoxingCache.swiftDictionaryMapClass, - _SwiftJavaCollectionJavaBoxingCache.swiftDictionaryMapWrapMemoryAddressUnsafe, + _JNIMethodIDCache.SwiftDictionaryMap.class, + _JNIMethodIDCache.SwiftDictionaryMap.wrapMemoryAddressUnsafe, &args ) """ @@ -1308,7 +1244,7 @@ extension JNISwift2JavaGenerator { } printer.println() printer.printBraceBlock("public static func jniNewArray(in environment: JNIEnvironment) -> JNINewArray") { printer in - printer.print("{ environment, size in environment.interface.NewObjectArray(environment, size, _SwiftJavaCollectionJavaBoxingCache.swiftDictionaryMapClass, nil) }") + printer.print("{ environment, size in environment.interface.NewObjectArray(environment, size, _JNIMethodIDCache.SwiftDictionaryMap.class, nil) }") } printer.println() printer.printBraceBlock("public static func jniGetArrayRegion(in environment: JNIEnvironment) -> JNIGetArrayRegion") { printer in @@ -1366,8 +1302,8 @@ extension JNISwift2JavaGenerator { args[1].l = JavaSwiftArena.defaultAutoArena.javaThis return environment.interface.CallStaticObjectMethodA( environment, - _SwiftJavaCollectionJavaBoxingCache.swiftSetClass, - _SwiftJavaCollectionJavaBoxingCache.swiftSetWrapMemoryAddressUnsafe, + _JNIMethodIDCache.SwiftSet.class, + _JNIMethodIDCache.SwiftSet.wrapMemoryAddressUnsafe, &args ) """ @@ -1420,7 +1356,7 @@ extension JNISwift2JavaGenerator { } printer.println() printer.printBraceBlock("public static func jniNewArray(in environment: JNIEnvironment) -> JNINewArray") { printer in - printer.print("{ environment, size in environment.interface.NewObjectArray(environment, size, _SwiftJavaCollectionJavaBoxingCache.swiftSetClass, nil) }") + printer.print("{ environment, size in environment.interface.NewObjectArray(environment, size, _JNIMethodIDCache.SwiftSet.class, nil) }") } printer.println() printer.printBraceBlock("public static func jniGetArrayRegion(in environment: JNIEnvironment) -> JNIGetArrayRegion") { printer in diff --git a/Sources/SwiftJavaRuntimeSupport/JNIMethodIDCaches.swift b/Sources/SwiftJavaRuntimeSupport/JNIMethodIDCaches.swift index 7c851e8f6..b48962a75 100644 --- a/Sources/SwiftJavaRuntimeSupport/JNIMethodIDCaches.swift +++ b/Sources/SwiftJavaRuntimeSupport/JNIMethodIDCaches.swift @@ -121,6 +121,48 @@ 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 _OutSwiftGenericInstance { private static let selfPointerField = Field( name: "selfPointer", diff --git a/Tests/JExtractSwiftTests/JNI/JUICollectionBoxableTests.swift b/Tests/JExtractSwiftTests/JNI/JNICollectionBoxableTests.swift similarity index 80% rename from Tests/JExtractSwiftTests/JNI/JUICollectionBoxableTests.swift rename to Tests/JExtractSwiftTests/JNI/JNICollectionBoxableTests.swift index d95c24cd4..f6425b760 100644 --- a/Tests/JExtractSwiftTests/JNI/JUICollectionBoxableTests.swift +++ b/Tests/JExtractSwiftTests/JNI/JNICollectionBoxableTests.swift @@ -16,7 +16,7 @@ import JExtractSwiftLib import Testing @Suite -struct JUICollectionBoxableTests { +struct JNICollectionBoxableTests { @Test("JNI generates JavaValue and JavaBoxable for dictionary element types") func dictionaryCustomValueGeneratesJavaBoxingConformance() throws { try assertOutput( @@ -86,7 +86,8 @@ struct JUICollectionBoxableTests { public static var jniPlaceholderValue: JNIType { nil } } """, - ] + ], + notExpectedChunks: ["private enum _SwiftJavaCollectionJavaBoxingCache"] ) } @@ -152,7 +153,72 @@ struct JUICollectionBoxableTests { ... } """, - ] + ], + notExpectedChunks: ["private enum _SwiftJavaCollectionJavaBoxingCache"] + ) + } + + @Test("JNI uses runtime support cache for nested dictionary boxing") + func nestedDictionaryUsesRuntimeSupportCache() throws { + try assertOutput( + input: """ + public struct ReefFish: Hashable {} + public func f(dict: [String: [Int: ReefFish]]) -> [String: [Int: ReefFish]] {} + """, + .jni, + .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + extension [Int: ReefFish]: JavaValue, JavaBoxable { + ... + let selfPointer$ = self.dictionaryGetJNIValue(in: environment) + 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 + ) + ... + } + """, + ], + notExpectedChunks: ["private enum _SwiftJavaCollectionJavaBoxingCache"] + ) + } + + @Test("JNI uses runtime support cache for nested set boxing") + func nestedSetUsesRuntimeSupportCache() throws { + try assertOutput( + input: """ + public struct ReefFish: Hashable {} + public func f(dict: [String: Set]) -> [String: Set] {} + """, + .jni, + .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + extension Set: JavaValue, JavaBoxable { + ... + let selfPointer$ = self.setGetJNIValue(in: environment) + 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 + ) + ... + } + """, + ], + notExpectedChunks: ["private enum _SwiftJavaCollectionJavaBoxingCache"] ) } From 72b4808549037812c88debd13be6cdd5578c23c2 Mon Sep 17 00:00:00 2001 From: Iceman Date: Thu, 7 May 2026 17:25:37 +0900 Subject: [PATCH 04/25] Remove JavaBoxable extension generations for collection types --- ...ift2JavaGenerator+SwiftThunkPrinting.swift | 257 +----------------- .../Collection+JavaBoxable.swift | 219 +++++++++++++++ .../JNI/JNICollectionBoxableTests.swift | 42 +-- 3 files changed, 235 insertions(+), 283 deletions(-) create mode 100644 Sources/SwiftJavaRuntimeSupport/Collection+JavaBoxable.swift diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift index 17ec896cf..c34f03d8d 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift @@ -830,21 +830,11 @@ extension JNISwift2JavaGenerator { } } - private struct ConcreteDictionaryJavaBoxable: Hashable { - let swiftTypeName: String - } - - private struct ConcreteSetJavaBoxable: Hashable { - let swiftTypeName: String - } - private struct CollectionJavaBoxableTypes { let nominalTypes: [ImportedNominalType] - let dictionaryTypes: [ConcreteDictionaryJavaBoxable] - let setTypes: [ConcreteSetJavaBoxable] var isEmpty: Bool { - nominalTypes.isEmpty && dictionaryTypes.isEmpty && setTypes.isEmpty + nominalTypes.isEmpty } } @@ -863,21 +853,10 @@ extension JNISwift2JavaGenerator { printer.println() } - for dictionaryType in boxableTypes.dictionaryTypes { - printConcreteDictionaryJavaBoxableExtension(&printer, dictionaryType) - printer.println() - } - - for setType in boxableTypes.setTypes { - printConcreteSetJavaBoxableExtension(&printer, setType) - printer.println() - } } private func collectCollectionJavaBoxableTypes() -> CollectionJavaBoxableTypes { var nominalTypes: Set = [] - var dictionaryTypes: Set = [] - var setTypes: Set = [] func collect(_ type: SwiftType, insideCollectionElement: Bool = false) { switch type { @@ -885,17 +864,11 @@ extension JNISwift2JavaGenerator { if let knownType = nominalType.asKnownType { switch knownType { case .dictionary(let keyType, let valueType): - if insideCollectionElement { - dictionaryTypes.insert(.init(swiftTypeName: type.description)) - } collect(keyType, insideCollectionElement: true) collect(valueType, insideCollectionElement: true) return case .set(let elementType): - if insideCollectionElement { - setTypes.insert(.init(swiftTypeName: type.description)) - } collect(elementType, insideCollectionElement: true) return @@ -974,9 +947,7 @@ extension JNISwift2JavaGenerator { } return CollectionJavaBoxableTypes( - nominalTypes: nominalTypes.sorted { $0.effectiveSwiftTypeName < $1.effectiveSwiftTypeName }, - dictionaryTypes: dictionaryTypes.sorted { $0.swiftTypeName < $1.swiftTypeName }, - setTypes: setTypes.sorted { $0.swiftTypeName < $1.swiftTypeName } + nominalTypes: nominalTypes.sorted { $0.effectiveSwiftTypeName < $1.effectiveSwiftTypeName } ) } @@ -1168,230 +1139,6 @@ extension JNISwift2JavaGenerator { } } - private func printConcreteDictionaryJavaBoxableExtension( - _ printer: inout CodePrinter, - _ type: ConcreteDictionaryJavaBoxable - ) { - printer.printBraceBlock("extension \(type.swiftTypeName): JavaValue, JavaBoxable") { printer in - printer.print("public typealias JNIType = jobject?") - printer.print("public static var jvalueKeyPath: WritableKeyPath { \\.l }") - printer.print(#"public static var javaType: JavaType { JavaType(className: "org.swift.swiftkit.core.collections.SwiftDictionaryMap") }"#) - printer.println() - printer.printBraceBlock("public func getJNIValue(in environment: JNIEnvironment) -> JNIType") { printer in - printer.print("toJavaObject(in: environment)") - } - printer.println() - printer.printBraceBlock("public func toJavaObject(in environment: JNIEnvironment) -> jobject?") { printer in - printer.print( - """ - let selfPointer$ = self.dictionaryGetJNIValue(in: environment) - 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 - ) - """ - ) - } - printer.println() - printer.printBraceBlock("public init(fromJNI value: JNIType, in environment: JNIEnvironment)") { printer in - printer.print("self = Self.fromJavaObject(value, in: environment)") - } - printer.println() - printer.printBraceBlock("public static func fromJavaObject(_ obj: jobject?, in environment: JNIEnvironment) -> Self") { printer in - printer.print( - """ - guard let obj else { - fatalError("\(type.swiftTypeName).fromJavaObject received a null Java object") - } - let selfPointer$ = environment.interface.CallLongMethodA( - environment, - obj, - _JNIMethodIDCache.JNISwiftInstance.memoryAddress, - nil - ) - return Self(fromJNI: selfPointer$, in: environment) - """ - ) - } - printer.println() - printer.printBraceBlock("public static func jniMethodCall(in environment: JNIEnvironment) -> JNIMethodCall") { printer in - printer.print("environment.interface.CallObjectMethodA") - } - printer.println() - printer.printBraceBlock("public static func jniFieldGet(in environment: JNIEnvironment) -> JNIFieldGet") { printer in - printer.print("environment.interface.GetObjectField") - } - printer.println() - printer.printBraceBlock("public static func jniFieldSet(in environment: JNIEnvironment) -> JNIFieldSet") { printer in - printer.print("environment.interface.SetObjectField") - } - printer.println() - printer.printBraceBlock("public static func jniStaticMethodCall(in environment: JNIEnvironment) -> JNIStaticMethodCall") { printer in - printer.print("environment.interface.CallStaticObjectMethodA") - } - printer.println() - printer.printBraceBlock("public static func jniStaticFieldGet(in environment: JNIEnvironment) -> JNIStaticFieldGet") { printer in - printer.print("environment.interface.GetStaticObjectField") - } - printer.println() - printer.printBraceBlock("public static func jniStaticFieldSet(in environment: JNIEnvironment) -> JNIStaticFieldSet") { printer in - printer.print("environment.interface.SetStaticObjectField") - } - printer.println() - printer.printBraceBlock("public static func jniNewArray(in environment: JNIEnvironment) -> JNINewArray") { printer in - printer.print("{ environment, size in environment.interface.NewObjectArray(environment, size, _JNIMethodIDCache.SwiftDictionaryMap.class, nil) }") - } - printer.println() - printer.printBraceBlock("public static func jniGetArrayRegion(in environment: JNIEnvironment) -> JNIGetArrayRegion") { printer in - printer.print( - """ - { environment, array, start, length, outPointer in - let buffer = UnsafeMutableBufferPointer(start: outPointer, count: Int(length)) - for i in start.. JNISetArrayRegion") { printer in - printer.print( - """ - { environment, array, start, length, outPointer in - let buffer = UnsafeBufferPointer(start: outPointer, count: Int(length)) - for i in start.. { \\.l }") - printer.print(#"public static var javaType: JavaType { JavaType(className: "org.swift.swiftkit.core.collections.SwiftSet") }"#) - printer.println() - printer.printBraceBlock("public func getJNIValue(in environment: JNIEnvironment) -> JNIType") { printer in - printer.print("toJavaObject(in: environment)") - } - printer.println() - printer.printBraceBlock("public func toJavaObject(in environment: JNIEnvironment) -> jobject?") { printer in - printer.print( - """ - let selfPointer$ = self.setGetJNIValue(in: environment) - 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 - ) - """ - ) - } - printer.println() - printer.printBraceBlock("public init(fromJNI value: JNIType, in environment: JNIEnvironment)") { printer in - printer.print("self = Self.fromJavaObject(value, in: environment)") - } - printer.println() - printer.printBraceBlock("public static func fromJavaObject(_ obj: jobject?, in environment: JNIEnvironment) -> Self") { printer in - printer.print( - """ - guard let obj else { - fatalError("\(type.swiftTypeName).fromJavaObject received a null Java object") - } - let selfPointer$ = environment.interface.CallLongMethodA( - environment, - obj, - _JNIMethodIDCache.JNISwiftInstance.memoryAddress, - nil - ) - return Self(fromJNI: selfPointer$, in: environment) - """ - ) - } - printer.println() - printer.printBraceBlock("public static func jniMethodCall(in environment: JNIEnvironment) -> JNIMethodCall") { printer in - printer.print("environment.interface.CallObjectMethodA") - } - printer.println() - printer.printBraceBlock("public static func jniFieldGet(in environment: JNIEnvironment) -> JNIFieldGet") { printer in - printer.print("environment.interface.GetObjectField") - } - printer.println() - printer.printBraceBlock("public static func jniFieldSet(in environment: JNIEnvironment) -> JNIFieldSet") { printer in - printer.print("environment.interface.SetObjectField") - } - printer.println() - printer.printBraceBlock("public static func jniStaticMethodCall(in environment: JNIEnvironment) -> JNIStaticMethodCall") { printer in - printer.print("environment.interface.CallStaticObjectMethodA") - } - printer.println() - printer.printBraceBlock("public static func jniStaticFieldGet(in environment: JNIEnvironment) -> JNIStaticFieldGet") { printer in - printer.print("environment.interface.GetStaticObjectField") - } - printer.println() - printer.printBraceBlock("public static func jniStaticFieldSet(in environment: JNIEnvironment) -> JNIStaticFieldSet") { printer in - printer.print("environment.interface.SetStaticObjectField") - } - printer.println() - printer.printBraceBlock("public static func jniNewArray(in environment: JNIEnvironment) -> JNINewArray") { printer in - printer.print("{ environment, size in environment.interface.NewObjectArray(environment, size, _JNIMethodIDCache.SwiftSet.class, nil) }") - } - printer.println() - printer.printBraceBlock("public static func jniGetArrayRegion(in environment: JNIEnvironment) -> JNIGetArrayRegion") { printer in - printer.print( - """ - { environment, array, start, length, outPointer in - let buffer = UnsafeMutableBufferPointer(start: outPointer, count: Int(length)) - for i in start.. JNISetArrayRegion") { printer in - printer.print( - """ - { environment, array, start, length, outPointer in - let buffer = UnsafeBufferPointer(start: outPointer, count: Int(length)) - for i in start.. { \.l } + + public static var javaType: JavaType { + JavaType(className: "org.swift.swiftkit.core.collections.SwiftDictionaryMap") + } + + public func getJNIValue(in environment: JNIEnvironment) -> JNIType { + toJavaObject(in: environment) + } + + public func toJavaObject(in environment: JNIEnvironment) -> jobject? { + let selfPointer = self.dictionaryGetJNIValue(in: environment) + 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) -> Self { + guard let obj else { + fatalError("Dictionary.fromJavaObject received a null Java object") + } + let selfPointer = environment.interface.CallLongMethodA( + environment, + obj, + _JNIMethodIDCache.JNISwiftInstance.memoryAddress, + nil + ) + return Self(fromJNI: selfPointer, in: environment) + } + + public init(fromJNI value: JNIType, in environment: JNIEnvironment) { + self = Self.fromJavaObject(value, in: environment) + } + + public static func jniMethodCall(in environment: JNIEnvironment) -> JNIMethodCall { + environment.interface.CallObjectMethodA + } + + public static func jniFieldGet(in environment: JNIEnvironment) -> JNIFieldGet { + environment.interface.GetObjectField + } + + public static func jniFieldSet(in environment: JNIEnvironment) -> JNIFieldSet { + environment.interface.SetObjectField + } + + public static func jniStaticMethodCall(in environment: JNIEnvironment) -> JNIStaticMethodCall { + environment.interface.CallStaticObjectMethodA + } + + public static func jniStaticFieldGet(in environment: JNIEnvironment) -> JNIStaticFieldGet { + environment.interface.GetStaticObjectField + } + + public static func jniStaticFieldSet(in environment: JNIEnvironment) -> JNIStaticFieldSet { + environment.interface.SetStaticObjectField + } + + public static func jniNewArray(in environment: JNIEnvironment) -> JNINewArray { + { environment, size in + environment.interface.NewObjectArray( + environment, + size, + _JNIMethodIDCache.SwiftDictionaryMap.class, + nil + ) + } + } + + public static func jniGetArrayRegion(in environment: JNIEnvironment) -> JNIGetArrayRegion { + { environment, array, start, length, outPointer in + let buffer = UnsafeMutableBufferPointer(start: outPointer, count: Int(length)) + for i in start.. JNISetArrayRegion { + { environment, array, start, length, outPointer in + let buffer = UnsafeBufferPointer(start: outPointer, count: Int(length)) + for i in start.. { \.l } + + public static var javaType: JavaType { + JavaType(className: "org.swift.swiftkit.core.collections.SwiftSet") + } + + public func getJNIValue(in environment: JNIEnvironment) -> JNIType { + toJavaObject(in: environment) + } + + public func toJavaObject(in environment: JNIEnvironment) -> jobject? { + let selfPointer = self.setGetJNIValue(in: environment) + 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) -> Self { + guard let obj else { + fatalError("Set.fromJavaObject received a null Java object") + } + let selfPointer = environment.interface.CallLongMethodA( + environment, + obj, + _JNIMethodIDCache.JNISwiftInstance.memoryAddress, + nil + ) + return Self(fromJNI: selfPointer, in: environment) + } + + public init(fromJNI value: JNIType, in environment: JNIEnvironment) { + self = Self.fromJavaObject(value, in: environment) + } + + public static func jniMethodCall(in environment: JNIEnvironment) -> JNIMethodCall { + environment.interface.CallObjectMethodA + } + + public static func jniFieldGet(in environment: JNIEnvironment) -> JNIFieldGet { + environment.interface.GetObjectField + } + + public static func jniFieldSet(in environment: JNIEnvironment) -> JNIFieldSet { + environment.interface.SetObjectField + } + + public static func jniStaticMethodCall(in environment: JNIEnvironment) -> JNIStaticMethodCall { + environment.interface.CallStaticObjectMethodA + } + + public static func jniStaticFieldGet(in environment: JNIEnvironment) -> JNIStaticFieldGet { + environment.interface.GetStaticObjectField + } + + public static func jniStaticFieldSet(in environment: JNIEnvironment) -> JNIStaticFieldSet { + environment.interface.SetStaticObjectField + } + + public static func jniNewArray(in environment: JNIEnvironment) -> JNINewArray { + { environment, size in + environment.interface.NewObjectArray( + environment, + size, + _JNIMethodIDCache.SwiftSet.class, + nil + ) + } + } + + public static func jniGetArrayRegion(in environment: JNIEnvironment) -> JNIGetArrayRegion { + { environment, array, start, length, outPointer in + let buffer = UnsafeMutableBufferPointer(start: outPointer, count: Int(length)) + for i in start.. JNISetArrayRegion { + { environment, array, start, length, outPointer in + let buffer = UnsafeBufferPointer(start: outPointer, count: Int(length)) + for i in start..!, thisClass: jclass, dict: jlong) -> jlong { + return SwiftModule.f(dict: [String: [Int: ReefFish]](fromJNI: dict, in: environment)).dictionaryGetJNIValue(in: environment) } """, ], - notExpectedChunks: ["private enum _SwiftJavaCollectionJavaBoxingCache"] + notExpectedChunks: [ + "private enum _SwiftJavaCollectionJavaBoxingCache", + "extension [Int: ReefFish]: JavaValue, JavaBoxable", + ] ) } @@ -202,23 +195,16 @@ struct JNICollectionBoxableTests { detectChunkByInitialLines: 1, expectedChunks: [ """ - extension Set: JavaValue, JavaBoxable { - ... - let selfPointer$ = self.setGetJNIValue(in: environment) - 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 - ) - ... + @_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: Set](fromJNI: dict, in: environment)).dictionaryGetJNIValue(in: environment) } """, ], - notExpectedChunks: ["private enum _SwiftJavaCollectionJavaBoxingCache"] + notExpectedChunks: [ + "private enum _SwiftJavaCollectionJavaBoxingCache", + "extension Set: JavaValue, JavaBoxable", + ] ) } From d31aa698c98afc9a9450224cda7ecc53c90b68bd Mon Sep 17 00:00:00 2001 From: Iceman Date: Thu, 7 May 2026 17:49:37 +0900 Subject: [PATCH 05/25] Update examples --- .../MySwiftLibrary/BoxSpecialization.swift | 4 +- .../MySwiftLibrary/CollectionBoxable.swift | 43 +++++-------- .../Sources/MySwiftLibrary/Dictionary.swift | 7 --- .../example/swift/CollectionBoxableTest.java | 62 ++++++++++--------- .../example/swift/SwiftDictionaryMapTest.java | 16 ----- ...ift2JavaGenerator+SwiftThunkPrinting.swift | 17 +---- 6 files changed, 54 insertions(+), 95 deletions(-) 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 index 015b7bd72..9c2ca2396 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/CollectionBoxable.swift +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/CollectionBoxable.swift @@ -12,44 +12,35 @@ // //===----------------------------------------------------------------------===// -public struct ReefFish: Hashable { - public var name: String - - public init(name: String) { - self.name = name - } -} - -public func makeIntToFishDictionary() -> [Int: ReefFish] { +public func makeIntToFishDictionary() -> [Int: Fish] { [ - 1: ReefFish(name: "salmon"), - 2: ReefFish(name: "clownfish"), + 1: Fish(name: "salmon"), + 2: Fish(name: "clownfish"), ] } -public func intToFishDictionary(dict: [Int: ReefFish]) -> [Int: ReefFish] { +public func intToFishDictionary(dict: [Int: Fish]) -> [Int: Fish] { dict } -public func insertIntoIntToFishDictionary(dict: [Int: ReefFish], key: Int, value: ReefFish) -> [Int: ReefFish] { - var copy = dict - copy[key] = value - return copy -} - -public func makeFishSet() -> Set { +public func makeFishSet() -> Set { [ - ReefFish(name: "salmon"), - ReefFish(name: "clownfish"), + Fish(name: "salmon"), + Fish(name: "clownfish"), ] } -public func fishSet(set: Set) -> Set { +public func fishSet(set: Set) -> Set { set } -public func insertIntoFishSet(set: Set, fish: ReefFish) -> Set { - var copy = set - copy.insert(fish) - return copy +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)] } diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Dictionary.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Dictionary.swift index b447b858a..78164e7e2 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Dictionary.swift +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Dictionary.swift @@ -28,10 +28,3 @@ public func insertIntoStringToLongDictionary(dict: [String: Int64], key: String, copy[key] = value return copy } - -public func makeMyIDToFish() -> [MyID: Fish] { - [ - .init(0): Fish(name: "salmon"), - .init(1): Fish(name: "clownfish"), - ] -} diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/CollectionBoxableTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/CollectionBoxableTest.java index b1e5bc00c..c6f19286b 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/CollectionBoxableTest.java +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/CollectionBoxableTest.java @@ -21,16 +21,19 @@ import static org.junit.jupiter.api.Assertions.*; +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); + 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); + SwiftDictionaryMap roundtripped = MySwiftLibrary.intToFishDictionary(original, arena); assertEquals(2, roundtripped.size()); assertEquals("salmon", roundtripped.get(1L).getName()); assertEquals("clownfish", roundtripped.get(2L).getName()); @@ -38,48 +41,47 @@ void intToFishDictionaryRoundtrip() { } @Test - void insertIntoIntToFishDictionary() { + void fishSetRoundtrip() { try (var arena = SwiftArena.ofConfined()) { - SwiftDictionaryMap original = MySwiftLibrary.makeIntToFishDictionary(arena); - ReefFish tuna = ReefFish.init("tuna", arena); - - SwiftDictionaryMap modified = - MySwiftLibrary.insertIntoIntToFishDictionary(original, 3L, tuna, arena); - - assertEquals(3, modified.size()); - assertEquals("tuna", modified.get(3L).getName()); + SwiftSet original = MySwiftLibrary.makeFishSet(arena); assertEquals(2, original.size()); - assertNull(original.get(3L)); + 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 fishSetRoundtrip() { + void makeMyIDToFish() { try (var arena = SwiftArena.ofConfined()) { - SwiftSet original = MySwiftLibrary.makeFishSet(arena); - assertEquals(2, original.size()); - assertTrue(original.contains(ReefFish.init("salmon", arena))); - assertTrue(original.contains(ReefFish.init("clownfish", arena))); + SwiftDictionaryMap, Fish> dict = MySwiftLibrary.makeMyIDToFish(arena); + assertEquals(2, dict.size()); - SwiftSet roundtripped = MySwiftLibrary.fishSet(original, arena); - assertEquals(2, roundtripped.size()); - assertTrue(roundtripped.contains(ReefFish.init("salmon", arena))); - assertTrue(roundtripped.contains(ReefFish.init("clownfish", arena))); + MyID salmonId = MyIDs.makeIntID(0, arena); + MyID clownfishId = MyIDs.makeIntID(1, arena); + + assertTrue(dict.containsKey(salmonId)); + assertTrue(dict.containsKey(clownfishId)); + assertEquals("salmon", dict.get(salmonId).getName()); + assertEquals("clownfish", dict.get(clownfishId).getName()); } } @Test - void insertIntoFishSet() { + void makeSpecializedGenericTypeSet() { try (var arena = SwiftArena.ofConfined()) { - SwiftSet original = MySwiftLibrary.makeFishSet(arena); - ReefFish tuna = ReefFish.init("tuna", arena); - - SwiftSet modified = MySwiftLibrary.insertIntoFishSet(original, tuna, arena); + SwiftSet> set = MySwiftLibrary.makeSpecializedGenericTypeSet(arena); - assertEquals(3, modified.size()); - assertTrue(modified.contains(tuna)); - assertEquals(2, original.size()); - assertFalse(original.contains(tuna)); + assertEquals( + set.stream() + .map(Box::getCount) + .collect(Collectors.toSet()), + Set.of(2L, 3L) + ); } } } diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/SwiftDictionaryMapTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/SwiftDictionaryMapTest.java index 1a5f63fd0..635d46f21 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/SwiftDictionaryMapTest.java +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/SwiftDictionaryMapTest.java @@ -92,20 +92,4 @@ void toJavaMap() { assertEquals(1L, javaMap.get("hello")); assertEquals(2L, javaMap.get("world")); } - - @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); - - assertTrue(dict.containsKey(salmonId)); - assertTrue(dict.containsKey(clownfishId)); - assertEquals("salmon", dict.get(salmonId).getName()); - assertEquals("clownfish", dict.get(clownfishId).getName()); - } - } } diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift index c34f03d8d..f52aab878 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift @@ -830,14 +830,6 @@ extension JNISwift2JavaGenerator { } } - private struct CollectionJavaBoxableTypes { - let nominalTypes: [ImportedNominalType] - - var isEmpty: Bool { - nominalTypes.isEmpty - } - } - private func printCollectionJavaBoxableExtensions(_ printer: inout CodePrinter) { let boxableTypes = collectCollectionJavaBoxableTypes() guard !boxableTypes.isEmpty else { @@ -846,16 +838,15 @@ extension JNISwift2JavaGenerator { printer.printSeparator("JavaBoxable conformances for Dictionary/Set element types") - for nominalType in boxableTypes.nominalTypes { + for nominalType in boxableTypes { printNominalJavaBoxableCache(&printer, nominalType) printer.println() printNominalJavaBoxableExtension(&printer, nominalType) printer.println() } - } - private func collectCollectionJavaBoxableTypes() -> CollectionJavaBoxableTypes { + private func collectCollectionJavaBoxableTypes() -> [ImportedNominalType] { var nominalTypes: Set = [] func collect(_ type: SwiftType, insideCollectionElement: Bool = false) { @@ -946,9 +937,7 @@ extension JNISwift2JavaGenerator { } } - return CollectionJavaBoxableTypes( - nominalTypes: nominalTypes.sorted { $0.effectiveSwiftTypeName < $1.effectiveSwiftTypeName } - ) + return nominalTypes.sorted { $0.effectiveSwiftTypeName < $1.effectiveSwiftTypeName } } private func importedNominalCollectionBoxingType(from nominalType: SwiftNominalType) -> ImportedNominalType? { From c744d2a72027c3ac085874bf21d74063ad48a204 Mon Sep 17 00:00:00 2001 From: Iceman Date: Fri, 8 May 2026 11:32:23 +0900 Subject: [PATCH 06/25] Fix optional boxing --- .../MySwiftLibrary/CollectionBoxable.swift | 10 +++++++ ...ift2JavaGenerator+SwiftThunkPrinting.swift | 27 ++++++----------- .../SwiftJava/BridgedValues/JavaBoxing.swift | 2 +- Sources/SwiftJava/Optional+JavaObject.swift | 17 +++++++++++ .../JNI/JNICollectionBoxableTests.swift | 30 +++++++++++++++++++ 5 files changed, 67 insertions(+), 19 deletions(-) diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/CollectionBoxable.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/CollectionBoxable.swift index 9c2ca2396..f6700e008 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/CollectionBoxable.swift +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/CollectionBoxable.swift @@ -12,6 +12,12 @@ // //===----------------------------------------------------------------------===// +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + public func makeIntToFishDictionary() -> [Int: Fish] { [ 1: Fish(name: "salmon"), @@ -44,3 +50,7 @@ public func makeMyIDToFish() -> [MyID: Fish] { public func makeSpecializedGenericTypeSet() -> Set { [.init(count: 2), .init(count: 3)] } + +public func makeFoundationTypeToOptional(uuid: UUID) -> [UUID: String?] { + [uuid: uuid.uuidString] +} diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift index f52aab878..1d56dd2da 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift @@ -858,37 +858,32 @@ extension JNISwift2JavaGenerator { collect(keyType, insideCollectionElement: true) collect(valueType, insideCollectionElement: true) return - case .set(let elementType): collect(elementType, insideCollectionElement: true) return - case .optional(let wrappedType): collect(wrappedType, insideCollectionElement: insideCollectionElement) return - case .array(let elementType): collect(elementType, insideCollectionElement: false) return - - default: + case .foundationData, .essentialsData, .foundationDate, .essentialsDate, .foundationUUID, .essentialsUUID: break + default: + // Other known types should be treatable as JavaBoxable. + return } } - guard insideCollectionElement else { - return - } guard !nominalType.isSwiftJavaWrapper else { return } - guard nominalType.nominalTypeDecl.knownTypeKind == nil else { - return - } - guard let importedType = importedNominalCollectionBoxingType(from: nominalType) else { - return + if insideCollectionElement { + guard let importedType = analysis.importedTypes[nominalType.nominalTypeDecl.qualifiedName] else { + return + } + nominalTypes.insert(importedType) } - nominalTypes.insert(importedType) case .tuple(let elements): for element in elements { @@ -940,10 +935,6 @@ extension JNISwift2JavaGenerator { return nominalTypes.sorted { $0.effectiveSwiftTypeName < $1.effectiveSwiftTypeName } } - private func importedNominalCollectionBoxingType(from nominalType: SwiftNominalType) -> ImportedNominalType? { - analysis.importedTypes[nominalType.nominalTypeDecl.qualifiedName] - } - private func nominalJavaBoxingCacheName(for type: ImportedNominalType) -> String { "_SwiftJavaBoxing_\(type.effectiveJavaTypeName.fullFlatName)" } diff --git a/Sources/SwiftJava/BridgedValues/JavaBoxing.swift b/Sources/SwiftJava/BridgedValues/JavaBoxing.swift index 00efeb500..a5afd8660 100644 --- a/Sources/SwiftJava/BridgedValues/JavaBoxing.swift +++ b/Sources/SwiftJava/BridgedValues/JavaBoxing.swift @@ -20,7 +20,7 @@ import SwiftJavaJNICore /// A type that can be boxed into and unboxed from a Java object via JNI. /// This is used for dictionary keys and values that need to cross the JNI boundary /// as boxed Java objects (e.g. Long, Double, Boolean, String). -public protocol JavaBoxable: JavaValue { +public protocol JavaBoxable { /// Convert this Swift value to a boxed Java object. func toJavaObject(in environment: JNIEnvironment) -> jobject? diff --git a/Sources/SwiftJava/Optional+JavaObject.swift b/Sources/SwiftJava/Optional+JavaObject.swift index e972048b3..295d40cc0 100644 --- a/Sources/SwiftJava/Optional+JavaObject.swift +++ b/Sources/SwiftJava/Optional+JavaObject.swift @@ -114,3 +114,20 @@ extension Optional: @retroactive JavaValue where Wrapped: AnyJavaObject { nil } } + +extension Optional: JavaBoxable where Wrapped: JavaBoxable { + public func toJavaObject(in environment: SwiftJavaJNICore.JNIEnvironment) -> jobject? { + switch self { + case let value?: value.toJavaObject(in: environment) + case nil: nil + } + } + + public static func fromJavaObject(_ obj: jobject?, in environment: SwiftJavaJNICore.JNIEnvironment) -> Optional { + if let obj { + return Wrapped.fromJavaObject(obj, in: environment) + } else { + return nil + } + } +} diff --git a/Tests/JExtractSwiftTests/JNI/JNICollectionBoxableTests.swift b/Tests/JExtractSwiftTests/JNI/JNICollectionBoxableTests.swift index 5a8e21dc1..432818ac3 100644 --- a/Tests/JExtractSwiftTests/JNI/JNICollectionBoxableTests.swift +++ b/Tests/JExtractSwiftTests/JNI/JNICollectionBoxableTests.swift @@ -158,6 +158,36 @@ struct JNICollectionBoxableTests { ) } + @Test("JNI supports optional collection values without generating optional boxing") + func dictionaryOptionalValueUsesRuntimeOptionalBoxing() throws { + try assertOutput( + input: """ + public struct ReefFish: Hashable {} + public func f(dict: [Int: ReefFish?]) -> [Int: ReefFish?] {} + """, + .jni, + .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + @_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: [Int: ReefFish?](fromJNI: dict, in: environment)).dictionaryGetJNIValue(in: environment) + } + """, + """ + extension ReefFish: JavaValue, JavaBoxable { + ... + } + """, + ], + notExpectedChunks: [ + "extension Optional: JavaValue, JavaBoxable", + "extension ReefFish?: JavaValue, JavaBoxable", + ] + ) + } + @Test("JNI uses runtime support cache for nested dictionary boxing") func nestedDictionaryUsesRuntimeSupportCache() throws { try assertOutput( From 381586e77f8c94b7bce277498b40bd6f2dcd89a4 Mon Sep 17 00:00:00 2001 From: Iceman Date: Fri, 8 May 2026 15:50:32 +0900 Subject: [PATCH 07/25] Skip optional supporting --- .../MySwiftLibrary/CollectionBoxable.swift | 4 - .../example/swift/CollectionBoxableTest.java | 2 + .../test/java/com/example/swift/DataTest.java | 2 +- ...ift2JavaGenerator+SwiftThunkPrinting.swift | 115 ++---------------- Sources/SwiftJava/Optional+JavaObject.swift | 17 --- 5 files changed, 14 insertions(+), 126 deletions(-) diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/CollectionBoxable.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/CollectionBoxable.swift index f6700e008..06848f30e 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/CollectionBoxable.swift +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/CollectionBoxable.swift @@ -50,7 +50,3 @@ public func makeMyIDToFish() -> [MyID: Fish] { public func makeSpecializedGenericTypeSet() -> Set { [.init(count: 2), .init(count: 3)] } - -public func makeFoundationTypeToOptional(uuid: UUID) -> [UUID: String?] { - [uuid: uuid.uuidString] -} diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/CollectionBoxableTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/CollectionBoxableTest.java index c6f19286b..de6355736 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/CollectionBoxableTest.java +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/CollectionBoxableTest.java @@ -63,9 +63,11 @@ void makeMyIDToFish() { 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()); } 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/JNISwift2JavaGenerator+SwiftThunkPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift index 1d56dd2da..8cf1949de 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift @@ -836,8 +836,6 @@ extension JNISwift2JavaGenerator { return } - printer.printSeparator("JavaBoxable conformances for Dictionary/Set element types") - for nominalType in boxableTypes { printNominalJavaBoxableCache(&printer, nominalType) printer.println() @@ -857,27 +855,30 @@ extension JNISwift2JavaGenerator { case .dictionary(let keyType, let valueType): collect(keyType, insideCollectionElement: true) collect(valueType, insideCollectionElement: true) - return case .set(let elementType): collect(elementType, insideCollectionElement: true) - return case .optional(let wrappedType): collect(wrappedType, insideCollectionElement: insideCollectionElement) - return + if insideCollectionElement { + logger.warning("\(knownType) is not boxable supported yet!") + } case .array(let elementType): collect(elementType, insideCollectionElement: false) - return case .foundationData, .essentialsData, .foundationDate, .essentialsDate, .foundationUUID, .essentialsUUID: - break + if insideCollectionElement { + logger.warning("\(knownType) is not boxable supported yet!") + } default: // Other known types should be treatable as JavaBoxable. - return + break } + return } - guard !nominalType.isSwiftJavaWrapper else { + if nominalType.isSwiftJavaWrapper { return } + if insideCollectionElement { guard let importedType = analysis.importedTypes[nominalType.nominalTypeDecl.qualifiedName] else { return @@ -973,20 +974,9 @@ extension JNISwift2JavaGenerator { private func printNominalJavaBoxableExtension(_ printer: inout CodePrinter, _ type: ImportedNominalType) { let swiftTypeName = type.effectiveSwiftTypeName let cacheName = nominalJavaBoxingCacheName(for: type) - let javaClassName = "\(javaPackage).\(type.effectiveJavaTypeName.jniEscapedName)" let isEffectivelyGeneric = type.swiftNominal.isGeneric && type.effectiveJavaTypeName == type.swiftNominal.qualifiedTypeName - printer.printBraceBlock("extension \(swiftTypeName): JavaValue, JavaBoxable") { printer in - printer.print("public typealias JNIType = jobject?") - printer.print("public static var jvalueKeyPath: WritableKeyPath { \\.l }") - printer.print(#"public static var javaType: JavaType { JavaType(className: "\#(javaClassName)") }"#) - printer.println() - printer.printBraceBlock( - "public func getJNIValue(in environment: JNIEnvironment) -> JNIType" - ) { printer in - printer.print("toJavaObject(in: environment)") - } - printer.println() + printer.printBraceBlock("extension \(swiftTypeName): JavaBoxable") { printer in printer.printBraceBlock("public func toJavaObject(in environment: JNIEnvironment) -> jobject?") { printer in printer.print("let selfPointer$ = UnsafeMutablePointer.allocate(capacity: 1)") printer.print("selfPointer$.initialize(to: self)") @@ -1013,12 +1003,6 @@ extension JNISwift2JavaGenerator { ) } printer.println() - printer.printBraceBlock( - "public init(fromJNI value: JNIType, in environment: JNIEnvironment)" - ) { printer in - printer.print("self = Self.fromJavaObject(value, in: environment)") - } - printer.println() printer.printBraceBlock("public static func fromJavaObject(_ obj: jobject?, in environment: JNIEnvironment) -> Self") { printer in printer.print( """ @@ -1039,83 +1023,6 @@ extension JNISwift2JavaGenerator { """ ) } - printer.println() - printer.printBraceBlock( - "public static func jniMethodCall(in environment: JNIEnvironment) -> JNIMethodCall" - ) { printer in - printer.print("environment.interface.CallObjectMethodA") - } - printer.println() - printer.printBraceBlock( - "public static func jniFieldGet(in environment: JNIEnvironment) -> JNIFieldGet" - ) { printer in - printer.print("environment.interface.GetObjectField") - } - printer.println() - printer.printBraceBlock( - "public static func jniFieldSet(in environment: JNIEnvironment) -> JNIFieldSet" - ) { printer in - printer.print("environment.interface.SetObjectField") - } - printer.println() - printer.printBraceBlock( - "public static func jniStaticMethodCall(in environment: JNIEnvironment) -> JNIStaticMethodCall" - ) { printer in - printer.print("environment.interface.CallStaticObjectMethodA") - } - printer.println() - printer.printBraceBlock( - "public static func jniStaticFieldGet(in environment: JNIEnvironment) -> JNIStaticFieldGet" - ) { printer in - printer.print("environment.interface.GetStaticObjectField") - } - printer.println() - printer.printBraceBlock( - "public static func jniStaticFieldSet(in environment: JNIEnvironment) -> JNIStaticFieldSet" - ) { printer in - printer.print("environment.interface.SetStaticObjectField") - } - printer.println() - printer.printBraceBlock( - "public static func jniNewArray(in environment: JNIEnvironment) -> JNINewArray" - ) { printer in - printer.print("{ environment, size in environment.interface.NewObjectArray(environment, size, \(cacheName).javaClass, nil) }") - } - printer.println() - printer.printBraceBlock( - "public static func jniGetArrayRegion(in environment: JNIEnvironment) -> JNIGetArrayRegion" - ) { printer in - printer.print( - """ - { environment, array, start, length, outPointer in - let buffer = UnsafeMutableBufferPointer(start: outPointer, count: Int(length)) - for i in start.. JNISetArrayRegion" - ) { printer in - printer.print( - """ - { environment, array, start, length, outPointer in - let buffer = UnsafeBufferPointer(start: outPointer, count: Int(length)) - for i in start.. jobject? { - switch self { - case let value?: value.toJavaObject(in: environment) - case nil: nil - } - } - - public static func fromJavaObject(_ obj: jobject?, in environment: SwiftJavaJNICore.JNIEnvironment) -> Optional { - if let obj { - return Wrapped.fromJavaObject(obj, in: environment) - } else { - return nil - } - } -} From 9d2ff60c49eefe113668fc43ca438568d02f13e0 Mon Sep 17 00:00:00 2001 From: Iceman Date: Fri, 8 May 2026 16:09:28 +0900 Subject: [PATCH 08/25] Nested array support --- .../MySwiftLibrary/CollectionBoxable.swift | 14 ++ .../example/swift/CollectionBoxableTest.java | 18 ++ .../Collection+JavaBoxable.swift | 157 ++---------------- 3 files changed, 43 insertions(+), 146 deletions(-) diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/CollectionBoxable.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/CollectionBoxable.swift index 06848f30e..6ea6fc410 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/CollectionBoxable.swift +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/CollectionBoxable.swift @@ -50,3 +50,17 @@ public func makeMyIDToFish() -> [MyID: Fish] { 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 makeArrayInDictionary() -> [String: [Int32]] { + [ + "even": [0, 2, 4], + "odd": [1, 3, 5], + ] +} diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/CollectionBoxableTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/CollectionBoxableTest.java index de6355736..e179b51d6 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/CollectionBoxableTest.java +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/CollectionBoxableTest.java @@ -86,4 +86,22 @@ void makeSpecializedGenericTypeSet() { ); } } + + @Test + void makeSetInDictionary() { + try (var arena = SwiftArena.ofConfined()) { + SwiftDictionaryMap> dict = MySwiftLibrary.makeSetInDictionary(arena); + assertEquals(dict.get("even").toJavaSet(), Set.of(0, 2, 4)); + assertNull(dict.get("unknown")); + } + } + + @Test + void makeArrayInDictionary() { + try (var arena = SwiftArena.ofConfined()) { + SwiftDictionaryMap dict = MySwiftLibrary.makeArrayInDictionary(arena); + assertEquals(dict.get("even").toJavaSet(), Set.of(0, 2, 4)); + assertNull(dict.get("unknown")); + } + } } diff --git a/Sources/SwiftJavaRuntimeSupport/Collection+JavaBoxable.swift b/Sources/SwiftJavaRuntimeSupport/Collection+JavaBoxable.swift index f7776b297..b7a5443f5 100644 --- a/Sources/SwiftJavaRuntimeSupport/Collection+JavaBoxable.swift +++ b/Sources/SwiftJavaRuntimeSupport/Collection+JavaBoxable.swift @@ -14,19 +14,20 @@ import SwiftJava -extension Dictionary: @retroactive JavaValue, JavaBoxable where Key: JavaBoxable & Hashable, Value: JavaBoxable { - public typealias JNIType = jobject? - - public static var jvalueKeyPath: WritableKeyPath { \.l } - - public static var javaType: JavaType { - JavaType(className: "org.swift.swiftkit.core.collections.SwiftDictionaryMap") +extension Array: JavaBoxable where Element: JavaValue { + public func toJavaObject(in environment: JNIEnvironment) -> jobject? { + getJNIValue(in: environment) } - public func getJNIValue(in environment: JNIEnvironment) -> JNIType { - toJavaObject(in: environment) + public static func fromJavaObject(_ obj: jobject?, in environment: JNIEnvironment) -> Self { + guard let obj else { + fatalError("Array.fromJavaObject received a null Java object") + } + return Self(fromJNI: obj, in: environment) } +} +extension Dictionary: JavaBoxable where Key: JavaBoxable & Hashable, Value: JavaBoxable { public func toJavaObject(in environment: JNIEnvironment) -> jobject? { let selfPointer = self.dictionaryGetJNIValue(in: environment) var args = [jvalue(), jvalue()] @@ -52,83 +53,9 @@ extension Dictionary: @retroactive JavaValue, JavaBoxable where Key: JavaBoxable ) return Self(fromJNI: selfPointer, in: environment) } - - public init(fromJNI value: JNIType, in environment: JNIEnvironment) { - self = Self.fromJavaObject(value, in: environment) - } - - public static func jniMethodCall(in environment: JNIEnvironment) -> JNIMethodCall { - environment.interface.CallObjectMethodA - } - - public static func jniFieldGet(in environment: JNIEnvironment) -> JNIFieldGet { - environment.interface.GetObjectField - } - - public static func jniFieldSet(in environment: JNIEnvironment) -> JNIFieldSet { - environment.interface.SetObjectField - } - - public static func jniStaticMethodCall(in environment: JNIEnvironment) -> JNIStaticMethodCall { - environment.interface.CallStaticObjectMethodA - } - - public static func jniStaticFieldGet(in environment: JNIEnvironment) -> JNIStaticFieldGet { - environment.interface.GetStaticObjectField - } - - public static func jniStaticFieldSet(in environment: JNIEnvironment) -> JNIStaticFieldSet { - environment.interface.SetStaticObjectField - } - - public static func jniNewArray(in environment: JNIEnvironment) -> JNINewArray { - { environment, size in - environment.interface.NewObjectArray( - environment, - size, - _JNIMethodIDCache.SwiftDictionaryMap.class, - nil - ) - } - } - - public static func jniGetArrayRegion(in environment: JNIEnvironment) -> JNIGetArrayRegion { - { environment, array, start, length, outPointer in - let buffer = UnsafeMutableBufferPointer(start: outPointer, count: Int(length)) - for i in start.. JNISetArrayRegion { - { environment, array, start, length, outPointer in - let buffer = UnsafeBufferPointer(start: outPointer, count: Int(length)) - for i in start.. { \.l } - - public static var javaType: JavaType { - JavaType(className: "org.swift.swiftkit.core.collections.SwiftSet") - } - - public func getJNIValue(in environment: JNIEnvironment) -> JNIType { - toJavaObject(in: environment) - } - +extension Set: JavaBoxable where Element: JavaBoxable & Hashable { public func toJavaObject(in environment: JNIEnvironment) -> jobject? { let selfPointer = self.setGetJNIValue(in: environment) var args = [jvalue(), jvalue()] @@ -154,66 +81,4 @@ extension Set: @retroactive JavaValue, JavaBoxable where Element: JavaBoxable & ) return Self(fromJNI: selfPointer, in: environment) } - - public init(fromJNI value: JNIType, in environment: JNIEnvironment) { - self = Self.fromJavaObject(value, in: environment) - } - - public static func jniMethodCall(in environment: JNIEnvironment) -> JNIMethodCall { - environment.interface.CallObjectMethodA - } - - public static func jniFieldGet(in environment: JNIEnvironment) -> JNIFieldGet { - environment.interface.GetObjectField - } - - public static func jniFieldSet(in environment: JNIEnvironment) -> JNIFieldSet { - environment.interface.SetObjectField - } - - public static func jniStaticMethodCall(in environment: JNIEnvironment) -> JNIStaticMethodCall { - environment.interface.CallStaticObjectMethodA - } - - public static func jniStaticFieldGet(in environment: JNIEnvironment) -> JNIStaticFieldGet { - environment.interface.GetStaticObjectField - } - - public static func jniStaticFieldSet(in environment: JNIEnvironment) -> JNIStaticFieldSet { - environment.interface.SetStaticObjectField - } - - public static func jniNewArray(in environment: JNIEnvironment) -> JNINewArray { - { environment, size in - environment.interface.NewObjectArray( - environment, - size, - _JNIMethodIDCache.SwiftSet.class, - nil - ) - } - } - - public static func jniGetArrayRegion(in environment: JNIEnvironment) -> JNIGetArrayRegion { - { environment, array, start, length, outPointer in - let buffer = UnsafeMutableBufferPointer(start: outPointer, count: Int(length)) - for i in start.. JNISetArrayRegion { - { environment, array, start, length, outPointer in - let buffer = UnsafeBufferPointer(start: outPointer, count: Int(length)) - for i in start.. Date: Fri, 8 May 2026 16:18:17 +0900 Subject: [PATCH 09/25] Remove array support --- .../Sources/MySwiftLibrary/CollectionBoxable.swift | 7 ------- .../com/example/swift/CollectionBoxableTest.java | 11 +---------- .../JNISwift2JavaGenerator+SwiftThunkPrinting.swift | 3 +++ .../Collection+JavaBoxable.swift | 13 ------------- 4 files changed, 4 insertions(+), 30 deletions(-) diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/CollectionBoxable.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/CollectionBoxable.swift index 6ea6fc410..e8a5a9df0 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/CollectionBoxable.swift +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/CollectionBoxable.swift @@ -57,10 +57,3 @@ public func makeSetInDictionary() -> [String: Set] { "odd": [1, 3, 5], ] } - -public func makeArrayInDictionary() -> [String: [Int32]] { - [ - "even": [0, 2, 4], - "odd": [1, 3, 5], - ] -} diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/CollectionBoxableTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/CollectionBoxableTest.java index e179b51d6..de3fef1bd 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/CollectionBoxableTest.java +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/CollectionBoxableTest.java @@ -91,16 +91,7 @@ void makeSpecializedGenericTypeSet() { void makeSetInDictionary() { try (var arena = SwiftArena.ofConfined()) { SwiftDictionaryMap> dict = MySwiftLibrary.makeSetInDictionary(arena); - assertEquals(dict.get("even").toJavaSet(), Set.of(0, 2, 4)); - assertNull(dict.get("unknown")); - } - } - - @Test - void makeArrayInDictionary() { - try (var arena = SwiftArena.ofConfined()) { - SwiftDictionaryMap dict = MySwiftLibrary.makeArrayInDictionary(arena); - assertEquals(dict.get("even").toJavaSet(), Set.of(0, 2, 4)); + assertEquals(Set.of(0, 2, 4), dict.get("even").toJavaSet()); assertNull(dict.get("unknown")); } } diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift index 8cf1949de..849fc300c 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift @@ -864,6 +864,9 @@ extension JNISwift2JavaGenerator { } case .array(let elementType): collect(elementType, insideCollectionElement: false) + if insideCollectionElement { + logger.warning("\(knownType) is not boxable supported yet!") + } case .foundationData, .essentialsData, .foundationDate, .essentialsDate, .foundationUUID, .essentialsUUID: if insideCollectionElement { logger.warning("\(knownType) is not boxable supported yet!") diff --git a/Sources/SwiftJavaRuntimeSupport/Collection+JavaBoxable.swift b/Sources/SwiftJavaRuntimeSupport/Collection+JavaBoxable.swift index b7a5443f5..929618b07 100644 --- a/Sources/SwiftJavaRuntimeSupport/Collection+JavaBoxable.swift +++ b/Sources/SwiftJavaRuntimeSupport/Collection+JavaBoxable.swift @@ -14,19 +14,6 @@ import SwiftJava -extension Array: JavaBoxable where Element: JavaValue { - public func toJavaObject(in environment: JNIEnvironment) -> jobject? { - getJNIValue(in: environment) - } - - public static func fromJavaObject(_ obj: jobject?, in environment: JNIEnvironment) -> Self { - guard let obj else { - fatalError("Array.fromJavaObject received a null Java object") - } - return Self(fromJNI: obj, in: environment) - } -} - extension Dictionary: JavaBoxable where Key: JavaBoxable & Hashable, Value: JavaBoxable { public func toJavaObject(in environment: JNIEnvironment) -> jobject? { let selfPointer = self.dictionaryGetJNIValue(in: environment) From 09cedb6def7512636def69f7ca53ce3fa932b187 Mon Sep 17 00:00:00 2001 From: Iceman Date: Fri, 8 May 2026 16:25:34 +0900 Subject: [PATCH 10/25] slim test code --- .../JNI/JNICollectionBoxableTests.swift | 185 +----------------- 1 file changed, 6 insertions(+), 179 deletions(-) diff --git a/Tests/JExtractSwiftTests/JNI/JNICollectionBoxableTests.swift b/Tests/JExtractSwiftTests/JNI/JNICollectionBoxableTests.swift index 432818ac3..8833bf3d2 100644 --- a/Tests/JExtractSwiftTests/JNI/JNICollectionBoxableTests.swift +++ b/Tests/JExtractSwiftTests/JNI/JNICollectionBoxableTests.swift @@ -17,8 +17,8 @@ import Testing @Suite struct JNICollectionBoxableTests { - @Test("JNI generates JavaValue and JavaBoxable for dictionary element types") - func dictionaryCustomValueGeneratesJavaBoxingConformance() throws { + @Test("JNI generates JavaBoxable for dictionary element types") + func generatesJavaBoxableConformance() throws { try assertOutput( input: """ public struct ReefFish: Hashable {} @@ -28,12 +28,6 @@ struct JNICollectionBoxableTests { .swift, detectChunkByInitialLines: 1, expectedChunks: [ - """ - @_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: [Int: ReefFish](fromJNI: dict, in: environment)).dictionaryGetJNIValue(in: environment) - } - """, """ private enum _SwiftJavaBoxing_ReefFish { private static let wrapMemoryAddressUnsafeMethod = _JNIMethodIDCache.Method( @@ -41,17 +35,9 @@ struct JNICollectionBoxableTests { signature: "(J)Lcom/example/swift/ReefFish;", isStatic: true ) - ... - } """, """ - extension ReefFish: JavaValue, JavaBoxable { - public typealias JNIType = jobject? - public static var jvalueKeyPath: WritableKeyPath { \\.l } - public static var javaType: JavaType { JavaType(className: "com.example.swift.ReefFish") } - public func getJNIValue(in environment: JNIEnvironment) -> JNIType { - toJavaObject(in: environment) - } + extension ReefFish: JavaBoxable { public func toJavaObject(in environment: JNIEnvironment) -> jobject? { let selfPointer$ = UnsafeMutablePointer.allocate(capacity: 1) selfPointer$.initialize(to: self) @@ -82,169 +68,19 @@ struct JNICollectionBoxableTests { } return valuePointer$.pointee } - ... - public static var jniPlaceholderValue: JNIType { nil } - } - """, - ], - notExpectedChunks: ["private enum _SwiftJavaCollectionJavaBoxingCache"] - ) - } - - @Test("JNI generates JavaValue and JavaBoxable for set element types") - func setCustomElementGeneratesJavaBoxingConformance() throws { - try assertOutput( - input: """ - public struct ReefFish: Hashable {} - public func f(set: Set) -> Set {} - """, - .jni, - .swift, - detectChunkByInitialLines: 1, - expectedChunks: [ - """ - @_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) } """, - """ - extension ReefFish: JavaValue, JavaBoxable { - public typealias JNIType = jobject? - public static var jvalueKeyPath: WritableKeyPath { \\.l } - public static var javaType: JavaType { JavaType(className: "com.example.swift.ReefFish") } - public func getJNIValue(in environment: JNIEnvironment) -> JNIType { - toJavaObject(in: environment) - } - public func toJavaObject(in environment: JNIEnvironment) -> jobject? { - let selfPointer$ = UnsafeMutablePointer.allocate(capacity: 1) - selfPointer$.initialize(to: self) - let selfPointerBits$ = Int64(Int(bitPattern: selfPointer$)) - var args = [jvalue()] - args[0].j = selfPointerBits$.getJNIValue(in: environment) - return environment.interface.CallStaticObjectMethodA( - environment, - _SwiftJavaBoxing_ReefFish.javaClass, - _SwiftJavaBoxing_ReefFish.wrapMemoryAddressUnsafe, - &args - ) - } - ... - public static func fromJavaObject(_ obj: jobject?, in environment: JNIEnvironment) -> Self { - guard let obj else { - fatalError("ReefFish.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("ReefFish.fromJavaObject received a null Swift memory address") - } - return valuePointer$.pointee - } - ... - public static func jniNewArray(in environment: JNIEnvironment) -> JNINewArray { - { environment, size in environment.interface.NewObjectArray(environment, size, _SwiftJavaBoxing_ReefFish.javaClass, nil) } - } - ... - } - """, - ], - notExpectedChunks: ["private enum _SwiftJavaCollectionJavaBoxingCache"] - ) - } - - @Test("JNI supports optional collection values without generating optional boxing") - func dictionaryOptionalValueUsesRuntimeOptionalBoxing() throws { - try assertOutput( - input: """ - public struct ReefFish: Hashable {} - public func f(dict: [Int: ReefFish?]) -> [Int: ReefFish?] {} - """, - .jni, - .swift, - detectChunkByInitialLines: 1, - expectedChunks: [ - """ - @_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: [Int: ReefFish?](fromJNI: dict, in: environment)).dictionaryGetJNIValue(in: environment) - } - """, - """ - extension ReefFish: JavaValue, JavaBoxable { - ... - } - """, - ], - notExpectedChunks: [ - "extension Optional: JavaValue, JavaBoxable", - "extension ReefFish?: JavaValue, JavaBoxable", - ] - ) - } - - @Test("JNI uses runtime support cache for nested dictionary boxing") - func nestedDictionaryUsesRuntimeSupportCache() throws { - try assertOutput( - input: """ - public struct ReefFish: Hashable {} - public func f(dict: [String: [Int: ReefFish]]) -> [String: [Int: ReefFish]] {} - """, - .jni, - .swift, - detectChunkByInitialLines: 1, - expectedChunks: [ - """ - @_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: [Int: ReefFish]](fromJNI: dict, in: environment)).dictionaryGetJNIValue(in: environment) - } - """, - ], - notExpectedChunks: [ - "private enum _SwiftJavaCollectionJavaBoxingCache", - "extension [Int: ReefFish]: JavaValue, JavaBoxable", ] ) } - @Test("JNI uses runtime support cache for nested set boxing") - func nestedSetUsesRuntimeSupportCache() throws { - try assertOutput( - input: """ - public struct ReefFish: Hashable {} - public func f(dict: [String: Set]) -> [String: Set] {} - """, - .jni, - .swift, - detectChunkByInitialLines: 1, - expectedChunks: [ - """ - @_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: Set](fromJNI: dict, in: environment)).dictionaryGetJNIValue(in: environment) - } - """, - ], - notExpectedChunks: [ - "private enum _SwiftJavaCollectionJavaBoxingCache", - "extension Set: JavaValue, JavaBoxable", - ] - ) - } @Test("JNI generates JavaBoxable for generic dictionary keys") - func genericDictionaryKeyGeneratesJavaBoxingConformance() throws { + func generatesJavaBoxableConformanceForGenericType() throws { try assertOutput( input: """ - public struct Fish: Hashable {} public struct MyID: Hashable {} - public func f() -> [MyID: Fish] {} + public func f() -> [MyID: String] {} """, .jni, .swift, @@ -257,17 +93,9 @@ struct JNICollectionBoxableTests { signature: "(JJ)Lcom/example/swift/MyID;", isStatic: true ) - ... - } """, """ - extension MyID: JavaValue, JavaBoxable { - public typealias JNIType = jobject? - public static var jvalueKeyPath: WritableKeyPath { \\.l } - public static var javaType: JavaType { JavaType(className: "com.example.swift.MyID") } - public func getJNIValue(in environment: JNIEnvironment) -> JNIType { - toJavaObject(in: environment) - } + extension MyID: JavaBoxable { public func toJavaObject(in environment: JNIEnvironment) -> jobject? { let selfPointer$ = UnsafeMutablePointer.allocate(capacity: 1) selfPointer$.initialize(to: self) @@ -301,7 +129,6 @@ struct JNICollectionBoxableTests { } return valuePointer$.pointee } - ... } """, ] From 8f8b1b4631623a99a7c1b949cd4f45d456eed951 Mon Sep 17 00:00:00 2001 From: Iceman Date: Wed, 13 May 2026 16:43:00 +0900 Subject: [PATCH 11/25] Add javaBoxClass support --- .../JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift | 4 ++++ .../SwiftJavaRuntimeSupport/Collection+JavaBoxable.swift | 8 ++++++++ 2 files changed, 12 insertions(+) diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift index 64e5a8c3b..485793388 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift @@ -906,6 +906,10 @@ extension JNISwift2JavaGenerator { let isEffectivelyGeneric = type.swiftNominal.isGeneric && type.effectiveJavaTypeName == type.swiftNominal.qualifiedTypeName printer.printBraceBlock("extension \(swiftTypeName): JavaBoxable") { printer in + printer.printBraceBlock("public static var javaBoxClass: jclass") { printer in + printer.print("\(cacheName).javaClass") + } + printer.println() printer.printBraceBlock("public func toJavaObject(in environment: JNIEnvironment) -> jobject?") { printer in printer.print("let selfPointer$ = UnsafeMutablePointer.allocate(capacity: 1)") printer.print("selfPointer$.initialize(to: self)") diff --git a/Sources/SwiftJavaRuntimeSupport/Collection+JavaBoxable.swift b/Sources/SwiftJavaRuntimeSupport/Collection+JavaBoxable.swift index 929618b07..7075e1ce7 100644 --- a/Sources/SwiftJavaRuntimeSupport/Collection+JavaBoxable.swift +++ b/Sources/SwiftJavaRuntimeSupport/Collection+JavaBoxable.swift @@ -15,6 +15,10 @@ import SwiftJava extension Dictionary: JavaBoxable where Key: JavaBoxable & Hashable, Value: JavaBoxable { + public static var javaBoxClass: jclass { + _JNIMethodIDCache.SwiftDictionaryMap.class + } + public func toJavaObject(in environment: JNIEnvironment) -> jobject? { let selfPointer = self.dictionaryGetJNIValue(in: environment) var args = [jvalue(), jvalue()] @@ -43,6 +47,10 @@ extension Dictionary: JavaBoxable where Key: JavaBoxable & Hashable, Value: Java } extension Set: JavaBoxable where Element: JavaBoxable & Hashable { + public static var javaBoxClass: jclass { + _JNIMethodIDCache.SwiftSet.class + } + public func toJavaObject(in environment: JNIEnvironment) -> jobject? { let selfPointer = self.setGetJNIValue(in: environment) var args = [jvalue(), jvalue()] From 37e19f92a5c02f4ccad8fa2c0858312533e6f42f Mon Sep 17 00:00:00 2001 From: Iceman Date: Thu, 14 May 2026 09:37:21 +0900 Subject: [PATCH 12/25] Use default arena explicitly --- ...ift2JavaGenerator+SwiftThunkPrinting.swift | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift index a10d78ad5..9ce422537 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift @@ -880,7 +880,11 @@ extension JNISwift2JavaGenerator { let cacheName = nominalJavaBoxingCacheName(for: type) let jniClassName = "\(javaPackagePath)/\(type.effectiveJavaTypeName.jniEscapedName)" let isEffectivelyGeneric = type.swiftNominal.isGeneric && type.effectiveJavaTypeName == type.swiftNominal.qualifiedTypeName - let signature = isEffectivelyGeneric ? "(JJ)L\(jniClassName);" : "(J)L\(jniClassName);" + 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( @@ -921,15 +925,18 @@ extension JNISwift2JavaGenerator { printer.print("let selfPointer$ = UnsafeMutablePointer.allocate(capacity: 1)") printer.print("selfPointer$.initialize(to: self)") printer.print("let selfPointerBits$ = Int64(Int(bitPattern: selfPointer$))") + var args = [ + ("j", "selfPointerBits$.getJNIValue(in: environment)") + ] if isEffectivelyGeneric { printer.print("let selfTypePointer$ = unsafeBitCast(Self.self, to: UnsafeRawPointer.self)") printer.print("let selfTypePointerBits$ = Int64(Int(bitPattern: selfTypePointer$))") - printer.print("var args = [jvalue(), jvalue()]") - printer.print("args[0].j = selfPointerBits$.getJNIValue(in: environment)") - printer.print("args[1].j = selfTypePointerBits$.getJNIValue(in: environment)") - } else { - printer.print("var args = [jvalue()]") - printer.print("args[0].j = selfPointerBits$.getJNIValue(in: environment)") + args.append(("j", "selfTypePointerBits$.getJNIValue(in: environment)")) + } + args.append(("l", "JavaSwiftArena.defaultAutoArena.javaThis")) + printer.print("var args = [\(Array(repeating: "jvalue()", count: args.count).joined(separator: ", "))]") + for (i, (jvalueField, expr)) in args.enumerated() { + printer.print("args[\(i)].\(jvalueField) = \(expr)") } printer.print( """ From 5cdfc4f229c172c009584b573b8f8e75909bfa7f Mon Sep 17 00:00:00 2001 From: Iceman Date: Thu, 14 May 2026 11:29:29 +0900 Subject: [PATCH 13/25] print JavaBoxable extension for all types --- ...ift2JavaGenerator+SwiftThunkPrinting.swift | 137 ++---------------- 1 file changed, 15 insertions(+), 122 deletions(-) diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift index 9ce422537..06d1463ed 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift @@ -290,8 +290,6 @@ extension JNISwift2JavaGenerator { printSwiftFunctionThunk(&printer, decl) printer.println() } - - printCollectionJavaBoxableExtensions(&printer) } private func printNominalTypeThunks(_ printer: inout CodePrinter, _ type: ImportedNominalType) throws { @@ -346,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() + printNominalJavaBoxableExtension(&printer, type) + printer.println() + } + printSpecificTypeThunks(&printer, type) printTypeMetadataAddressThunk(&printer, type) printer.println() @@ -763,121 +769,8 @@ extension JNISwift2JavaGenerator { } } - private func printCollectionJavaBoxableExtensions(_ printer: inout CodePrinter) { - let boxableTypes = collectCollectionJavaBoxableTypes() - guard !boxableTypes.isEmpty else { - return - } - - for nominalType in boxableTypes { - printNominalJavaBoxableCache(&printer, nominalType) - printer.println() - printNominalJavaBoxableExtension(&printer, nominalType) - printer.println() - } - } - - private func collectCollectionJavaBoxableTypes() -> [ImportedNominalType] { - var nominalTypes: Set = [] - - func collect(_ type: SwiftType, insideCollectionElement: Bool = false) { - switch type { - case .nominal(let nominalType): - if let knownType = nominalType.asKnownType { - switch knownType { - case .dictionary(let keyType, let valueType): - collect(keyType, insideCollectionElement: true) - collect(valueType, insideCollectionElement: true) - case .set(let elementType): - collect(elementType, insideCollectionElement: true) - case .optional(let wrappedType): - collect(wrappedType, insideCollectionElement: insideCollectionElement) - if insideCollectionElement { - logger.warning("\(knownType) is not boxable supported yet!") - } - case .array(let elementType): - collect(elementType, insideCollectionElement: false) - if insideCollectionElement { - logger.warning("\(knownType) is not boxable supported yet!") - } - case .foundationData, .essentialsData, .foundationDate, .essentialsDate, .foundationUUID, .essentialsUUID: - if insideCollectionElement { - logger.warning("\(knownType) is not boxable supported yet!") - } - default: - // Other known types should be treatable as JavaBoxable. - break - } - return - } - - if nominalType.isSwiftJavaWrapper { - return - } - - if insideCollectionElement { - guard let importedType = analysis.importedTypes[nominalType.nominalTypeDecl.qualifiedName] else { - return - } - nominalTypes.insert(importedType) - } - - case .tuple(let elements): - for element in elements { - collect(element.type, insideCollectionElement: false) - } - - case .existential(let innerType), .opaque(let innerType), .metatype(let innerType): - collect(innerType, insideCollectionElement: false) - - case .function(let functionType): - for parameter in functionType.parameters { - collect(parameter.type, insideCollectionElement: false) - } - collect(functionType.resultType, insideCollectionElement: false) - - case .composite(let innerTypes): - for innerType in innerTypes { - collect(innerType, insideCollectionElement: false) - } - - case .genericParameter: - break - } - } - - for decl in analysis.importedGlobalFuncs { - for parameter in decl.functionSignature.parameters { - collect(parameter.type) - } - collect(decl.functionSignature.result.type) - } - - for decl in analysis.importedGlobalVariables { - for parameter in decl.functionSignature.parameters { - collect(parameter.type) - } - collect(decl.functionSignature.result.type) - } - - for importedType in analysis.importedTypes.values { - for decl in importedType.initializers + importedType.methods + importedType.variables { - for parameter in decl.functionSignature.parameters { - collect(parameter.type) - } - collect(decl.functionSignature.result.type) - } - } - - return nominalTypes.sorted { $0.effectiveSwiftTypeName < $1.effectiveSwiftTypeName } - } - - private func nominalJavaBoxingCacheName(for type: ImportedNominalType) -> String { - "_SwiftJavaBoxing_\(type.effectiveJavaTypeName.fullFlatName)" - } - - private func printNominalJavaBoxableCache(_ printer: inout CodePrinter, _ type: ImportedNominalType) { - let cacheName = nominalJavaBoxingCacheName(for: type) + 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 { @@ -913,7 +806,7 @@ extension JNISwift2JavaGenerator { private func printNominalJavaBoxableExtension(_ printer: inout CodePrinter, _ type: ImportedNominalType) { let swiftTypeName = type.effectiveSwiftTypeName - let cacheName = nominalJavaBoxingCacheName(for: type) + let cacheName = JNICaching.cacheName(for: type) let isEffectivelyGeneric = type.swiftNominal.isGeneric && type.effectiveJavaTypeName == type.swiftNominal.qualifiedTypeName printer.printBraceBlock("extension \(swiftTypeName): JavaBoxable") { printer in @@ -922,14 +815,14 @@ extension JNISwift2JavaGenerator { } printer.println() printer.printBraceBlock("public func toJavaObject(in environment: JNIEnvironment) -> jobject?") { printer in - printer.print("let selfPointer$ = UnsafeMutablePointer.allocate(capacity: 1)") + printer.print("let selfPointer$ = UnsafeMutablePointer<\(swiftTypeName)>.allocate(capacity: 1)") printer.print("selfPointer$.initialize(to: self)") printer.print("let selfPointerBits$ = Int64(Int(bitPattern: selfPointer$))") var args = [ ("j", "selfPointerBits$.getJNIValue(in: environment)") ] if isEffectivelyGeneric { - printer.print("let selfTypePointer$ = unsafeBitCast(Self.self, to: UnsafeRawPointer.self)") + printer.print("let selfTypePointer$ = unsafeBitCast(\(swiftTypeName).self, to: UnsafeRawPointer.self)") printer.print("let selfTypePointerBits$ = Int64(Int(bitPattern: selfTypePointer$))") args.append(("j", "selfTypePointerBits$.getJNIValue(in: environment)")) } @@ -950,7 +843,7 @@ extension JNISwift2JavaGenerator { ) } printer.println() - printer.printBraceBlock("public static func fromJavaObject(_ obj: jobject?, in environment: JNIEnvironment) -> Self") { printer in + printer.printBraceBlock("public static func fromJavaObject(_ obj: jobject?, in environment: JNIEnvironment) -> \(swiftTypeName)") { printer in printer.print( """ guard let obj else { @@ -963,7 +856,7 @@ extension JNISwift2JavaGenerator { nil ) let selfPointerBits$ = Int(Int64(fromJNI: selfPointer$, in: environment)) - guard let valuePointer$ = UnsafeMutablePointer(bitPattern: selfPointerBits$) else { + guard let valuePointer$ = UnsafeMutablePointer<\(swiftTypeName)>(bitPattern: selfPointerBits$) else { fatalError("\(swiftTypeName).fromJavaObject received a null Swift memory address") } return valuePointer$.pointee From e2790d2973410eb83158028aebe8dc35de35c482 Mon Sep 17 00:00:00 2001 From: Iceman Date: Thu, 14 May 2026 16:27:54 +0900 Subject: [PATCH 14/25] Use bridge type strategy --- Sources/JExtractSwiftLib/JNI/JNICaching.swift | 12 ++ ...wift2JavaGenerator+NativeTranslation.swift | 95 ++++++++++++++-- ...ift2JavaGenerator+SwiftThunkPrinting.swift | 40 ++++--- .../SwiftJava/BridgedValues/JavaBoxing.swift | 105 +++++++++++++++--- .../BridgedValues/JavaValue+Dictionary.swift | 41 ++++++- .../BridgedValues/JavaValue+Set.swift | 30 ++++- .../Collection+JavaBoxable.swift | 59 +++++++--- .../JNI/JNICollectionBoxableTests.swift | 74 +++++++----- .../JNI/JNIDictionaryTest.swift | 12 +- Tests/JExtractSwiftTests/JNI/JNISetTest.swift | 10 +- 10 files changed, 378 insertions(+), 100 deletions(-) diff --git a/Sources/JExtractSwiftLib/JNI/JNICaching.swift b/Sources/JExtractSwiftLib/JNI/JNICaching.swift index f91d7b7d4..a2e427f96 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 { + "_SwiftJavaBridge_\(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..9bf66f997 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift @@ -995,11 +995,23 @@ 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: .method( + .constant("\(swiftDictionaryType)"), + function: "init", + arguments: [ + ("fromJNI", .placeholder), + ("in", .constant("environment")), + ("keyBridge", .constant("\(keyBridgeType).self")), + ("valueBridge", .constant("\(valueBridgeType).self")), + ] + ), indirectConversion: nil, conversionCheck: nil ) @@ -1010,12 +1022,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 +1043,21 @@ 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: .method( + .constant("\(swiftSetType)"), + function: "init", + arguments: [ + ("fromJNI", .placeholder), + ("in", .constant("environment")), + ("elementBridge", .constant("\(elementBridgeType).self")), + ] + ), indirectConversion: nil, conversionCheck: nil ) @@ -1039,16 +1067,67 @@ 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 .dictionary(let key, let value): + return "JavaDictionaryBridge<\(try bridgeTypeName(for: key)), \(try bridgeTypeName(for: value))>" + case .set(let element): + return "JavaSetBridge<\(try bridgeTypeName(for: element))>" + default: + guard let javaType = JNIJavaTypeTranslator.translate(knownType: knownType.kind, config: self.config), + javaType.implementsJavaValue + else { + throw JavaTranslationError.unsupportedSwiftType(swiftType) + } + return "JavaBoxableBridge<\(swiftType)>" + } + } + + if nominalType.isSwiftJavaWrapper { + return "JavaObjectBridge<\(swiftType)>" + } + + let bridgeName = JNICaching.bridgeName(for: nominalType) + if nominalType.genericArguments.isEmpty { + return bridgeName + } + let genericArguments = try nominalType.genericArguments.map { try renderedBridgeNominalGenericArgument($0) } + return "\(bridgeName)<\(genericArguments.joined(separator: ", "))>" + + case .genericParameter: + throw JavaTranslationError.unsupportedSwiftType(swiftType) + + default: + throw JavaTranslationError.unsupportedSwiftType(swiftType) + } + } + + private func renderedBridgeNominalGenericArgument(_ swiftType: SwiftType) throws -> String { + switch swiftType { + case .nominal, .genericParameter: + return "\(swiftType)" + default: + throw JavaTranslationError.unsupportedSwiftType(swiftType) + } + } } struct NativeFunctionSignature { diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift index 06d1463ed..014f93e03 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift @@ -348,7 +348,7 @@ extension JNISwift2JavaGenerator { if !type.isSpecialization && !isNeverLike { printJNICache(&printer, type) printer.println() - printNominalJavaBoxableExtension(&printer, type) + printNominalJavaBridge(&printer, type) printer.println() } @@ -804,25 +804,39 @@ extension JNISwift2JavaGenerator { } } - private func printNominalJavaBoxableExtension(_ printer: inout CodePrinter, _ type: ImportedNominalType) { - let swiftTypeName = type.effectiveSwiftTypeName + 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.effectiveJavaTypeName == type.swiftNominal.qualifiedTypeName + let bridgeGenericClause = + if type.swiftNominal.genericParameters.isEmpty { + "" + } else { + "<\(type.swiftNominal.genericParameters.map { $0.syntax.trimmedDescription }.joined(separator: ", "))>" + } + let bridgedSwiftType = + if type.genericParameterNames.isEmpty { + type.effectiveSwiftTypeName + } else { + "\(type.baseTypeName)<\(type.genericParameterNames.joined(separator: ", "))>" + } - printer.printBraceBlock("extension \(swiftTypeName): JavaBoxable") { printer in - printer.printBraceBlock("public static var javaBoxClass: jclass") { printer in + printer.printBraceBlock("enum \(bridgeName)\(bridgeGenericClause): JavaClassBackedTypeBridge") { 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("public func toJavaObject(in environment: JNIEnvironment) -> jobject?") { printer in - printer.print("let selfPointer$ = UnsafeMutablePointer<\(swiftTypeName)>.allocate(capacity: 1)") - printer.print("selfPointer$.initialize(to: self)") + printer.printBraceBlock("static func toJavaObject(_ value: SwiftType, in environment: JNIEnvironment) -> jobject?") { printer in + printer.print("let selfPointer$ = UnsafeMutablePointer.allocate(capacity: 1)") + printer.print("selfPointer$.initialize(to: value)") printer.print("let selfPointerBits$ = Int64(Int(bitPattern: selfPointer$))") var args = [ ("j", "selfPointerBits$.getJNIValue(in: environment)") ] if isEffectivelyGeneric { - printer.print("let selfTypePointer$ = unsafeBitCast(\(swiftTypeName).self, to: UnsafeRawPointer.self)") + printer.print("let selfTypePointer$ = unsafeBitCast(SwiftType.self, to: UnsafeRawPointer.self)") printer.print("let selfTypePointerBits$ = Int64(Int(bitPattern: selfTypePointer$))") args.append(("j", "selfTypePointerBits$.getJNIValue(in: environment)")) } @@ -843,11 +857,11 @@ extension JNISwift2JavaGenerator { ) } printer.println() - printer.printBraceBlock("public static func fromJavaObject(_ obj: jobject?, in environment: JNIEnvironment) -> \(swiftTypeName)") { printer in + printer.printBraceBlock("static func fromJavaObject(_ obj: jobject?, in environment: JNIEnvironment) -> SwiftType") { printer in printer.print( """ guard let obj else { - fatalError("\(swiftTypeName).fromJavaObject received a null Java object") + fatalError("\(bridgedSwiftType).fromJavaObject received a null Java object") } let selfPointer$ = environment.interface.CallLongMethodA( environment, @@ -856,8 +870,8 @@ extension JNISwift2JavaGenerator { nil ) let selfPointerBits$ = Int(Int64(fromJNI: selfPointer$, in: environment)) - guard let valuePointer$ = UnsafeMutablePointer<\(swiftTypeName)>(bitPattern: selfPointerBits$) else { - fatalError("\(swiftTypeName).fromJavaObject received a null Swift memory address") + guard let valuePointer$ = UnsafeMutablePointer(bitPattern: selfPointerBits$) else { + fatalError("\(bridgedSwiftType).fromJavaObject received a null Swift memory address") } return valuePointer$.pointee """ diff --git a/Sources/SwiftJava/BridgedValues/JavaBoxing.swift b/Sources/SwiftJava/BridgedValues/JavaBoxing.swift index 4bb67582b..3874ba68b 100644 --- a/Sources/SwiftJava/BridgedValues/JavaBoxing.swift +++ b/Sources/SwiftJava/BridgedValues/JavaBoxing.swift @@ -14,6 +14,78 @@ import SwiftJavaJNICore +// ==== ----------------------------------------------------------------------- +// MARK: Explicit Java bridge protocols + +/// 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 JavaTypeBridge { + associatedtype SwiftType + + /// Returns whether the given Java object can be consumed by this bridge. + static func isJavaObject(_ obj: jobject?, in environment: JNIEnvironment) -> Bool + + /// 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 +} + +/// Convenience protocol for bridges backed by a specific Java class. +public protocol JavaClassBackedTypeBridge: JavaTypeBridge { + static var javaClass: jclass { get } +} + +extension JavaClassBackedTypeBridge { + public static func isJavaObject(_ obj: jobject?, in environment: JNIEnvironment) -> Bool { + guard let obj else { return false } + return environment.interface.IsInstanceOf(environment, obj, javaClass) == JNI_TRUE + } +} + +/// Adapter that reuses an existing `JavaBoxable` conformance as a `JavaTypeBridge`. +public enum JavaBoxableBridge: JavaClassBackedTypeBridge { + public typealias SwiftType = T + + public static var javaClass: jclass { + T.javaBoxClass + } + + 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) + } +} + +/// Adapter for Swift projections of Java reference types. +public enum JavaObjectBridge: JavaTypeBridge { + public typealias SwiftType = T + + public static func isJavaObject(_ obj: jobject?, in environment: JNIEnvironment) -> Bool { + guard let obj else { return false } + return (try? T.withJNIClass(in: environment) { cls in + environment.interface.IsInstanceOf(environment, obj, cls) == JNI_TRUE + }) ?? false + } + + 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) + } +} + // ==== ----------------------------------------------------------------------- // MARK: JavaBoxable protocol @@ -329,10 +401,11 @@ 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 { + let dictionary: [KeyBridge.SwiftType: ValueBridge.SwiftType] - init(_ dictionary: [K: V]) { + init(_ dictionary: [KeyBridge.SwiftType: ValueBridge.SwiftType]) { self.dictionary = dictionary } @@ -345,19 +418,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 +439,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 +450,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 +473,10 @@ 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 { + let set: Set - init(_ set: Set) { + init(_ set: Set) { self.set = set } @@ -416,10 +489,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 +501,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..067ae879a 100644 --- a/Sources/SwiftJava/BridgedValues/JavaValue+Dictionary.swift +++ b/Sources/SwiftJava/BridgedValues/JavaValue+Dictionary.swift @@ -17,20 +17,51 @@ import SwiftJavaJNICore // ==== ----------------------------------------------------------------------- // MARK: Dictionary extension for JNI bridging -extension Dictionary where Key: JavaBoxable & Hashable, Value: JavaBoxable { +extension Dictionary where Key: Hashable { /// 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 } } + +extension Dictionary where Key: JavaBoxable & Hashable, Value: JavaBoxable { + /// 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 { + dictionaryGetJNIValue( + in: environment, + keyBridge: JavaBoxableBridge.self, + valueBridge: JavaBoxableBridge.self + ) + } + + /// Reconstruct a Swift dictionary from a JNI jlong pointer to a SwiftDictionaryBox. + public init(fromJNI value: jlong, in environment: JNIEnvironment) { + self.init( + fromJNI: value, + in: environment, + keyBridge: JavaBoxableBridge.self, + valueBridge: JavaBoxableBridge.self + ) + } +} diff --git a/Sources/SwiftJava/BridgedValues/JavaValue+Set.swift b/Sources/SwiftJava/BridgedValues/JavaValue+Set.swift index 2adb7d381..fce03d7f4 100644 --- a/Sources/SwiftJava/BridgedValues/JavaValue+Set.swift +++ b/Sources/SwiftJava/BridgedValues/JavaValue+Set.swift @@ -17,20 +17,40 @@ import SwiftJavaJNICore // ==== ----------------------------------------------------------------------- // MARK: Set extension for JNI bridging -extension Set where Element: JavaBoxable & Hashable { +extension Set where Element: Hashable { /// 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 } } + +extension Set where Element: JavaBoxable & Hashable { + /// 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 { + setGetJNIValue(in: environment, elementBridge: JavaBoxableBridge.self) + } + + /// Reconstruct a Swift set from a JNI jlong pointer to a SwiftSetBox. + public init(fromJNI value: jlong, in environment: JNIEnvironment) { + self.init(fromJNI: value, in: environment, elementBridge: JavaBoxableBridge.self) + } +} diff --git a/Sources/SwiftJavaRuntimeSupport/Collection+JavaBoxable.swift b/Sources/SwiftJavaRuntimeSupport/Collection+JavaBoxable.swift index 7075e1ce7..40515747f 100644 --- a/Sources/SwiftJavaRuntimeSupport/Collection+JavaBoxable.swift +++ b/Sources/SwiftJavaRuntimeSupport/Collection+JavaBoxable.swift @@ -14,13 +14,16 @@ import SwiftJava -extension Dictionary: JavaBoxable where Key: JavaBoxable & Hashable, Value: JavaBoxable { - public static var javaBoxClass: jclass { +public enum JavaDictionaryBridge: JavaClassBackedTypeBridge +where KeyBridge.SwiftType: Hashable { + public typealias SwiftType = [KeyBridge.SwiftType: ValueBridge.SwiftType] + + public static var javaClass: jclass { _JNIMethodIDCache.SwiftDictionaryMap.class } - - public func toJavaObject(in environment: JNIEnvironment) -> jobject? { - let selfPointer = self.dictionaryGetJNIValue(in: environment) + + 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 @@ -32,7 +35,7 @@ extension Dictionary: JavaBoxable where Key: JavaBoxable & Hashable, Value: Java ) } - public static func fromJavaObject(_ obj: jobject?, in environment: JNIEnvironment) -> Self { + public static func fromJavaObject(_ obj: jobject?, in environment: JNIEnvironment) -> SwiftType { guard let obj else { fatalError("Dictionary.fromJavaObject received a null Java object") } @@ -42,17 +45,19 @@ extension Dictionary: JavaBoxable where Key: JavaBoxable & Hashable, Value: Java _JNIMethodIDCache.JNISwiftInstance.memoryAddress, nil ) - return Self(fromJNI: selfPointer, in: environment) + return SwiftType(fromJNI: selfPointer, in: environment, keyBridge: KeyBridge.self, valueBridge: ValueBridge.self) } } -extension Set: JavaBoxable where Element: JavaBoxable & Hashable { - public static var javaBoxClass: jclass { +public enum JavaSetBridge: JavaClassBackedTypeBridge where ElementBridge.SwiftType: Hashable { + public typealias SwiftType = Set + + public static var javaClass: jclass { _JNIMethodIDCache.SwiftSet.class } - public func toJavaObject(in environment: JNIEnvironment) -> jobject? { - let selfPointer = self.setGetJNIValue(in: environment) + 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 @@ -64,7 +69,7 @@ extension Set: JavaBoxable where Element: JavaBoxable & Hashable { ) } - public static func fromJavaObject(_ obj: jobject?, in environment: JNIEnvironment) -> Self { + public static func fromJavaObject(_ obj: jobject?, in environment: JNIEnvironment) -> SwiftType { guard let obj else { fatalError("Set.fromJavaObject received a null Java object") } @@ -74,6 +79,34 @@ extension Set: JavaBoxable where Element: JavaBoxable & Hashable { _JNIMethodIDCache.JNISwiftInstance.memoryAddress, nil ) - return Self(fromJNI: selfPointer, in: environment) + return SwiftType(fromJNI: selfPointer, in: environment, elementBridge: ElementBridge.self) + } +} + +extension Dictionary: JavaBoxable where Key: JavaBoxable & Hashable, Value: JavaBoxable { + public static var javaBoxClass: jclass { + _JNIMethodIDCache.SwiftDictionaryMap.class + } + + public func toJavaObject(in environment: JNIEnvironment) -> jobject? { + JavaDictionaryBridge, JavaBoxableBridge>.toJavaObject(self, in: environment) + } + + public static func fromJavaObject(_ obj: jobject?, in environment: JNIEnvironment) -> Self { + JavaDictionaryBridge, JavaBoxableBridge>.fromJavaObject(obj, in: environment) + } +} + +extension Set: JavaBoxable where Element: JavaBoxable & Hashable { + public static var javaBoxClass: jclass { + _JNIMethodIDCache.SwiftSet.class + } + + public func toJavaObject(in environment: JNIEnvironment) -> jobject? { + JavaSetBridge>.toJavaObject(self, in: environment) + } + + public static func fromJavaObject(_ obj: jobject?, in environment: JNIEnvironment) -> Self { + JavaSetBridge>.fromJavaObject(obj, in: environment) } } diff --git a/Tests/JExtractSwiftTests/JNI/JNICollectionBoxableTests.swift b/Tests/JExtractSwiftTests/JNI/JNICollectionBoxableTests.swift index 8833bf3d2..716d68d3c 100644 --- a/Tests/JExtractSwiftTests/JNI/JNICollectionBoxableTests.swift +++ b/Tests/JExtractSwiftTests/JNI/JNICollectionBoxableTests.swift @@ -17,8 +17,8 @@ import Testing @Suite struct JNICollectionBoxableTests { - @Test("JNI generates JavaBoxable for dictionary element types") - func generatesJavaBoxableConformance() throws { + @Test("JNI generates explicit bridges for dictionary element types") + func generatesBridgeDeclaration() throws { try assertOutput( input: """ public struct ReefFish: Hashable {} @@ -29,30 +29,35 @@ struct JNICollectionBoxableTests { detectChunkByInitialLines: 1, expectedChunks: [ """ - private enum _SwiftJavaBoxing_ReefFish { + private enum _JNI_ReefFish { private static let wrapMemoryAddressUnsafeMethod = _JNIMethodIDCache.Method( name: "wrapMemoryAddressUnsafe", - signature: "(J)Lcom/example/swift/ReefFish;", + signature: "(JLorg/swift/swiftkit/core/SwiftArena;)Lcom/example/swift/ReefFish;", isStatic: true ) """, """ - extension ReefFish: JavaBoxable { - public func toJavaObject(in environment: JNIEnvironment) -> jobject? { - let selfPointer$ = UnsafeMutablePointer.allocate(capacity: 1) - selfPointer$.initialize(to: self) + enum _SwiftJavaBridge_ReefFish: JavaClassBackedTypeBridge { + typealias SwiftType = ReefFish + static var javaClass: jclass { + _JNI_ReefFish.javaClass + } + 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()] + var args = [jvalue(), jvalue()] args[0].j = selfPointerBits$.getJNIValue(in: environment) + args[1].l = JavaSwiftArena.defaultAutoArena.javaThis return environment.interface.CallStaticObjectMethodA( environment, - _SwiftJavaBoxing_ReefFish.javaClass, - _SwiftJavaBoxing_ReefFish.wrapMemoryAddressUnsafe, + _JNI_ReefFish.javaClass, + _JNI_ReefFish.wrapMemoryAddressUnsafe, &args ) } ... - public static func fromJavaObject(_ obj: jobject?, in environment: JNIEnvironment) -> Self { + static func fromJavaObject(_ obj: jobject?, in environment: JNIEnvironment) -> SwiftType { guard let obj else { fatalError("ReefFish.fromJavaObject received a null Java object") } @@ -63,20 +68,23 @@ struct JNICollectionBoxableTests { nil ) let selfPointerBits$ = Int(Int64(fromJNI: selfPointer$, in: environment)) - guard let valuePointer$ = UnsafeMutablePointer(bitPattern: selfPointerBits$) else { + guard let valuePointer$ = UnsafeMutablePointer(bitPattern: selfPointerBits$) else { fatalError("ReefFish.fromJavaObject received a null Swift memory address") } return valuePointer$.pointee } } """, + """ + return SwiftModule.f(dict: [Int: ReefFish].init(fromJNI: dict, in: environment, keyBridge: JavaBoxableBridge.self, valueBridge: _SwiftJavaBridge_ReefFish.self)).dictionaryGetJNIValue(in: environment, keyBridge: JavaBoxableBridge.self, valueBridge: _SwiftJavaBridge_ReefFish.self) + """, ] ) } - @Test("JNI generates JavaBoxable for generic dictionary keys") - func generatesJavaBoxableConformanceForGenericType() throws { + @Test("JNI generates explicit bridges for generic dictionary keys") + func generatesBridgeDeclarationForGenericType() throws { try assertOutput( input: """ public struct MyID: Hashable {} @@ -87,35 +95,40 @@ struct JNICollectionBoxableTests { detectChunkByInitialLines: 1, expectedChunks: [ """ - private enum _SwiftJavaBoxing_MyID { + private enum _JNI_MyID { private static let wrapMemoryAddressUnsafeMethod = _JNIMethodIDCache.Method( name: "wrapMemoryAddressUnsafe", - signature: "(JJ)Lcom/example/swift/MyID;", + signature: "(JJLorg/swift/swiftkit/core/SwiftArena;)Lcom/example/swift/MyID;", isStatic: true ) """, """ - extension MyID: JavaBoxable { - public func toJavaObject(in environment: JNIEnvironment) -> jobject? { - let selfPointer$ = UnsafeMutablePointer.allocate(capacity: 1) - selfPointer$.initialize(to: self) + enum _SwiftJavaBridge_MyID: JavaClassBackedTypeBridge { + typealias SwiftType = MyID + static var javaClass: jclass { + _JNI_MyID.javaClass + } + 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(Self.self, to: UnsafeRawPointer.self) + let selfTypePointer$ = unsafeBitCast(SwiftType.self, to: UnsafeRawPointer.self) let selfTypePointerBits$ = Int64(Int(bitPattern: selfTypePointer$)) - var args = [jvalue(), jvalue()] + 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, - _SwiftJavaBoxing_MyID.javaClass, - _SwiftJavaBoxing_MyID.wrapMemoryAddressUnsafe, + _JNI_MyID.javaClass, + _JNI_MyID.wrapMemoryAddressUnsafe, &args ) } ... - public static func fromJavaObject(_ obj: jobject?, in environment: JNIEnvironment) -> Self { + static func fromJavaObject(_ obj: jobject?, in environment: JNIEnvironment) -> SwiftType { guard let obj else { - fatalError("MyID.fromJavaObject received a null Java object") + fatalError("MyID.fromJavaObject received a null Java object") } let selfPointer$ = environment.interface.CallLongMethodA( environment, @@ -124,13 +137,16 @@ struct JNICollectionBoxableTests { nil ) let selfPointerBits$ = Int(Int64(fromJNI: selfPointer$, in: environment)) - guard let valuePointer$ = UnsafeMutablePointer(bitPattern: selfPointerBits$) else { - fatalError("MyID.fromJavaObject received a null Swift memory address") + guard let valuePointer$ = UnsafeMutablePointer(bitPattern: selfPointerBits$) else { + fatalError("MyID.fromJavaObject received a null Swift memory address") } return valuePointer$.pointee } } """, + """ + return SwiftModule.f().dictionaryGetJNIValue(in: environment, keyBridge: _SwiftJavaBridge_MyID.self, valueBridge: JavaBoxableBridge.self) + """, ] ) } diff --git a/Tests/JExtractSwiftTests/JNI/JNIDictionaryTest.swift b/Tests/JExtractSwiftTests/JNI/JNIDictionaryTest.swift index ebb8ed163..eaa233d44 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].init(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].init(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].init(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].init(fromJNI: a, in: environment, keyBridge: JavaBoxableBridge.self, valueBridge: JavaBoxableBridge.self), b: [String: Bool].init(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].init(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/JNISetTest.swift b/Tests/JExtractSwiftTests/JNI/JNISetTest.swift index 8adec8d58..655994067 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.init(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.init(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.init(fromJNI: a, in: environment, elementBridge: JavaBoxableBridge.self), b: Set.init(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.init(fromJNI: set, in: environment, elementBridge: JavaBoxableBridge.self), element: String(fromJNI: element, in: environment)).setGetJNIValue(in: environment, elementBridge: JavaBoxableBridge.self) } """ ] From 153fd1d7560caa4a9ead5d49d79997d5209d436f Mon Sep 17 00:00:00 2001 From: Iceman Date: Thu, 14 May 2026 17:34:32 +0900 Subject: [PATCH 15/25] Simplified Implementation --- Sources/JExtractSwiftLib/JNI/JNICaching.swift | 2 +- ...wift2JavaGenerator+NativeTranslation.swift | 49 +++++------ ...ift2JavaGenerator+SwiftThunkPrinting.swift | 56 ++----------- .../SwiftJava/BridgedValues/JavaBoxing.swift | 72 ----------------- .../BridgedValues/JavaTypeBridge.swift | 69 ++++++++++++++++ .../Collection+JavaBoxable.swift | 15 ++-- .../JextractedTypeBridge.swift | 81 +++++++++++++++++++ .../JNI/JNICollectionBoxableTests.swift | 4 +- 8 files changed, 193 insertions(+), 155 deletions(-) create mode 100644 Sources/SwiftJava/BridgedValues/JavaTypeBridge.swift create mode 100644 Sources/SwiftJavaRuntimeSupport/JextractedTypeBridge.swift diff --git a/Sources/JExtractSwiftLib/JNI/JNICaching.swift b/Sources/JExtractSwiftLib/JNI/JNICaching.swift index a2e427f96..4ce16af9a 100644 --- a/Sources/JExtractSwiftLib/JNI/JNICaching.swift +++ b/Sources/JExtractSwiftLib/JNI/JNICaching.swift @@ -34,7 +34,7 @@ enum JNICaching { } private static func bridgeName(for typeName: SwiftQualifiedTypeName) -> String { - "_SwiftJavaBridge_\(typeName.fullFlatName)" + "_JNIBridge_\(typeName.fullFlatName)" } static func cacheMemberName(for enumCase: ImportedEnumCase) -> String { diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift index 9bf66f997..8dac84adb 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift @@ -1002,9 +1002,8 @@ extension JNISwift2JavaGenerator { parameters: [ JavaParameter(name: parameterName, type: .long) ], - conversion: .method( - .constant("\(swiftDictionaryType)"), - function: "init", + conversion: .constructor( + swiftDictionaryType, arguments: [ ("fromJNI", .placeholder), ("in", .constant("environment")), @@ -1049,9 +1048,8 @@ extension JNISwift2JavaGenerator { parameters: [ JavaParameter(name: parameterName, type: .long) ], - conversion: .method( - .constant("\(swiftSetType)"), - function: "init", + conversion: .constructor( + swiftSetType, arguments: [ ("fromJNI", .placeholder), ("in", .constant("environment")), @@ -1091,13 +1089,10 @@ extension JNISwift2JavaGenerator { return "JavaDictionaryBridge<\(try bridgeTypeName(for: key)), \(try bridgeTypeName(for: value))>" case .set(let element): return "JavaSetBridge<\(try bridgeTypeName(for: element))>" - default: - guard let javaType = JNIJavaTypeTranslator.translate(knownType: knownType.kind, config: self.config), - javaType.implementsJavaValue - else { - throw JavaTranslationError.unsupportedSwiftType(swiftType) - } + case .bool, .int, .uint, .int8, .uint8, .int16, .uint16, .int32, .uint32, .int64, .uint64, .float, .double, .string: return "JavaBoxableBridge<\(swiftType)>" + default: + throw JavaTranslationError.unsupportedSwiftType(swiftType) } } @@ -1108,9 +1103,9 @@ extension JNISwift2JavaGenerator { let bridgeName = JNICaching.bridgeName(for: nominalType) if nominalType.genericArguments.isEmpty { return bridgeName + } else { + return "\(bridgeName)<\(nominalType.genericArguments.map(\.description).joined(separator: ", "))>" } - let genericArguments = try nominalType.genericArguments.map { try renderedBridgeNominalGenericArgument($0) } - return "\(bridgeName)<\(genericArguments.joined(separator: ", "))>" case .genericParameter: throw JavaTranslationError.unsupportedSwiftType(swiftType) @@ -1119,15 +1114,6 @@ extension JNISwift2JavaGenerator { throw JavaTranslationError.unsupportedSwiftType(swiftType) } } - - private func renderedBridgeNominalGenericArgument(_ swiftType: SwiftType) throws -> String { - switch swiftType { - case .nominal, .genericParameter: - return "\(swiftType)" - default: - throw JavaTranslationError.unsupportedSwiftType(swiftType) - } - } } struct NativeFunctionSignature { @@ -1256,6 +1242,11 @@ extension JNISwift2JavaGenerator { outArgumentName: String ) + indirect case constructor( + _ swiftType: SwiftType, + arguments: [(String?, NativeSwiftConversionStep)] = [] + ) + indirect case method( NativeSwiftConversionStep, function: String, @@ -1663,6 +1654,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 014f93e03..263b68b9a 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift @@ -787,6 +787,7 @@ extension JNISwift2JavaGenerator { signature: "\(signature)", isStatic: true ) + private static let cache = _JNIMethodIDCache( className: "\(jniClassName)", methods: [wrapMemoryAddressUnsafeMethod] @@ -807,7 +808,7 @@ extension JNISwift2JavaGenerator { 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.effectiveJavaTypeName == type.swiftNominal.qualifiedTypeName + let isEffectivelyGeneric = type.swiftNominal.isGeneric && !type.isSpecialization let bridgeGenericClause = if type.swiftNominal.genericParameters.isEmpty { "" @@ -820,62 +821,17 @@ extension JNISwift2JavaGenerator { } else { "\(type.baseTypeName)<\(type.genericParameterNames.joined(separator: ", "))>" } + let parentProtocol = isEffectivelyGeneric ? "JextractedGenericTypeBridge" : "JextractedTypeBridge" - printer.printBraceBlock("enum \(bridgeName)\(bridgeGenericClause): JavaClassBackedTypeBridge") { printer in + printer.printBraceBlock("enum \(bridgeName)\(bridgeGenericClause): \(parentProtocol)") { 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 func toJavaObject(_ value: SwiftType, in environment: JNIEnvironment) -> jobject?") { printer in - printer.print("let selfPointer$ = UnsafeMutablePointer.allocate(capacity: 1)") - printer.print("selfPointer$.initialize(to: value)") - printer.print("let selfPointerBits$ = Int64(Int(bitPattern: selfPointer$))") - var args = [ - ("j", "selfPointerBits$.getJNIValue(in: environment)") - ] - if isEffectivelyGeneric { - printer.print("let selfTypePointer$ = unsafeBitCast(SwiftType.self, to: UnsafeRawPointer.self)") - printer.print("let selfTypePointerBits$ = Int64(Int(bitPattern: selfTypePointer$))") - args.append(("j", "selfTypePointerBits$.getJNIValue(in: environment)")) - } - args.append(("l", "JavaSwiftArena.defaultAutoArena.javaThis")) - printer.print("var args = [\(Array(repeating: "jvalue()", count: args.count).joined(separator: ", "))]") - for (i, (jvalueField, expr)) in args.enumerated() { - printer.print("args[\(i)].\(jvalueField) = \(expr)") - } - printer.print( - """ - return environment.interface.CallStaticObjectMethodA( - environment, - \(cacheName).javaClass, - \(cacheName).wrapMemoryAddressUnsafe, - &args - ) - """ - ) - } - printer.println() - printer.printBraceBlock("static func fromJavaObject(_ obj: jobject?, in environment: JNIEnvironment) -> SwiftType") { printer in - printer.print( - """ - guard let obj else { - fatalError("\(bridgedSwiftType).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("\(bridgedSwiftType).fromJavaObject received a null Swift memory address") - } - return valuePointer$.pointee - """ - ) + printer.printBraceBlock("static var wrapMemoryAddressUnsafe: jmethodID") { printer in + printer.print("\(cacheName).wrapMemoryAddressUnsafe") } } } diff --git a/Sources/SwiftJava/BridgedValues/JavaBoxing.swift b/Sources/SwiftJava/BridgedValues/JavaBoxing.swift index 3874ba68b..4285cb602 100644 --- a/Sources/SwiftJava/BridgedValues/JavaBoxing.swift +++ b/Sources/SwiftJava/BridgedValues/JavaBoxing.swift @@ -14,78 +14,6 @@ import SwiftJavaJNICore -// ==== ----------------------------------------------------------------------- -// MARK: Explicit Java bridge protocols - -/// 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 JavaTypeBridge { - associatedtype SwiftType - - /// Returns whether the given Java object can be consumed by this bridge. - static func isJavaObject(_ obj: jobject?, in environment: JNIEnvironment) -> Bool - - /// 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 -} - -/// Convenience protocol for bridges backed by a specific Java class. -public protocol JavaClassBackedTypeBridge: JavaTypeBridge { - static var javaClass: jclass { get } -} - -extension JavaClassBackedTypeBridge { - public static func isJavaObject(_ obj: jobject?, in environment: JNIEnvironment) -> Bool { - guard let obj else { return false } - return environment.interface.IsInstanceOf(environment, obj, javaClass) == JNI_TRUE - } -} - -/// Adapter that reuses an existing `JavaBoxable` conformance as a `JavaTypeBridge`. -public enum JavaBoxableBridge: JavaClassBackedTypeBridge { - public typealias SwiftType = T - - public static var javaClass: jclass { - T.javaBoxClass - } - - 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) - } -} - -/// Adapter for Swift projections of Java reference types. -public enum JavaObjectBridge: JavaTypeBridge { - public typealias SwiftType = T - - public static func isJavaObject(_ obj: jobject?, in environment: JNIEnvironment) -> Bool { - guard let obj else { return false } - return (try? T.withJNIClass(in: environment) { cls in - environment.interface.IsInstanceOf(environment, obj, cls) == JNI_TRUE - }) ?? false - } - - 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) - } -} - // ==== ----------------------------------------------------------------------- // MARK: JavaBoxable protocol diff --git a/Sources/SwiftJava/BridgedValues/JavaTypeBridge.swift b/Sources/SwiftJava/BridgedValues/JavaTypeBridge.swift new file mode 100644 index 000000000..86169f960 --- /dev/null +++ b/Sources/SwiftJava/BridgedValues/JavaTypeBridge.swift @@ -0,0 +1,69 @@ +//===----------------------------------------------------------------------===// +// +// 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 JavaTypeBridge { + associatedtype SwiftType + + /// Returns whether the given Java object can be consumed by this bridge. + static func isJavaObject(_ obj: jobject?, in environment: JNIEnvironment) -> Bool + + /// 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 +} + +public enum JavaBoxableBridge: JavaTypeBridge { + public typealias SwiftType = T + + public static func isJavaObject(_ obj: jobject?, in environment: JNIEnvironment) -> Bool { + guard let obj else { return false } + return environment.interface.IsInstanceOf(environment, obj, T.javaBoxClass) == JNI_TRUE + } + + 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 enum JavaObjectBridge: JavaTypeBridge { + public typealias SwiftType = T + + public static func isJavaObject(_ obj: jobject?, in environment: JNIEnvironment) -> Bool { + guard let obj else { return false } + return (try? T.withJNIClass(in: environment) { cls in + environment.interface.IsInstanceOf(environment, obj, cls) == JNI_TRUE + }) ?? false + } + + 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) + } +} diff --git a/Sources/SwiftJavaRuntimeSupport/Collection+JavaBoxable.swift b/Sources/SwiftJavaRuntimeSupport/Collection+JavaBoxable.swift index 40515747f..ad73611a4 100644 --- a/Sources/SwiftJavaRuntimeSupport/Collection+JavaBoxable.swift +++ b/Sources/SwiftJavaRuntimeSupport/Collection+JavaBoxable.swift @@ -14,12 +14,12 @@ import SwiftJava -public enum JavaDictionaryBridge: JavaClassBackedTypeBridge -where KeyBridge.SwiftType: Hashable { +public enum JavaDictionaryBridge: JavaTypeBridge where KeyBridge.SwiftType: Hashable { public typealias SwiftType = [KeyBridge.SwiftType: ValueBridge.SwiftType] - public static var javaClass: jclass { - _JNIMethodIDCache.SwiftDictionaryMap.class + public static func isJavaObject(_ obj: jobject?, in environment: JNIEnvironment) -> Bool { + guard let obj else { return false } + return environment.interface.IsInstanceOf(environment, obj, _JNIMethodIDCache.SwiftDictionaryMap.class) == JNI_TRUE } public static func toJavaObject(_ value: SwiftType, in environment: JNIEnvironment) -> jobject? { @@ -49,11 +49,12 @@ where KeyBridge.SwiftType: Hashable { } } -public enum JavaSetBridge: JavaClassBackedTypeBridge where ElementBridge.SwiftType: Hashable { +public enum JavaSetBridge: JavaTypeBridge where ElementBridge.SwiftType: Hashable { public typealias SwiftType = Set - public static var javaClass: jclass { - _JNIMethodIDCache.SwiftSet.class + public static func isJavaObject(_ obj: jobject?, in environment: JNIEnvironment) -> Bool { + guard let obj else { return false } + return environment.interface.IsInstanceOf(environment, obj, _JNIMethodIDCache.SwiftSet.class) == JNI_TRUE } public static func toJavaObject(_ value: SwiftType, in environment: JNIEnvironment) -> jobject? { diff --git a/Sources/SwiftJavaRuntimeSupport/JextractedTypeBridge.swift b/Sources/SwiftJavaRuntimeSupport/JextractedTypeBridge.swift new file mode 100644 index 000000000..aefa8ad22 --- /dev/null +++ b/Sources/SwiftJavaRuntimeSupport/JextractedTypeBridge.swift @@ -0,0 +1,81 @@ +//===----------------------------------------------------------------------===// +// +// 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 protocol JextractedTypeBridge: JavaTypeBridge { + static var javaClass: jclass { get } + static var wrapMemoryAddressUnsafe: jmethodID { get } +} + +extension JextractedTypeBridge { + public static func isJavaObject(_ obj: jobject?, in environment: JNIEnvironment) -> Bool { + guard let obj else { return false } + return environment.interface.IsInstanceOf(environment, obj, javaClass) == JNI_TRUE + } + + 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("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("fromJavaObject received a null Swift memory address") + } + return valuePointer$.pointee + } +} + +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/Tests/JExtractSwiftTests/JNI/JNICollectionBoxableTests.swift b/Tests/JExtractSwiftTests/JNI/JNICollectionBoxableTests.swift index 716d68d3c..6ad0ee4af 100644 --- a/Tests/JExtractSwiftTests/JNI/JNICollectionBoxableTests.swift +++ b/Tests/JExtractSwiftTests/JNI/JNICollectionBoxableTests.swift @@ -37,7 +37,7 @@ struct JNICollectionBoxableTests { ) """, """ - enum _SwiftJavaBridge_ReefFish: JavaClassBackedTypeBridge { + enum _SwiftJavaBridge_ReefFish: JextractedTypeBridge { typealias SwiftType = ReefFish static var javaClass: jclass { _JNI_ReefFish.javaClass @@ -103,7 +103,7 @@ struct JNICollectionBoxableTests { ) """, """ - enum _SwiftJavaBridge_MyID: JavaClassBackedTypeBridge { + enum _SwiftJavaBridge_MyID: JextractedTypeBridge { typealias SwiftType = MyID static var javaClass: jclass { _JNI_MyID.javaClass From dccc87480ad8785d4fa1aa80497bafa97ec4f0b4 Mon Sep 17 00:00:00 2001 From: Iceman Date: Thu, 14 May 2026 18:04:27 +0900 Subject: [PATCH 16/25] Restore removal of JavaValue from JavaBoxable --- .../SwiftJava/BridgedValues/JavaBoxing.swift | 13 +++++---- .../BridgedValues/JavaValue+Dictionary.swift | 26 ++--------------- .../BridgedValues/JavaValue+Set.swift | 15 +--------- ...vaBoxable.swift => CollectionBridge.swift} | 28 ------------------- 4 files changed, 11 insertions(+), 71 deletions(-) rename Sources/SwiftJavaRuntimeSupport/{Collection+JavaBoxable.swift => CollectionBridge.swift} (76%) diff --git a/Sources/SwiftJava/BridgedValues/JavaBoxing.swift b/Sources/SwiftJava/BridgedValues/JavaBoxing.swift index 4285cb602..b39e09afe 100644 --- a/Sources/SwiftJava/BridgedValues/JavaBoxing.swift +++ b/Sources/SwiftJava/BridgedValues/JavaBoxing.swift @@ -20,7 +20,7 @@ import SwiftJavaJNICore /// A type that can be boxed into and unboxed from a Java object via JNI. /// This is used for dictionary keys and values that need to cross the JNI boundary /// as boxed Java objects (e.g. Long, Double, Boolean, String). -public protocol JavaBoxable { +public protocol JavaBoxable: JavaValue { /// Convert this Swift value to a boxed Java object. func toJavaObject(in environment: JNIEnvironment) -> jobject? @@ -331,9 +331,11 @@ class AnySwiftDictionaryBox { /// Generic subclass that wraps a concrete `[K: V]` Swift dictionary. final class SwiftDictionaryBox: AnySwiftDictionaryBox where KeyBridge.SwiftType: Hashable { - let dictionary: [KeyBridge.SwiftType: ValueBridge.SwiftType] + typealias Key = KeyBridge.SwiftType + typealias Value = ValueBridge.SwiftType + let dictionary: [Key: Value] - init(_ dictionary: [KeyBridge.SwiftType: ValueBridge.SwiftType]) { + init(_ dictionary: [Key: Value]) { self.dictionary = dictionary } @@ -402,9 +404,10 @@ class AnySwiftSetBox { /// Generic subclass that wraps a concrete `Set` Swift set. final class SwiftSetBox: AnySwiftSetBox where ElementBridge.SwiftType: Hashable { - let set: Set + typealias Element = ElementBridge.SwiftType + let set: Set - init(_ set: Set) { + init(_ set: Set) { self.set = set } diff --git a/Sources/SwiftJava/BridgedValues/JavaValue+Dictionary.swift b/Sources/SwiftJava/BridgedValues/JavaValue+Dictionary.swift index 067ae879a..eed8056d6 100644 --- a/Sources/SwiftJava/BridgedValues/JavaValue+Dictionary.swift +++ b/Sources/SwiftJava/BridgedValues/JavaValue+Dictionary.swift @@ -17,7 +17,7 @@ import SwiftJavaJNICore // ==== ----------------------------------------------------------------------- // MARK: Dictionary extension for JNI bridging -extension Dictionary where Key: Hashable { +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( @@ -34,7 +34,7 @@ extension Dictionary where Key: Hashable { /// Reconstruct a Swift dictionary from a JNI jlong pointer to a SwiftDictionaryBox. public init( fromJNI value: jlong, - in environment: JNIEnvironment, + in environment: JNIEnvironment, keyBridge: KeyBridge.Type, valueBridge: ValueBridge.Type ) where KeyBridge.SwiftType == Key, ValueBridge.SwiftType == Value { @@ -43,25 +43,3 @@ extension Dictionary where Key: Hashable { self = box.dictionary } } - -extension Dictionary where Key: JavaBoxable & Hashable, Value: JavaBoxable { - /// 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 { - dictionaryGetJNIValue( - in: environment, - keyBridge: JavaBoxableBridge.self, - valueBridge: JavaBoxableBridge.self - ) - } - - /// Reconstruct a Swift dictionary from a JNI jlong pointer to a SwiftDictionaryBox. - public init(fromJNI value: jlong, in environment: JNIEnvironment) { - self.init( - fromJNI: value, - in: environment, - keyBridge: JavaBoxableBridge.self, - valueBridge: JavaBoxableBridge.self - ) - } -} diff --git a/Sources/SwiftJava/BridgedValues/JavaValue+Set.swift b/Sources/SwiftJava/BridgedValues/JavaValue+Set.swift index fce03d7f4..92eafee9f 100644 --- a/Sources/SwiftJava/BridgedValues/JavaValue+Set.swift +++ b/Sources/SwiftJava/BridgedValues/JavaValue+Set.swift @@ -17,7 +17,7 @@ import SwiftJavaJNICore // ==== ----------------------------------------------------------------------- // MARK: Set extension for JNI bridging -extension Set where Element: 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( @@ -41,16 +41,3 @@ extension Set where Element: Hashable { self = box.set } } - -extension Set where Element: JavaBoxable & Hashable { - /// 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 { - setGetJNIValue(in: environment, elementBridge: JavaBoxableBridge.self) - } - - /// Reconstruct a Swift set from a JNI jlong pointer to a SwiftSetBox. - public init(fromJNI value: jlong, in environment: JNIEnvironment) { - self.init(fromJNI: value, in: environment, elementBridge: JavaBoxableBridge.self) - } -} diff --git a/Sources/SwiftJavaRuntimeSupport/Collection+JavaBoxable.swift b/Sources/SwiftJavaRuntimeSupport/CollectionBridge.swift similarity index 76% rename from Sources/SwiftJavaRuntimeSupport/Collection+JavaBoxable.swift rename to Sources/SwiftJavaRuntimeSupport/CollectionBridge.swift index ad73611a4..de7dc5476 100644 --- a/Sources/SwiftJavaRuntimeSupport/Collection+JavaBoxable.swift +++ b/Sources/SwiftJavaRuntimeSupport/CollectionBridge.swift @@ -83,31 +83,3 @@ public enum JavaSetBridge: JavaTypeBridge where E return SwiftType(fromJNI: selfPointer, in: environment, elementBridge: ElementBridge.self) } } - -extension Dictionary: JavaBoxable where Key: JavaBoxable & Hashable, Value: JavaBoxable { - public static var javaBoxClass: jclass { - _JNIMethodIDCache.SwiftDictionaryMap.class - } - - public func toJavaObject(in environment: JNIEnvironment) -> jobject? { - JavaDictionaryBridge, JavaBoxableBridge>.toJavaObject(self, in: environment) - } - - public static func fromJavaObject(_ obj: jobject?, in environment: JNIEnvironment) -> Self { - JavaDictionaryBridge, JavaBoxableBridge>.fromJavaObject(obj, in: environment) - } -} - -extension Set: JavaBoxable where Element: JavaBoxable & Hashable { - public static var javaBoxClass: jclass { - _JNIMethodIDCache.SwiftSet.class - } - - public func toJavaObject(in environment: JNIEnvironment) -> jobject? { - JavaSetBridge>.toJavaObject(self, in: environment) - } - - public static func fromJavaObject(_ obj: jobject?, in environment: JNIEnvironment) -> Self { - JavaSetBridge>.fromJavaObject(obj, in: environment) - } -} From 81b1a21bd9427549c7dde6eaa78549367422fe18 Mon Sep 17 00:00:00 2001 From: Iceman Date: Fri, 15 May 2026 09:32:51 +0900 Subject: [PATCH 17/25] rename bridge types --- .../JNI/JNISwift2JavaGenerator+NativeTranslation.swift | 4 ++-- Sources/SwiftJava/BridgedValues/JavaBoxing.swift | 4 ++-- Sources/SwiftJava/BridgedValues/JavaValue+Dictionary.swift | 4 ++-- Sources/SwiftJava/BridgedValues/JavaValue+Set.swift | 4 ++-- .../{JavaTypeBridge.swift => JobjectBridge.swift} | 6 +++--- Sources/SwiftJavaRuntimeSupport/CollectionBridge.swift | 4 ++-- Sources/SwiftJavaRuntimeSupport/JextractedTypeBridge.swift | 2 +- 7 files changed, 14 insertions(+), 14 deletions(-) rename Sources/SwiftJava/BridgedValues/{JavaTypeBridge.swift => JobjectBridge.swift} (93%) diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift index 8dac84adb..3f5d129e3 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift @@ -1086,9 +1086,9 @@ extension JNISwift2JavaGenerator { if let knownType = nominalType.asKnownType { switch knownType { case .dictionary(let key, let value): - return "JavaDictionaryBridge<\(try bridgeTypeName(for: key)), \(try bridgeTypeName(for: value))>" + return "DictionaryBridge<\(try bridgeTypeName(for: key)), \(try bridgeTypeName(for: value))>" case .set(let element): - return "JavaSetBridge<\(try bridgeTypeName(for: 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: diff --git a/Sources/SwiftJava/BridgedValues/JavaBoxing.swift b/Sources/SwiftJava/BridgedValues/JavaBoxing.swift index b39e09afe..f26d3b5a5 100644 --- a/Sources/SwiftJava/BridgedValues/JavaBoxing.swift +++ b/Sources/SwiftJava/BridgedValues/JavaBoxing.swift @@ -329,7 +329,7 @@ class AnySwiftDictionaryBox { } /// Generic subclass that wraps a concrete `[K: V]` Swift dictionary. -final class SwiftDictionaryBox: AnySwiftDictionaryBox +final class SwiftDictionaryBox: AnySwiftDictionaryBox where KeyBridge.SwiftType: Hashable { typealias Key = KeyBridge.SwiftType typealias Value = ValueBridge.SwiftType @@ -403,7 +403,7 @@ class AnySwiftSetBox { } /// Generic subclass that wraps a concrete `Set` Swift set. -final class SwiftSetBox: AnySwiftSetBox where ElementBridge.SwiftType: Hashable { +final class SwiftSetBox: AnySwiftSetBox where ElementBridge.SwiftType: Hashable { typealias Element = ElementBridge.SwiftType let set: Set diff --git a/Sources/SwiftJava/BridgedValues/JavaValue+Dictionary.swift b/Sources/SwiftJava/BridgedValues/JavaValue+Dictionary.swift index eed8056d6..cf3bc3ff3 100644 --- a/Sources/SwiftJava/BridgedValues/JavaValue+Dictionary.swift +++ b/Sources/SwiftJava/BridgedValues/JavaValue+Dictionary.swift @@ -20,7 +20,7 @@ import SwiftJavaJNICore 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( + public func dictionaryGetJNIValue( in environment: JNIEnvironment, keyBridge: KeyBridge.Type, valueBridge: ValueBridge.Type @@ -32,7 +32,7 @@ extension Dictionary { } /// Reconstruct a Swift dictionary from a JNI jlong pointer to a SwiftDictionaryBox. - public init( + public init( fromJNI value: jlong, in environment: JNIEnvironment, keyBridge: KeyBridge.Type, diff --git a/Sources/SwiftJava/BridgedValues/JavaValue+Set.swift b/Sources/SwiftJava/BridgedValues/JavaValue+Set.swift index 92eafee9f..6b59d8d55 100644 --- a/Sources/SwiftJava/BridgedValues/JavaValue+Set.swift +++ b/Sources/SwiftJava/BridgedValues/JavaValue+Set.swift @@ -20,7 +20,7 @@ import SwiftJavaJNICore 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( + public func setGetJNIValue( in environment: JNIEnvironment, elementBridge: ElementBridge.Type ) -> jlong where ElementBridge.SwiftType == Element { @@ -31,7 +31,7 @@ extension Set { } /// Reconstruct a Swift set from a JNI jlong pointer to a SwiftSetBox. - public init( + public init( fromJNI value: jlong, in environment: JNIEnvironment, elementBridge: ElementBridge.Type diff --git a/Sources/SwiftJava/BridgedValues/JavaTypeBridge.swift b/Sources/SwiftJava/BridgedValues/JobjectBridge.swift similarity index 93% rename from Sources/SwiftJava/BridgedValues/JavaTypeBridge.swift rename to Sources/SwiftJava/BridgedValues/JobjectBridge.swift index 86169f960..ffa06bb4c 100644 --- a/Sources/SwiftJava/BridgedValues/JavaTypeBridge.swift +++ b/Sources/SwiftJava/BridgedValues/JobjectBridge.swift @@ -16,7 +16,7 @@ /// /// Unlike `JavaBoxable`, this is not attached to the bridged nominal type itself, /// which avoids protocol-conformance conflicts for inheritable classes. -public protocol JavaTypeBridge { +public protocol JobjectBridge { associatedtype SwiftType /// Returns whether the given Java object can be consumed by this bridge. @@ -29,7 +29,7 @@ public protocol JavaTypeBridge { static func fromJavaObject(_ obj: jobject?, in environment: JNIEnvironment) -> SwiftType } -public enum JavaBoxableBridge: JavaTypeBridge { +public enum JavaBoxableBridge: JobjectBridge { public typealias SwiftType = T public static func isJavaObject(_ obj: jobject?, in environment: JNIEnvironment) -> Bool { @@ -46,7 +46,7 @@ public enum JavaBoxableBridge: JavaTypeBridge { } } -public enum JavaObjectBridge: JavaTypeBridge { +public enum JavaObjectBridge: JobjectBridge { public typealias SwiftType = T public static func isJavaObject(_ obj: jobject?, in environment: JNIEnvironment) -> Bool { diff --git a/Sources/SwiftJavaRuntimeSupport/CollectionBridge.swift b/Sources/SwiftJavaRuntimeSupport/CollectionBridge.swift index de7dc5476..1e8c138c6 100644 --- a/Sources/SwiftJavaRuntimeSupport/CollectionBridge.swift +++ b/Sources/SwiftJavaRuntimeSupport/CollectionBridge.swift @@ -14,7 +14,7 @@ import SwiftJava -public enum JavaDictionaryBridge: JavaTypeBridge where KeyBridge.SwiftType: Hashable { +public enum DictionaryBridge: JobjectBridge where KeyBridge.SwiftType: Hashable { public typealias SwiftType = [KeyBridge.SwiftType: ValueBridge.SwiftType] public static func isJavaObject(_ obj: jobject?, in environment: JNIEnvironment) -> Bool { @@ -49,7 +49,7 @@ public enum JavaDictionaryBridge: JavaTypeBridge where ElementBridge.SwiftType: Hashable { +public enum SetBridge: JobjectBridge where ElementBridge.SwiftType: Hashable { public typealias SwiftType = Set public static func isJavaObject(_ obj: jobject?, in environment: JNIEnvironment) -> Bool { diff --git a/Sources/SwiftJavaRuntimeSupport/JextractedTypeBridge.swift b/Sources/SwiftJavaRuntimeSupport/JextractedTypeBridge.swift index aefa8ad22..8e0687c46 100644 --- a/Sources/SwiftJavaRuntimeSupport/JextractedTypeBridge.swift +++ b/Sources/SwiftJavaRuntimeSupport/JextractedTypeBridge.swift @@ -14,7 +14,7 @@ import SwiftJava -public protocol JextractedTypeBridge: JavaTypeBridge { +public protocol JextractedTypeBridge: JobjectBridge { static var javaClass: jclass { get } static var wrapMemoryAddressUnsafe: jmethodID { get } } From 163193d2188ab7397414fa9c8eb0090aef707d29 Mon Sep 17 00:00:00 2001 From: Iceman Date: Fri, 15 May 2026 09:50:31 +0900 Subject: [PATCH 18/25] Fix tests --- .../JNI/JNICollectionBoxableTests.swift | 153 ------------------ .../JNI/JNIDictionaryTest.swift | 10 +- .../JExtractSwiftTests/JNI/JNIEnumTests.swift | 3 - .../JNI/JNIJobjectBridgeTests.swift | 92 +++++++++++ Tests/JExtractSwiftTests/JNI/JNISetTest.swift | 8 +- 5 files changed, 101 insertions(+), 165 deletions(-) delete mode 100644 Tests/JExtractSwiftTests/JNI/JNICollectionBoxableTests.swift create mode 100644 Tests/JExtractSwiftTests/JNI/JNIJobjectBridgeTests.swift diff --git a/Tests/JExtractSwiftTests/JNI/JNICollectionBoxableTests.swift b/Tests/JExtractSwiftTests/JNI/JNICollectionBoxableTests.swift deleted file mode 100644 index 6ad0ee4af..000000000 --- a/Tests/JExtractSwiftTests/JNI/JNICollectionBoxableTests.swift +++ /dev/null @@ -1,153 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// 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 JNICollectionBoxableTests { - @Test("JNI generates explicit bridges for dictionary element types") - func generatesBridgeDeclaration() throws { - try assertOutput( - input: """ - public struct ReefFish: Hashable {} - public func f(dict: [Int: ReefFish]) -> [Int: ReefFish] {} - """, - .jni, - .swift, - detectChunkByInitialLines: 1, - 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 - ) - """, - """ - enum _SwiftJavaBridge_ReefFish: JextractedTypeBridge { - typealias SwiftType = ReefFish - static var javaClass: jclass { - _JNI_ReefFish.javaClass - } - 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, - _JNI_ReefFish.javaClass, - _JNI_ReefFish.wrapMemoryAddressUnsafe, - &args - ) - } - ... - static func fromJavaObject(_ obj: jobject?, in environment: JNIEnvironment) -> SwiftType { - guard let obj else { - fatalError("ReefFish.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("ReefFish.fromJavaObject received a null Swift memory address") - } - return valuePointer$.pointee - } - } - """, - """ - return SwiftModule.f(dict: [Int: ReefFish].init(fromJNI: dict, in: environment, keyBridge: JavaBoxableBridge.self, valueBridge: _SwiftJavaBridge_ReefFish.self)).dictionaryGetJNIValue(in: environment, keyBridge: JavaBoxableBridge.self, valueBridge: _SwiftJavaBridge_ReefFish.self) - """, - ] - ) - } - - - @Test("JNI generates explicit bridges for generic dictionary keys") - func generatesBridgeDeclarationForGenericType() throws { - try assertOutput( - input: """ - public struct MyID: Hashable {} - public func f() -> [MyID: String] {} - """, - .jni, - .swift, - detectChunkByInitialLines: 1, - expectedChunks: [ - """ - private enum _JNI_MyID { - private static let wrapMemoryAddressUnsafeMethod = _JNIMethodIDCache.Method( - name: "wrapMemoryAddressUnsafe", - signature: "(JJLorg/swift/swiftkit/core/SwiftArena;)Lcom/example/swift/MyID;", - isStatic: true - ) - """, - """ - enum _SwiftJavaBridge_MyID: JextractedTypeBridge { - typealias SwiftType = MyID - static var javaClass: jclass { - _JNI_MyID.javaClass - } - 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, - _JNI_MyID.javaClass, - _JNI_MyID.wrapMemoryAddressUnsafe, - &args - ) - } - ... - static func fromJavaObject(_ obj: jobject?, in environment: JNIEnvironment) -> SwiftType { - guard let obj else { - fatalError("MyID.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("MyID.fromJavaObject received a null Swift memory address") - } - return valuePointer$.pointee - } - } - """, - """ - return SwiftModule.f().dictionaryGetJNIValue(in: environment, keyBridge: _SwiftJavaBridge_MyID.self, valueBridge: JavaBoxableBridge.self) - """, - ] - ) - } -} diff --git a/Tests/JExtractSwiftTests/JNI/JNIDictionaryTest.swift b/Tests/JExtractSwiftTests/JNI/JNIDictionaryTest.swift index eaa233d44..88ae2397a 100644 --- a/Tests/JExtractSwiftTests/JNI/JNIDictionaryTest.swift +++ b/Tests/JExtractSwiftTests/JNI/JNIDictionaryTest.swift @@ -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].init(fromJNI: dict, in: environment, keyBridge: JavaBoxableBridge.self, valueBridge: JavaBoxableBridge.self)) + 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].init(fromJNI: dict, in: environment, keyBridge: JavaBoxableBridge.self, valueBridge: JavaBoxableBridge.self)).dictionaryGetJNIValue(in: environment, keyBridge: JavaBoxableBridge.self, valueBridge: JavaBoxableBridge.self) + 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].init(fromJNI: dict, in: environment, keyBridge: JavaBoxableBridge.self, valueBridge: JavaBoxableBridge.self)).dictionaryGetJNIValue(in: environment, keyBridge: JavaBoxableBridge.self, valueBridge: JavaBoxableBridge.self) + 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].init(fromJNI: a, in: environment, keyBridge: JavaBoxableBridge.self, valueBridge: JavaBoxableBridge.self), b: [String: Bool].init(fromJNI: b, in: environment, keyBridge: JavaBoxableBridge.self, valueBridge: JavaBoxableBridge.self)) + 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].init(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) + 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..6fc8c239e --- /dev/null +++ b/Tests/JExtractSwiftTests/JNI/JNIJobjectBridgeTests.swift @@ -0,0 +1,92 @@ +//===----------------------------------------------------------------------===// +// +// 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") + func generatesBridgeDeclarationForGenericType() throws { + try assertOutput( + input: """ + public struct MyID: Hashable {} + public func f() -> [MyID: String] {} + """, + .jni, + .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + enum _JNIBridge_MyID: JextractedGenericTypeBridge { + typealias SwiftType = MyID + """, + """ + return SwiftModule.f().dictionaryGetJNIValue(in: environment, keyBridge: _JNIBridge_MyID.self, valueBridge: JavaBoxableBridge.self) + """, + ] + ) + } +} diff --git a/Tests/JExtractSwiftTests/JNI/JNISetTest.swift b/Tests/JExtractSwiftTests/JNI/JNISetTest.swift index 655994067..6a79c4c92 100644 --- a/Tests/JExtractSwiftTests/JNI/JNISetTest.swift +++ b/Tests/JExtractSwiftTests/JNI/JNISetTest.swift @@ -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.init(fromJNI: set, in: environment, elementBridge: JavaBoxableBridge.self)) + 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.init(fromJNI: set, in: environment, elementBridge: JavaBoxableBridge.self)).setGetJNIValue(in: environment, elementBridge: JavaBoxableBridge.self) + 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.init(fromJNI: a, in: environment, elementBridge: JavaBoxableBridge.self), b: Set.init(fromJNI: b, in: environment, elementBridge: JavaBoxableBridge.self)) + 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.init(fromJNI: set, in: environment, elementBridge: JavaBoxableBridge.self), element: String(fromJNI: element, in: environment)).setGetJNIValue(in: environment, elementBridge: JavaBoxableBridge.self) + return SwiftModule.f(set: Set(fromJNI: set, in: environment, elementBridge: JavaBoxableBridge.self), element: String(fromJNI: element, in: environment)).setGetJNIValue(in: environment, elementBridge: JavaBoxableBridge.self) } """ ] From f33c2bf56e80f4f019b497e09a8e9efc71365541 Mon Sep 17 00:00:00 2001 From: Iceman Date: Fri, 15 May 2026 11:08:22 +0900 Subject: [PATCH 19/25] Add Array and Optional support --- .../MySwiftLibrary/CollectionBoxable.swift | 27 ++++ .../example/swift/CollectionBoxableTest.java | 44 ++++++ ...wift2JavaGenerator+NativeTranslation.swift | 4 + .../BridgedValues/JobjectBridge.swift | 41 +++-- .../CollectionBridge.swift | 143 ++++++++++++++++-- .../JNIMethodIDCaches.swift | 49 ++++++ .../JextractedTypeBridge.swift | 12 +- 7 files changed, 291 insertions(+), 29 deletions(-) diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/CollectionBoxable.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/CollectionBoxable.swift index e8a5a9df0..33232de4d 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/CollectionBoxable.swift +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/CollectionBoxable.swift @@ -57,3 +57,30 @@ public func makeSetInDictionary() -> [String: Set] { "odd": [1, 3, 5], ] } + +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/src/test/java/com/example/swift/CollectionBoxableTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/CollectionBoxableTest.java index de3fef1bd..e94e1b288 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/CollectionBoxableTest.java +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/CollectionBoxableTest.java @@ -21,6 +21,8 @@ import static org.junit.jupiter.api.Assertions.*; +import java.util.Arrays; +import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; @@ -95,4 +97,46 @@ void makeSetInDictionary() { assertNull(dict.get("unknown")); } } + + @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/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift index 3f5d129e3..1442b4158 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift @@ -1085,6 +1085,10 @@ extension JNISwift2JavaGenerator { 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): diff --git a/Sources/SwiftJava/BridgedValues/JobjectBridge.swift b/Sources/SwiftJava/BridgedValues/JobjectBridge.swift index ffa06bb4c..7674415eb 100644 --- a/Sources/SwiftJava/BridgedValues/JobjectBridge.swift +++ b/Sources/SwiftJava/BridgedValues/JobjectBridge.swift @@ -19,23 +19,29 @@ public protocol JobjectBridge { associatedtype SwiftType - /// Returns whether the given Java object can be consumed by this bridge. - static func isJavaObject(_ obj: jobject?, in environment: JNIEnvironment) -> Bool - /// 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 -} -public enum JavaBoxableBridge: JobjectBridge { - public typealias SwiftType = T + 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 } - return environment.interface.IsInstanceOf(environment, obj, T.javaBoxClass) == JNI_TRUE + return (try? withJNIClass(in: environment) { cls in + environment.interface.IsInstanceOf(environment, obj, cls) == JNI_TRUE + }) ?? false } +} + +public enum JavaBoxableBridge: JobjectBridge { + public typealias SwiftType = T public static func toJavaObject(_ value: T, in environment: JNIEnvironment) -> jobject? { value.toJavaObject(in: environment) @@ -44,18 +50,18 @@ public enum JavaBoxableBridge: JobjectBridge { 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 isJavaObject(_ obj: jobject?, in environment: JNIEnvironment) -> Bool { - guard let obj else { return false } - return (try? T.withJNIClass(in: environment) { cls in - environment.interface.IsInstanceOf(environment, obj, cls) == JNI_TRUE - }) ?? false - } - public static func toJavaObject(_ value: T, in environment: JNIEnvironment) -> jobject? { value.javaThis } @@ -66,4 +72,11 @@ public enum JavaObjectBridge: JobjectBridge { } 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 index 1e8c138c6..398491f5c 100644 --- a/Sources/SwiftJavaRuntimeSupport/CollectionBridge.swift +++ b/Sources/SwiftJavaRuntimeSupport/CollectionBridge.swift @@ -17,11 +17,6 @@ import SwiftJava public enum DictionaryBridge: JobjectBridge where KeyBridge.SwiftType: Hashable { public typealias SwiftType = [KeyBridge.SwiftType: ValueBridge.SwiftType] - public static func isJavaObject(_ obj: jobject?, in environment: JNIEnvironment) -> Bool { - guard let obj else { return false } - return environment.interface.IsInstanceOf(environment, obj, _JNIMethodIDCache.SwiftDictionaryMap.class) == JNI_TRUE - } - 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()] @@ -47,16 +42,18 @@ public enum DictionaryBridge( + 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 isJavaObject(_ obj: jobject?, in environment: JNIEnvironment) -> Bool { - guard let obj else { return false } - return environment.interface.IsInstanceOf(environment, obj, _JNIMethodIDCache.SwiftSet.class) == JNI_TRUE - } - public static func toJavaObject(_ value: SwiftType, in environment: JNIEnvironment) -> jobject? { let selfPointer = value.setGetJNIValue(in: environment, elementBridge: ElementBridge.self) var args = [jvalue(), jvalue()] @@ -82,4 +79,130 @@ public enum SetBridge: JobjectBridge where Element ) 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) { componentClass in + guard let array = environment.interface.NewObjectArray( + environment, + jsize(value.count), + componentClass, + nil + ) else { + fatalError("Array.toJavaObject failed to allocate a Java array") + } + + for (index, element) in value.enumerated() { + let javaElement = ElementBridge.toJavaObject(element, in: environment) + environment.interface.SetObjectArrayElement(environment, array, jsize(index), 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 index 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 b48962a75..b938def03 100644 --- a/Sources/SwiftJavaRuntimeSupport/JNIMethodIDCaches.swift +++ b/Sources/SwiftJavaRuntimeSupport/JNIMethodIDCaches.swift @@ -163,6 +163,55 @@ extension _JNIMethodIDCache { } } + 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 index 8e0687c46..c56a4dcff 100644 --- a/Sources/SwiftJavaRuntimeSupport/JextractedTypeBridge.swift +++ b/Sources/SwiftJavaRuntimeSupport/JextractedTypeBridge.swift @@ -20,11 +20,6 @@ public protocol JextractedTypeBridge: JobjectBridge { } extension JextractedTypeBridge { - public static func isJavaObject(_ obj: jobject?, in environment: JNIEnvironment) -> Bool { - guard let obj else { return false } - return environment.interface.IsInstanceOf(environment, obj, javaClass) == JNI_TRUE - } - public static func toJavaObject(_ value: SwiftType, in environment: JNIEnvironment) -> jobject? { let selfPointer$ = UnsafeMutablePointer.allocate(capacity: 1) selfPointer$.initialize(to: value) @@ -56,6 +51,13 @@ extension JextractedTypeBridge { } return valuePointer$.pointee } + + public static func withJNIClass( + in environment: JNIEnvironment, + _ body: (jclass) throws -> Result + ) throws -> Result { + try body(javaClass) + } } public protocol JextractedGenericTypeBridge: JextractedTypeBridge {} From 407886d529c8fd11cca259f82743117d10c6be60 Mon Sep 17 00:00:00 2001 From: Iceman Date: Fri, 15 May 2026 11:32:04 +0900 Subject: [PATCH 20/25] Add intArrayDictionary test --- .../Sources/MySwiftLibrary/CollectionBoxable.swift | 11 +++++++++++ .../com/example/swift/CollectionBoxableTest.java | 11 +++++++++++ .../SwiftJavaRuntimeSupport/CollectionBridge.swift | 12 ++++++------ 3 files changed, 28 insertions(+), 6 deletions(-) diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/CollectionBoxable.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/CollectionBoxable.swift index 33232de4d..c2f9d374d 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/CollectionBoxable.swift +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/CollectionBoxable.swift @@ -58,6 +58,17 @@ public func makeSetInDictionary() -> [String: Set] { ] } +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": [ diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/CollectionBoxableTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/CollectionBoxableTest.java index e94e1b288..63c874f9a 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/CollectionBoxableTest.java +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/CollectionBoxableTest.java @@ -98,6 +98,17 @@ void makeSetInDictionary() { } } + @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()) { diff --git a/Sources/SwiftJavaRuntimeSupport/CollectionBridge.swift b/Sources/SwiftJavaRuntimeSupport/CollectionBridge.swift index 398491f5c..ee8be293a 100644 --- a/Sources/SwiftJavaRuntimeSupport/CollectionBridge.swift +++ b/Sources/SwiftJavaRuntimeSupport/CollectionBridge.swift @@ -147,19 +147,19 @@ public enum ArrayBridge: JobjectBridge { public typealias SwiftType = [ElementBridge.SwiftType] public static func toJavaObject(_ value: SwiftType, in environment: JNIEnvironment) -> jobject? { - try! ElementBridge.withJNIClass(in: environment) { componentClass in + try! ElementBridge.withJNIClass(in: environment) { elementClass in guard let array = environment.interface.NewObjectArray( environment, jsize(value.count), - componentClass, + elementClass, nil ) else { fatalError("Array.toJavaObject failed to allocate a Java array") } - for (index, element) in value.enumerated() { + for (i, element) in value.enumerated() { let javaElement = ElementBridge.toJavaObject(element, in: environment) - environment.interface.SetObjectArrayElement(environment, array, jsize(index), javaElement) + environment.interface.SetObjectArrayElement(environment, array, jsize(i), javaElement) } return array } @@ -175,8 +175,8 @@ public enum ArrayBridge: JobjectBridge { var result: SwiftType = [] result.reserveCapacity(count) - for index in 0.. Date: Fri, 15 May 2026 11:33:13 +0900 Subject: [PATCH 21/25] formatting --- ...JNISwift2JavaGenerator+SwiftThunkPrinting.swift | 13 +++++++------ .../BridgedValues/JavaValue+Dictionary.swift | 2 +- .../SwiftJava/BridgedValues/JobjectBridge.swift | 10 +++++++--- .../SwiftJavaRuntimeSupport/CollectionBridge.swift | 14 ++++++++------ .../JNI/JNIJobjectBridgeTests.swift | 2 +- 5 files changed, 24 insertions(+), 17 deletions(-) diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift index 263b68b9a..d9fd99a69 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift @@ -773,11 +773,12 @@ extension JNISwift2JavaGenerator { 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);" - } + 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( @@ -787,7 +788,7 @@ extension JNISwift2JavaGenerator { signature: "\(signature)", isStatic: true ) - + private static let cache = _JNIMethodIDCache( className: "\(jniClassName)", methods: [wrapMemoryAddressUnsafeMethod] diff --git a/Sources/SwiftJava/BridgedValues/JavaValue+Dictionary.swift b/Sources/SwiftJava/BridgedValues/JavaValue+Dictionary.swift index cf3bc3ff3..5ac2028d1 100644 --- a/Sources/SwiftJava/BridgedValues/JavaValue+Dictionary.swift +++ b/Sources/SwiftJava/BridgedValues/JavaValue+Dictionary.swift @@ -34,7 +34,7 @@ extension Dictionary { /// Reconstruct a Swift dictionary from a JNI jlong pointer to a SwiftDictionaryBox. public init( fromJNI value: jlong, - in environment: JNIEnvironment, + in environment: JNIEnvironment, keyBridge: KeyBridge.Type, valueBridge: ValueBridge.Type ) where KeyBridge.SwiftType == Key, ValueBridge.SwiftType == Value { diff --git a/Sources/SwiftJava/BridgedValues/JobjectBridge.swift b/Sources/SwiftJava/BridgedValues/JobjectBridge.swift index 7674415eb..df0e5f18b 100644 --- a/Sources/SwiftJava/BridgedValues/JobjectBridge.swift +++ b/Sources/SwiftJava/BridgedValues/JobjectBridge.swift @@ -34,9 +34,13 @@ public protocol JobjectBridge { extension JobjectBridge { public static func isJavaObject(_ obj: jobject?, in environment: JNIEnvironment) -> Bool { guard let obj else { return false } - return (try? withJNIClass(in: environment) { cls in - environment.interface.IsInstanceOf(environment, obj, cls) == JNI_TRUE - }) ?? false + do { + return try withJNIClass(in: environment) { cls in + environment.interface.IsInstanceOf(environment, obj, cls) == JNI_TRUE + } + } catch { + return false + } } } diff --git a/Sources/SwiftJavaRuntimeSupport/CollectionBridge.swift b/Sources/SwiftJavaRuntimeSupport/CollectionBridge.swift index ee8be293a..baad3cd07 100644 --- a/Sources/SwiftJavaRuntimeSupport/CollectionBridge.swift +++ b/Sources/SwiftJavaRuntimeSupport/CollectionBridge.swift @@ -148,12 +148,14 @@ public enum ArrayBridge: JobjectBridge { 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 { + guard + let array = environment.interface.NewObjectArray( + environment, + jsize(value.count), + elementClass, + nil + ) + else { fatalError("Array.toJavaObject failed to allocate a Java array") } diff --git a/Tests/JExtractSwiftTests/JNI/JNIJobjectBridgeTests.swift b/Tests/JExtractSwiftTests/JNI/JNIJobjectBridgeTests.swift index 6fc8c239e..2c098e142 100644 --- a/Tests/JExtractSwiftTests/JNI/JNIJobjectBridgeTests.swift +++ b/Tests/JExtractSwiftTests/JNI/JNIJobjectBridgeTests.swift @@ -54,7 +54,7 @@ struct JNIJobjectBridgeTests { static var javaClass: jclass { _JNI_ReefFish.javaClass } - + static var wrapMemoryAddressUnsafe: jmethodID { _JNI_ReefFish.wrapMemoryAddressUnsafe } From 6c421e2111fc2116afcab29b54c190135fb63152 Mon Sep 17 00:00:00 2001 From: Iceman Date: Fri, 15 May 2026 14:21:18 +0900 Subject: [PATCH 22/25] Add documentation comment and error details --- .../SwiftJavaRuntimeSupport/JextractedTypeBridge.swift | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Sources/SwiftJavaRuntimeSupport/JextractedTypeBridge.swift b/Sources/SwiftJavaRuntimeSupport/JextractedTypeBridge.swift index c56a4dcff..b9a41ebbd 100644 --- a/Sources/SwiftJavaRuntimeSupport/JextractedTypeBridge.swift +++ b/Sources/SwiftJavaRuntimeSupport/JextractedTypeBridge.swift @@ -14,6 +14,10 @@ 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 } @@ -37,7 +41,7 @@ extension JextractedTypeBridge { public static func fromJavaObject(_ obj: jobject?, in environment: JNIEnvironment) -> SwiftType { guard let obj else { - fatalError("fromJavaObject received a null Java object") + fatalError("\(Self.self).fromJavaObject received a null Java object") } let selfPointer$ = environment.interface.CallLongMethodA( environment, @@ -47,7 +51,7 @@ extension JextractedTypeBridge { ) let selfPointerBits$ = Int(Int64(fromJNI: selfPointer$, in: environment)) guard let valuePointer$ = UnsafeMutablePointer(bitPattern: selfPointerBits$) else { - fatalError("fromJavaObject received a null Swift memory address") + fatalError("\(Self.self).fromJavaObject received a null Swift memory address") } return valuePointer$.pointee } From 9b61ec00c8a88e68d14ff5e1f7c7f548d2828c36 Mon Sep 17 00:00:00 2001 From: Iceman Date: Fri, 15 May 2026 14:31:49 +0900 Subject: [PATCH 23/25] Fix build error in LinkageTest --- Sources/JavaKit/Helpers/_JNIMethodIDCache.swift | 2 +- Sources/SwiftJavaRuntimeSupport/_JNIMethodIDCache.swift | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) 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/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 From c5cdc0a04334bbacd261586bbb9edcaec3d5e9a6 Mon Sep 17 00:00:00 2001 From: Iceman Date: Fri, 15 May 2026 17:05:33 +0900 Subject: [PATCH 24/25] Fix generic clause join operation --- .../JNISwift2JavaGenerator+SwiftThunkPrinting.swift | 2 +- .../JExtractSwiftTests/JNI/JNIJobjectBridgeTests.swift | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift index d9fd99a69..193d91056 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift @@ -814,7 +814,7 @@ extension JNISwift2JavaGenerator { if type.swiftNominal.genericParameters.isEmpty { "" } else { - "<\(type.swiftNominal.genericParameters.map { $0.syntax.trimmedDescription }.joined(separator: ", "))>" + "<\(type.swiftNominal.genericParameters.map { $0.syntax.trimmedDescription }.joined(separator: " "))>" } let bridgedSwiftType = if type.genericParameterNames.isEmpty { diff --git a/Tests/JExtractSwiftTests/JNI/JNIJobjectBridgeTests.swift b/Tests/JExtractSwiftTests/JNI/JNIJobjectBridgeTests.swift index 2c098e142..7379bc191 100644 --- a/Tests/JExtractSwiftTests/JNI/JNIJobjectBridgeTests.swift +++ b/Tests/JExtractSwiftTests/JNI/JNIJobjectBridgeTests.swift @@ -72,19 +72,19 @@ struct JNIJobjectBridgeTests { func generatesBridgeDeclarationForGenericType() throws { try assertOutput( input: """ - public struct MyID: Hashable {} - public func f() -> [MyID: String] {} + public struct MyID: Hashable {} + public func f() -> [MyID: String] {} """, .jni, .swift, detectChunkByInitialLines: 1, expectedChunks: [ """ - enum _JNIBridge_MyID: JextractedGenericTypeBridge { - typealias SwiftType = MyID + enum _JNIBridge_MyID: JextractedGenericTypeBridge { + typealias SwiftType = MyID """, """ - return SwiftModule.f().dictionaryGetJNIValue(in: environment, keyBridge: _JNIBridge_MyID.self, valueBridge: JavaBoxableBridge.self) + return SwiftModule.f().dictionaryGetJNIValue(in: environment, keyBridge: _JNIBridge_MyID.self, valueBridge: JavaBoxableBridge.self) """, ] ) From 02ea80ef45370879804086fc7e618ff26738d87e Mon Sep 17 00:00:00 2001 From: Iceman Date: Fri, 15 May 2026 17:33:12 +0900 Subject: [PATCH 25/25] Pickup generic where clause --- .../JNISwift2JavaGenerator+SwiftThunkPrinting.swift | 10 +++++++++- .../SwiftTypes/SwiftNominalTypeDeclaration.swift | 4 ++++ .../JNI/JNIJobjectBridgeTests.swift | 12 +++++++++--- 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift index 193d91056..43391270c 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift @@ -816,6 +816,7 @@ extension JNISwift2JavaGenerator { } else { "<\(type.swiftNominal.genericParameters.map { $0.syntax.trimmedDescription }.joined(separator: " "))>" } + let bridgeWhereClause = type.swiftNominal.genericWhereClause?.trimmedDescription let bridgedSwiftType = if type.genericParameterNames.isEmpty { type.effectiveSwiftTypeName @@ -824,7 +825,14 @@ extension JNISwift2JavaGenerator { } let parentProtocol = isEffectivelyGeneric ? "JextractedGenericTypeBridge" : "JextractedTypeBridge" - printer.printBraceBlock("enum \(bridgeName)\(bridgeGenericClause): \(parentProtocol)") { printer in + 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 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/Tests/JExtractSwiftTests/JNI/JNIJobjectBridgeTests.swift b/Tests/JExtractSwiftTests/JNI/JNIJobjectBridgeTests.swift index 7379bc191..9653503f4 100644 --- a/Tests/JExtractSwiftTests/JNI/JNIJobjectBridgeTests.swift +++ b/Tests/JExtractSwiftTests/JNI/JNIJobjectBridgeTests.swift @@ -68,12 +68,14 @@ struct JNIJobjectBridgeTests { } - @Test("JNI generates explicit bridges for generic dictionary keys") + @Test("JNI generates explicit bridges for generic dictionary keys and values") func generatesBridgeDeclarationForGenericType() throws { try assertOutput( input: """ public struct MyID: Hashable {} - public func f() -> [MyID: String] {} + public struct MyValue where O : Swift.Comparable, E : Swift.Error {} + public enum Never: Error {} + public func f() -> [MyID: MyValue] {} """, .jni, .swift, @@ -84,7 +86,11 @@ struct JNIJobjectBridgeTests { typealias SwiftType = MyID """, """ - return SwiftModule.f().dictionaryGetJNIValue(in: environment, keyBridge: _JNIBridge_MyID.self, valueBridge: JavaBoxableBridge.self) + 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) """, ] )