From 2e98adbe294d9c33f6c7f93f34f66d6b7b004ff5 Mon Sep 17 00:00:00 2001 From: Konrad Malawski Date: Wed, 8 Apr 2026 12:20:52 +0900 Subject: [PATCH 1/3] jextract/jni: handle tuples with array elements We'd wrongly render the array used for the tuple out parameter and the dimension of the elements would be at the end of the type, but it must be [3][][][] rather than [][][]][3]. This allows returning arrays inside tuples --- .../Sources/MySwiftLibrary/Tuples.swift | 4 ++ .../java/com/example/swift/TupleTest.java | 11 ++++ ...ISwift2JavaGenerator+JavaTranslation.swift | 12 ++++- .../JExtractSwiftTests/JNI/JNIArrayTest.swift | 50 +++++++++++++++++++ 4 files changed, 75 insertions(+), 2 deletions(-) diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Tuples.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Tuples.swift index 32a07f774..b3038f977 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Tuples.swift +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Tuples.swift @@ -41,3 +41,7 @@ public func makeBigTuple() -> ( 11, 12, 13, 14.0 ) } + +public func namedByteArrayTuple() -> (name: [UInt8], another: [UInt8]) { + (name: [1, 2, 3], another: [4, 5]) +} diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/TupleTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/TupleTest.java index a3ad4b197..1a110eb2a 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/TupleTest.java +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/TupleTest.java @@ -84,4 +84,15 @@ void makeBigTuple() { assertEquals(13L, result.$14); assertEquals(14.0f, result.$15); } + + @Test + void namedByteArrayTuple() { + var result = MySwiftLibrary.namedByteArrayTuple(); + + assertArrayEquals(new byte[] { 1, 2, 3 }, result.name()); + assertArrayEquals(new byte[] { 4, 5 }, result.another()); + + assertArrayEquals(new byte[] { 1, 2, 3 }, (byte[]) result.$0); + assertArrayEquals(new byte[] { 4, 5 }, (byte[]) result.$1); + } } diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift index f5a2fe320..204c5f27c 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift @@ -1731,10 +1731,18 @@ extension JNISwift2JavaGenerator { func render(type: JavaType) -> String { switch self { case .newArray(let javaType, let size): - "new \(javaType)[\(size)]" + // For array element types like byte[], we need "new byte[size][]" + // not "new byte[][size]" + var baseType = javaType + var extraDimensions = "" + while case .array(let inner) = baseType { + extraDimensions += "[]" + baseType = inner + } + return "new \(baseType)[\(size)]\(extraDimensions)" case .new: - "new \(type)()" + return "new \(type)()" } } } diff --git a/Tests/JExtractSwiftTests/JNI/JNIArrayTest.swift b/Tests/JExtractSwiftTests/JNI/JNIArrayTest.swift index 3be27da76..753b750c2 100644 --- a/Tests/JExtractSwiftTests/JNI/JNIArrayTest.swift +++ b/Tests/JExtractSwiftTests/JNI/JNIArrayTest.swift @@ -311,4 +311,54 @@ struct JNIArrayTest { ] ) } + + // ==== ----------------------------------------------------------------------- + // MARK: Tuples with array elements + + @Test("Import: () -> (name: [UInt8], another: [UInt8]) (Java)") + func tupleByteArrays_java() throws { + try assertOutput( + input: "public func namedByteArrayTuple() -> (name: [UInt8], another: [UInt8]) {}", + .jni, + .java, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + @Unsigned + public static LabeledTuple_namedByteArrayTuple_name_another namedByteArrayTuple() { + byte[][] result_0$ = new byte[1][]; + byte[][] result_1$ = new byte[1][]; + SwiftModule.$namedByteArrayTuple(result_0$, result_1$); + return new LabeledTuple_namedByteArrayTuple_name_another(result_0$[0], result_1$[0]); + } + """, + """ + private static native void $namedByteArrayTuple(byte[][] result_0$, byte[][] result_1$); + """, + ] + ) + } + + @Test("Import: () -> (name: [UInt8], another: [UInt8]) (Swift)") + func tupleByteArrays_swift() throws { + try assertOutput( + input: "public func namedByteArrayTuple() -> (name: [UInt8], another: [UInt8]) {}", + .jni, + .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + @_cdecl("Java_com_example_swift_SwiftModule__00024namedByteArrayTuple___3_3B_3_3B") + public func Java_com_example_swift_SwiftModule__00024namedByteArrayTuple___3_3B_3_3B(environment: UnsafeMutablePointer!, thisClass: jclass, result_0$: jobjectArray?, result_1$: jobjectArray?) { + let tupleResult$ = SwiftModule.namedByteArrayTuple() + let element_0_jni$ = tupleResult$.name.getJNILocalRefValue(in: environment) + environment.interface.SetObjectArrayElement(environment, result_0$, 0, element_0_jni$) + let element_1_jni$ = tupleResult$.another.getJNILocalRefValue(in: environment) + environment.interface.SetObjectArrayElement(environment, result_1$, 0, element_1_jni$) + return + } + """ + ] + ) + } } From 36d24fa9f5c3b0127d878364c72c143b1edd013d Mon Sep 17 00:00:00 2001 From: Konrad Malawski Date: Wed, 8 Apr 2026 12:59:01 +0900 Subject: [PATCH 2/3] Handle unsigned in tuple results properly --- .../JNISwift2JavaGenerator+JavaTranslation.swift | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift index 204c5f27c..9039cf595 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift @@ -1236,8 +1236,19 @@ extension JNISwift2JavaGenerator { elements: tupleElements ) + // Collect annotations from tuple elements - if any element is @Unsigned, + // propagate that to the method level + var tupleAnnotations: [JavaAnnotation] = [] + for element in elements { + let elementAnnotations = getJavaTypeAnnotations(swiftType: element.type, config: config) + for annotation in elementAnnotations where !tupleAnnotations.contains(annotation) { + tupleAnnotations.append(annotation) + } + } + return TranslatedResult( javaType: javaResultType, + annotations: tupleAnnotations, outParameters: outParameters, conversion: javaNativeConversionStep ) @@ -2130,7 +2141,7 @@ extension JNISwift2JavaGenerator { _fileID: String = #fileID, _line: Int = #line, ) -> JavaTranslationError { - .unsupportedSwiftType(known: type, fileID: _fileID, line: _line) + .unsupportedSwiftType(knowvan: type, fileID: _fileID, line: _line) } /// The user has not supplied a mapping from `SwiftType` to From e2cfe984ffa95773334ecef80a27e10330586127 Mon Sep 17 00:00:00 2001 From: Konrad Malawski Date: Wed, 8 Apr 2026 14:41:23 +0900 Subject: [PATCH 3/3] Seems we never formally made @Unsigned apply to methods, even tho generating code which assumed this was correct huh --- .../JNI/JNISwift2JavaGenerator+JavaTranslation.swift | 2 +- .../java/org/swift/swiftkit/core/annotations/Unsigned.java | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift index 9039cf595..ddb88b9f4 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift @@ -2141,7 +2141,7 @@ extension JNISwift2JavaGenerator { _fileID: String = #fileID, _line: Int = #line, ) -> JavaTranslationError { - .unsupportedSwiftType(knowvan: type, fileID: _fileID, line: _line) + .unsupportedSwiftType(known: type, fileID: _fileID, line: _line) } /// The user has not supplied a mapping from `SwiftType` to diff --git a/SwiftKitCore/src/main/java/org/swift/swiftkit/core/annotations/Unsigned.java b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/annotations/Unsigned.java index 4bf8e3544..93906c51f 100644 --- a/SwiftKitCore/src/main/java/org/swift/swiftkit/core/annotations/Unsigned.java +++ b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/annotations/Unsigned.java @@ -32,11 +32,13 @@ * in a method signature corresponds to a Swift {@code UInt64} type, and therefore * negative values reported by the signed {@code long} should instead be interpreted positive values, * larger than {@code Long.MAX_VALUE} that are just not representable using a signed {@code long}. + *

+ * If this annotation is used on a method, it refers to the return type using an unsigned integer. */ @Documented @Label("Unsigned integer type") @Description("Value should be interpreted as unsigned data type") -@Target({TYPE_USE, PARAMETER, FIELD}) +@Target({TYPE_USE, PARAMETER, FIELD, METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface Unsigned { }