From 3bb14057fabfe9d4f5320457d015bc835a4a3c1c Mon Sep 17 00:00:00 2001 From: Konrad Malawski Date: Wed, 1 Apr 2026 19:41:35 +0900 Subject: [PATCH 1/3] jextract/jni: support generic params : DataProtocol Functions with `` generic constraints failed to translate because `.foundationDataProtocol` and `.essentialsDataProtocol` fell through to the default case in `translateGenericTypeParameter`. --- .../Sources/MySwiftLibrary/Data.swift | 11 +++++ .../test/java/com/example/swift/DataTest.java | 27 ++++++++++ ...Generator+InterfaceWrapperGeneration.swift | 7 +++ ...ISwift2JavaGenerator+JavaTranslation.swift | 3 ++ .../SwiftTypes/SwiftKnownTypes.swift | 8 +++ .../JExtractSwiftTests/DataImportTests.swift | 49 +++++++++++++++++++ 6 files changed, 105 insertions(+) diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Data.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Data.swift index 11aa38a2..3dcbc613 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Data.swift +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Data.swift @@ -35,3 +35,14 @@ public func getDataCount(_ data: Data) -> Int { public func compareData(_ data1: Data, _ data2: Data) -> Bool { data1 == data2 } + +// ==== ----------------------------------------------------------------------- +// MARK: DataProtocol generic parameter + +public func getDataCountGeneric(_ data: D) -> Int { + data.count +} + +public func compareDataGeneric(_ data1: D1, _ data2: D2) -> Bool { + data1.elementsEqual(data2) +} 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 1088b176..a6024ca1 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/DataTest.java +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/DataTest.java @@ -128,4 +128,31 @@ void data_toByteArray_roundTrip() { assertArrayEquals(original, result); } } + + // DataProtocol generic parameter tests + + @Test + void data_getCountGeneric() { + try (var arena = SwiftArena.ofConfined()) { + byte[] bytes = new byte[] { 1, 2, 3, 4, 5 }; + var data = Data.fromByteArray(bytes, arena); + assertEquals(5, MySwiftLibrary.getDataCountGeneric(data)); + } + } + + @Test + void data_compareDataGeneric() { + try (var arena = SwiftArena.ofConfined()) { + byte[] bytes1 = new byte[] { 1, 2, 3 }; + byte[] bytes2 = new byte[] { 1, 2, 3 }; + byte[] bytes3 = new byte[] { 1, 2, 4 }; + + var data1 = Data.fromByteArray(bytes1, arena); + var data2 = Data.fromByteArray(bytes2, arena); + var data3 = Data.fromByteArray(bytes3, arena); + + assertTrue(MySwiftLibrary.compareDataGeneric(data1, data2)); + assertFalse(MySwiftLibrary.compareDataGeneric(data1, data3)); + } + } } diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+InterfaceWrapperGeneration.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+InterfaceWrapperGeneration.swift index 2f19b39a..3d36ee34 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+InterfaceWrapperGeneration.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+InterfaceWrapperGeneration.swift @@ -25,6 +25,13 @@ extension JNISwift2JavaGenerator { var wrappers = [ImportedNominalType: JavaInterfaceSwiftWrapper]() for type in types where type.swiftNominal.kind == .protocol { + // Skip protocols that have a known representative concrete type (e.g. DataProtocol). + if let knownKind = type.swiftNominal.knownTypeKind, + SwiftKnownTypes.representativeType(of: knownKind) != nil + { + continue + } + do { let translator = JavaInterfaceProtocolWrapperGenerator() wrappers[type] = try translator.generate(for: type) diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift index dc28006b..efd2cb12 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift @@ -1088,6 +1088,9 @@ extension JNISwift2JavaGenerator { case .foundationData, .essentialsData: return .class(package: nil, name: "Data") + case .foundationDataProtocol, .essentialsDataProtocol: + return .class(package: nil, name: "DataProtocol") + case .foundationUUID, .essentialsUUID: return .javaUtilUUID diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownTypes.swift b/Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownTypes.swift index a1a21fd6..45349084 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownTypes.swift +++ b/Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownTypes.swift @@ -152,4 +152,12 @@ struct SwiftKnownTypes { default: return nil } } + + /// Returns true if the given protocol kind has a representative concrete type + static func hasRepresentativeType(_ knownProtocol: SwiftKnownTypeDeclKind) -> Bool { + switch knownProtocol { + case .foundationDataProtocol, .essentialsDataProtocol: return true + default: return false + } + } } diff --git a/Tests/JExtractSwiftTests/DataImportTests.swift b/Tests/JExtractSwiftTests/DataImportTests.swift index 47d5a473..06797eee 100644 --- a/Tests/JExtractSwiftTests/DataImportTests.swift +++ b/Tests/JExtractSwiftTests/DataImportTests.swift @@ -578,4 +578,53 @@ final class DataImportTests { ) } + // ==== ----------------------------------------------------------------------- + // MARK: JNI DataProtocol generic parameter + + @Test("Import DataProtocol: JNI generic parameter") + func dataProtocol_jni_genericParameter() throws { + let text = """ + import Foundation + + public struct MyResult { + public init() {} + } + public func processData(data: D) -> MyResult + """ + + try assertOutput( + input: text, + .jni, + .java, + detectChunkByInitialLines: 2, + expectedChunks: [ + // The Java binding must have type parameter + """ + public static MyResult processData(D data, SwiftArena swiftArena) { + """ + ] + ) + } + + @Test("Import DataProtocol: JNI multiple generic parameters") + func dataProtocol_jni_multipleGenericParameters() throws { + let text = """ + import Foundation + + public func verify(first: D1, second: D2) -> Bool + """ + + try assertOutput( + input: text, + .jni, + .java, + detectChunkByInitialLines: 2, + expectedChunks: [ + """ + public static boolean verify(D1 first, D2 second) { + """ + ] + ) + } + } From abf15bb6992812ac681a6ac217ee4f48b28295d1 Mon Sep 17 00:00:00 2001 From: Konrad Malawski Date: Wed, 1 Apr 2026 21:45:43 +0900 Subject: [PATCH 2/3] include whole opening thunks for reference in tests --- .../SwiftTypes/SwiftKnownTypes.swift | 18 ++- .../JExtractSwiftTests/DataImportTests.swift | 110 +++++++++++++++++- 2 files changed, 117 insertions(+), 11 deletions(-) diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownTypes.swift b/Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownTypes.swift index 45349084..198a1c14 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownTypes.swift +++ b/Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownTypes.swift @@ -144,20 +144,18 @@ struct SwiftKnownTypes { } /// Returns the known representative concrete type if there is one for the - /// given protocol kind. E.g. `String` for `StringProtocol` + /// given protocol kind. E.g. `Data` for `DataProtocol` func representativeType(of knownProtocol: SwiftKnownTypeDeclKind) -> SwiftType? { - switch knownProtocol { - case .foundationDataProtocol: return self.foundationData - case .essentialsDataProtocol: return self.essentialsData - default: return nil - } + guard let kind = Self.representativeType(of: knownProtocol) else { return nil } + return .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[kind])) } - /// Returns true if the given protocol kind has a representative concrete type - static func hasRepresentativeType(_ knownProtocol: SwiftKnownTypeDeclKind) -> Bool { + /// Returns the representative concrete type kind for a protocol, if one exists + static func representativeType(of knownProtocol: SwiftKnownTypeDeclKind) -> SwiftKnownTypeDeclKind? { switch knownProtocol { - case .foundationDataProtocol, .essentialsDataProtocol: return true - default: return false + case .foundationDataProtocol: return .foundationData + case .essentialsDataProtocol: return .essentialsData + default: return nil } } } diff --git a/Tests/JExtractSwiftTests/DataImportTests.swift b/Tests/JExtractSwiftTests/DataImportTests.swift index 06797eee..cc061e02 100644 --- a/Tests/JExtractSwiftTests/DataImportTests.swift +++ b/Tests/JExtractSwiftTests/DataImportTests.swift @@ -598,7 +598,6 @@ final class DataImportTests { .java, detectChunkByInitialLines: 2, expectedChunks: [ - // The Java binding must have type parameter """ public static MyResult processData(D data, SwiftArena swiftArena) { """ @@ -627,4 +626,113 @@ final class DataImportTests { ) } + @Test("Import DataProtocol: JNI generic parameter Swift thunk") + func dataProtocol_jni_genericParameter_swiftThunk() throws { + let text = """ + import Foundation + + public struct MyResult { + public init() {} + } + public func processData(data: D) -> MyResult + """ + + try assertOutput( + input: text, + .jni, + .swift, + detectChunkByInitialLines: 2, + expectedChunks: [ + """ + public func Java_com_example_swift_SwiftModule__00024processData__Ljava_lang_Object_2(environment: UnsafeMutablePointer!, thisClass: jclass, data: jobject?) -> jlong { + let dataswiftObject$: (DataProtocol) + let datapointer$ = environment.interface.CallLongMethodA(environment, data, _JNIMethodIDCache.JNISwiftInstance.memoryAddress, []) + let datatypeMetadata$ = environment.interface.CallLongMethodA(environment, data, _JNIMethodIDCache.JNISwiftInstance.typeMetadataAddress, []) + guard let datapointer$TypeMetadataPointer$ = UnsafeRawPointer(bitPattern: Int(Int64(fromJNI: datatypeMetadata$, in: environment))) else { + fatalError("datatypeMetadata$ memory address was null") + } + let datapointer$DynamicType$: Any.Type = unsafeBitCast(datapointer$TypeMetadataPointer$, to: Any.Type.self) + guard let datapointer$RawPointer$ = UnsafeMutableRawPointer(bitPattern: Int(Int64(fromJNI: datapointer$, in: environment))) else { + fatalError("datapointer$ memory address was null") + } + #if hasFeature(ImplicitOpenExistentials) + let datapointer$Existential$ = datapointer$RawPointer$.load(as: datapointer$DynamicType$) as! any (DataProtocol) + #else + func datapointer$DoLoad(_ ty: Ty.Type) -> any (DataProtocol) { + datapointer$RawPointer$.load(as: ty) as! any (DataProtocol) + } + let datapointer$Existential$ = _openExistential(datapointer$DynamicType$, do: datapointer$DoLoad) + #endif + dataswiftObject$ = datapointer$Existential$ + let result$ = UnsafeMutablePointer.allocate(capacity: 1) + result$.initialize(to: SwiftModule.processData(data: dataswiftObject$)) + let resultBits$ = Int64(Int(bitPattern: result$)) + return resultBits$.getJNILocalRefValue(in: environment) + } + """ + ] + ) + } + + @Test("Import DataProtocol: JNI mixed generic and some Swift thunk") + func dataProtocol_jni_multipleGenericParameters_swiftThunk() throws { + let text = """ + import Foundation + + public func verify(first: D1, second: some DataProtocol) -> Bool + """ + + try assertOutput( + input: text, + .jni, + .swift, + detectChunkByInitialLines: 2, + expectedChunks: [ + """ + public func Java_com_example_swift_SwiftModule__00024verify__Ljava_lang_Object_2Ljava_lang_Object_2(environment: UnsafeMutablePointer!, thisClass: jclass, first: jobject?, second: jobject?) -> jboolean { + let firstswiftObject$: (DataProtocol) + let firstpointer$ = environment.interface.CallLongMethodA(environment, first, _JNIMethodIDCache.JNISwiftInstance.memoryAddress, []) + let firsttypeMetadata$ = environment.interface.CallLongMethodA(environment, first, _JNIMethodIDCache.JNISwiftInstance.typeMetadataAddress, []) + guard let firstpointer$TypeMetadataPointer$ = UnsafeRawPointer(bitPattern: Int(Int64(fromJNI: firsttypeMetadata$, in: environment))) else { + fatalError("firsttypeMetadata$ memory address was null") + } + let firstpointer$DynamicType$: Any.Type = unsafeBitCast(firstpointer$TypeMetadataPointer$, to: Any.Type.self) + guard let firstpointer$RawPointer$ = UnsafeMutableRawPointer(bitPattern: Int(Int64(fromJNI: firstpointer$, in: environment))) else { + fatalError("firstpointer$ memory address was null") + } + #if hasFeature(ImplicitOpenExistentials) + let firstpointer$Existential$ = firstpointer$RawPointer$.load(as: firstpointer$DynamicType$) as! any (DataProtocol) + #else + func firstpointer$DoLoad(_ ty: Ty.Type) -> any (DataProtocol) { + firstpointer$RawPointer$.load(as: ty) as! any (DataProtocol) + } + let firstpointer$Existential$ = _openExistential(firstpointer$DynamicType$, do: firstpointer$DoLoad) + #endif + firstswiftObject$ = firstpointer$Existential$ + let secondswiftObject$: (DataProtocol) + let secondpointer$ = environment.interface.CallLongMethodA(environment, second, _JNIMethodIDCache.JNISwiftInstance.memoryAddress, []) + let secondtypeMetadata$ = environment.interface.CallLongMethodA(environment, second, _JNIMethodIDCache.JNISwiftInstance.typeMetadataAddress, []) + guard let secondpointer$TypeMetadataPointer$ = UnsafeRawPointer(bitPattern: Int(Int64(fromJNI: secondtypeMetadata$, in: environment))) else { + fatalError("secondtypeMetadata$ memory address was null") + } + let secondpointer$DynamicType$: Any.Type = unsafeBitCast(secondpointer$TypeMetadataPointer$, to: Any.Type.self) + guard let secondpointer$RawPointer$ = UnsafeMutableRawPointer(bitPattern: Int(Int64(fromJNI: secondpointer$, in: environment))) else { + fatalError("secondpointer$ memory address was null") + } + #if hasFeature(ImplicitOpenExistentials) + let secondpointer$Existential$ = secondpointer$RawPointer$.load(as: secondpointer$DynamicType$) as! any (DataProtocol) + #else + func secondpointer$DoLoad(_ ty: Ty.Type) -> any (DataProtocol) { + secondpointer$RawPointer$.load(as: ty) as! any (DataProtocol) + } + let secondpointer$Existential$ = _openExistential(secondpointer$DynamicType$, do: secondpointer$DoLoad) + #endif + secondswiftObject$ = secondpointer$Existential$ + return SwiftModule.verify(first: firstswiftObject$, second: secondswiftObject$).getJNILocalRefValue(in: environment) + } + """ + ] + ) + } + } From 573b85eae663f5f6289735a62606da6f092a5fd0 Mon Sep 17 00:00:00 2001 From: Konrad Malawski Date: Wed, 1 Apr 2026 21:54:34 +0900 Subject: [PATCH 3/3] add a line item for the feature matrix --- .../Documentation.docc/SupportedFeatures.md | 1 + .../JExtractSwiftTests/DataImportTests.swift | 74 ++----------------- 2 files changed, 9 insertions(+), 66 deletions(-) diff --git a/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md b/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md index 87bb5047..ae183df2 100644 --- a/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md +++ b/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md @@ -64,6 +64,7 @@ SwiftJava's `swift-java jextract` tool automates generating Java bindings from S | Generic type: `struct S` | ❌ | ✅ | | Functions or properties using generic type param: `struct S { func f(_: T) {} }` | ❌ | ❌ | | Generic type specialization and conditional extensions: `struct S{} extension S where T == Value {}` | ✅ | ❌ | +| Generic parameters over `some DataProtocol` handled with efficient Java type | ✅ | ✅ | | Static functions or properties in generic type | ❌ | ❌ | | Generic parameters in functions: `func f(x: T)` | ❌ | ✅ | | Generic return values in functions: `func f() -> T` | ❌ | ❌ | diff --git a/Tests/JExtractSwiftTests/DataImportTests.swift b/Tests/JExtractSwiftTests/DataImportTests.swift index cc061e02..e67a3db8 100644 --- a/Tests/JExtractSwiftTests/DataImportTests.swift +++ b/Tests/JExtractSwiftTests/DataImportTests.swift @@ -641,35 +641,14 @@ final class DataImportTests { input: text, .jni, .swift, - detectChunkByInitialLines: 2, + detectChunkByInitialLines: 1, expectedChunks: [ """ public func Java_com_example_swift_SwiftModule__00024processData__Ljava_lang_Object_2(environment: UnsafeMutablePointer!, thisClass: jclass, data: jobject?) -> jlong { - let dataswiftObject$: (DataProtocol) - let datapointer$ = environment.interface.CallLongMethodA(environment, data, _JNIMethodIDCache.JNISwiftInstance.memoryAddress, []) - let datatypeMetadata$ = environment.interface.CallLongMethodA(environment, data, _JNIMethodIDCache.JNISwiftInstance.typeMetadataAddress, []) - guard let datapointer$TypeMetadataPointer$ = UnsafeRawPointer(bitPattern: Int(Int64(fromJNI: datatypeMetadata$, in: environment))) else { - fatalError("datatypeMetadata$ memory address was null") - } - let datapointer$DynamicType$: Any.Type = unsafeBitCast(datapointer$TypeMetadataPointer$, to: Any.Type.self) - guard let datapointer$RawPointer$ = UnsafeMutableRawPointer(bitPattern: Int(Int64(fromJNI: datapointer$, in: environment))) else { - fatalError("datapointer$ memory address was null") - } - #if hasFeature(ImplicitOpenExistentials) - let datapointer$Existential$ = datapointer$RawPointer$.load(as: datapointer$DynamicType$) as! any (DataProtocol) - #else - func datapointer$DoLoad(_ ty: Ty.Type) -> any (DataProtocol) { - datapointer$RawPointer$.load(as: ty) as! any (DataProtocol) - } - let datapointer$Existential$ = _openExistential(datapointer$DynamicType$, do: datapointer$DoLoad) - #endif - dataswiftObject$ = datapointer$Existential$ - let result$ = UnsafeMutablePointer.allocate(capacity: 1) - result$.initialize(to: SwiftModule.processData(data: dataswiftObject$)) - let resultBits$ = Int64(Int(bitPattern: result$)) - return resultBits$.getJNILocalRefValue(in: environment) - } + """, """ + result$.initialize(to: SwiftModule.processData(data: dataswiftObject$)) + """, ] ) } @@ -686,51 +665,14 @@ final class DataImportTests { input: text, .jni, .swift, - detectChunkByInitialLines: 2, + detectChunkByInitialLines: 1, expectedChunks: [ """ public func Java_com_example_swift_SwiftModule__00024verify__Ljava_lang_Object_2Ljava_lang_Object_2(environment: UnsafeMutablePointer!, thisClass: jclass, first: jobject?, second: jobject?) -> jboolean { - let firstswiftObject$: (DataProtocol) - let firstpointer$ = environment.interface.CallLongMethodA(environment, first, _JNIMethodIDCache.JNISwiftInstance.memoryAddress, []) - let firsttypeMetadata$ = environment.interface.CallLongMethodA(environment, first, _JNIMethodIDCache.JNISwiftInstance.typeMetadataAddress, []) - guard let firstpointer$TypeMetadataPointer$ = UnsafeRawPointer(bitPattern: Int(Int64(fromJNI: firsttypeMetadata$, in: environment))) else { - fatalError("firsttypeMetadata$ memory address was null") - } - let firstpointer$DynamicType$: Any.Type = unsafeBitCast(firstpointer$TypeMetadataPointer$, to: Any.Type.self) - guard let firstpointer$RawPointer$ = UnsafeMutableRawPointer(bitPattern: Int(Int64(fromJNI: firstpointer$, in: environment))) else { - fatalError("firstpointer$ memory address was null") - } - #if hasFeature(ImplicitOpenExistentials) - let firstpointer$Existential$ = firstpointer$RawPointer$.load(as: firstpointer$DynamicType$) as! any (DataProtocol) - #else - func firstpointer$DoLoad(_ ty: Ty.Type) -> any (DataProtocol) { - firstpointer$RawPointer$.load(as: ty) as! any (DataProtocol) - } - let firstpointer$Existential$ = _openExistential(firstpointer$DynamicType$, do: firstpointer$DoLoad) - #endif - firstswiftObject$ = firstpointer$Existential$ - let secondswiftObject$: (DataProtocol) - let secondpointer$ = environment.interface.CallLongMethodA(environment, second, _JNIMethodIDCache.JNISwiftInstance.memoryAddress, []) - let secondtypeMetadata$ = environment.interface.CallLongMethodA(environment, second, _JNIMethodIDCache.JNISwiftInstance.typeMetadataAddress, []) - guard let secondpointer$TypeMetadataPointer$ = UnsafeRawPointer(bitPattern: Int(Int64(fromJNI: secondtypeMetadata$, in: environment))) else { - fatalError("secondtypeMetadata$ memory address was null") - } - let secondpointer$DynamicType$: Any.Type = unsafeBitCast(secondpointer$TypeMetadataPointer$, to: Any.Type.self) - guard let secondpointer$RawPointer$ = UnsafeMutableRawPointer(bitPattern: Int(Int64(fromJNI: secondpointer$, in: environment))) else { - fatalError("secondpointer$ memory address was null") - } - #if hasFeature(ImplicitOpenExistentials) - let secondpointer$Existential$ = secondpointer$RawPointer$.load(as: secondpointer$DynamicType$) as! any (DataProtocol) - #else - func secondpointer$DoLoad(_ ty: Ty.Type) -> any (DataProtocol) { - secondpointer$RawPointer$.load(as: ty) as! any (DataProtocol) - } - let secondpointer$Existential$ = _openExistential(secondpointer$DynamicType$, do: secondpointer$DoLoad) - #endif - secondswiftObject$ = secondpointer$Existential$ - return SwiftModule.verify(first: firstswiftObject$, second: secondswiftObject$).getJNILocalRefValue(in: environment) - } + """, """ + return SwiftModule.verify(first: firstswiftObject$, second: secondswiftObject$).getJNILocalRefValue(in: environment) + """, ] ) }