From 4efa302fbfe77cbad5ad3b5f75ec1be907300042 Mon Sep 17 00:00:00 2001 From: Aman Date: Thu, 19 Mar 2026 13:39:53 +0530 Subject: [PATCH 1/6] implemented tuple support for ffm --- ...t2JavaGenerator+JavaBindingsPrinting.swift | 28 ++++ ...MSwift2JavaGenerator+JavaTranslation.swift | 153 +++++++++++++++++- .../FFM/FFMTupleTests.swift | 133 +++++++++++++++ 3 files changed, 311 insertions(+), 3 deletions(-) create mode 100644 Tests/JExtractSwiftTests/FFM/FFMTupleTests.swift diff --git a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaBindingsPrinting.swift b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaBindingsPrinting.swift index 2f2ceecc1..4f473f036 100644 --- a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaBindingsPrinting.swift +++ b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaBindingsPrinting.swift @@ -622,6 +622,12 @@ extension FFMSwift2JavaGenerator.JavaConversionStep { case .commaSeparated(let list, _): return list.contains(where: { $0.requiresSwiftArena }) + + case .replacingPlaceholder(let inner, _): + return inner.requiresSwiftArena + + case .tupleFromOutParams(_, let elements): + return elements.contains(where: { $0.elementConversion.requiresSwiftArena }) } } @@ -654,6 +660,12 @@ extension FFMSwift2JavaGenerator.JavaConversionStep { return inner.requiresTemporaryArena case .commaSeparated(let list, _): return list.contains(where: { $0.requiresTemporaryArena }) + + case .replacingPlaceholder(let inner, _): + return inner.requiresTemporaryArena + + case .tupleFromOutParams(_, let elements): + return elements.contains(where: { $0.elementConversion.requiresTemporaryArena }) } } @@ -754,6 +766,22 @@ extension FFMSwift2JavaGenerator.JavaConversionStep { case .readMemorySegment(let inner, let javaType): let inner = inner.render(&printer, placeholder) return "\(inner).get(\(ForeignValueLayout(javaType: javaType)!), 0)" + + case .replacingPlaceholder(let inner, let root): + return inner.render(&printer, root, placeholderForDowncall: placeholderForDowncall) + + case .tupleFromOutParams(let tupleClassName, let elements): + var args: [String] = [] + for element in elements { + args.append( + element.elementConversion.render( + &printer, + element.outParamName, + placeholderForDowncall: placeholderForDowncall + ) + ) + } + return "\(tupleClassName)(\(args.joined(separator: ", ")))" } } } diff --git a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift index a88f50b66..185a6867b 100644 --- a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift +++ b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift @@ -191,7 +191,7 @@ extension FFMSwift2JavaGenerator { let translatedClosure = try translateFunctionType(name: paramName, swiftType: funcTy, cdeclType: cdeclTy) funcTypes.append(translatedClosure) case .tuple: - // TODO: Implement + // Tuple-typed closure parameters are not supported (same as JNI / lowering). break default: break @@ -462,8 +462,29 @@ extension FFMSwift2JavaGenerator { conversion: .swiftValueSelfSegment(.placeholder) ) + case .tuple([]): + return TranslatedParameter( + javaParameters: [ + JavaParameter( + name: parameterName, + type: .void, + annotations: parameterAnnotations + ) + ], + conversion: .placeholder + ) + + case .tuple(let elements) where !elements.isEmpty: + return try translateTupleParameter( + elements: elements, + convention: convention, + parameterName: parameterName, + methodName: methodName, + genericParameters: genericParameters, + genericRequirements: genericRequirements + ) + case .tuple: - // TODO: Implement. throw JavaTranslationError.unhandledType(swiftType) case .function: @@ -539,6 +560,56 @@ extension FFMSwift2JavaGenerator { } } + /// Tuple parameters: one `TupleN<…>` on the Java API; conversion reads `.$0`, `.$1`, … (mirrors JNI). + func translateTupleParameter( + elements: [SwiftTupleElement], + convention: SwiftParameterConvention, + parameterName: String, + methodName: String, + genericParameters: [SwiftGenericParameterDeclaration], + genericRequirements: [SwiftGenericRequirement] + ) throws -> TranslatedParameter { + let lowering = CdeclLowering(knownTypes: knownTypes) + var elementJavaTypes: [JavaType] = [] + var elementConversions: [JavaConversionStep] = [] + + for (idx, element) in elements.enumerated() { + let subLowered = try lowering.lowerParameter( + element.type, + convention: convention, + parameterName: "\(parameterName)_\(idx)", + genericParameters: genericParameters, + genericRequirements: genericRequirements + ) + let elementTranslated = try translateParameter( + type: element.type, + convention: convention, + parameterName: "\(parameterName)_\(idx)", + loweredParam: subLowered, + methodName: methodName, + genericParameters: genericParameters, + genericRequirements: genericRequirements + ) + guard elementTranslated.javaParameters.count == 1 else { + throw JavaTranslationError.unhandledType(element.type) + } + let extraction = JavaConversionStep.replacingPlaceholder( + elementTranslated.conversion, + placeholder: "\(parameterName).$\(idx)" + ) + elementConversions.append(extraction) + elementJavaTypes.append(elementTranslated.javaParameters[0].type.javaType) + } + + let javaType: JavaType = .tuple(elementTypes: elementJavaTypes) + return TranslatedParameter( + javaParameters: [ + JavaParameter(name: parameterName, type: javaType) + ], + conversion: .commaSeparated(elementConversions) + ) + } + /// Translate an Optional Swift API parameter to the user-facing Java API parameter. func translateOptionalParameter( wrappedType swiftType: SwiftType, @@ -722,8 +793,21 @@ extension FFMSwift2JavaGenerator { conversion: .wrapMemoryAddressUnsafe(.placeholder, javaType) ) + case .tuple([]): + return TranslatedResult( + javaResultType: .void, + annotations: resultAnnotations, + outParameters: [], + conversion: .placeholder + ) + + case .tuple(let elements) where !elements.isEmpty: + return try translateTupleResult( + elements: elements, + resultAnnotations: resultAnnotations + ) + case .tuple: - // TODO: Implement. throw JavaTranslationError.unhandledType(swiftType) case .array(let wrapped) where wrapped == knownTypes.uint8: @@ -782,6 +866,60 @@ extension FFMSwift2JavaGenerator { } + /// Tuple results: indirect `MemorySegment` per element, then `new TupleN<>(…)` (mirrors JNI out-arrays). + func translateTupleResult( + elements: [SwiftTupleElement], + resultAnnotations: [JavaAnnotation] + ) throws -> TranslatedResult { + let arity = elements.count + var outParameters: [JavaParameter] = [] + var tupleElements: [(outParamName: String, elementConversion: JavaConversionStep)] = [] + var elementJavaTypes: [JavaType] = [] + + for (idx, element) in elements.enumerated() { + let (javaType, elementConversion) = try translateTupleElementResult(type: element.type) + outParameters.append(JavaParameter(name: "\(idx)", type: javaType)) + tupleElements.append((outParamName: "_result_\(idx)", elementConversion: elementConversion)) + elementJavaTypes.append(javaType) + } + + let javaResultType: JavaType = .tuple(elementTypes: elementJavaTypes) + let fullTupleClassName = "org.swift.swiftkit.core.tuple.Tuple\(arity)" + + return TranslatedResult( + javaResultType: javaResultType, + annotations: resultAnnotations, + outParameters: outParameters, + conversion: .tupleFromOutParams( + tupleClassName: "new \(fullTupleClassName)<>", + elements: tupleElements + ) + ) + } + + /// Single tuple element for the Java result (mirrors JNI `translateTupleElementResult`). + private func translateTupleElementResult(type: SwiftType) throws -> (JavaType, JavaConversionStep) { + switch type { + case .nominal(let nominalType): + if nominalType.nominalTypeDecl.knownTypeKind != nil { + if let cType = try? CType(cdeclType: type) { + return (cType.javaType, .readMemorySegment(.placeholder, as: cType.javaType)) + } + throw JavaTranslationError.unhandledType(type) + } + + guard !nominalType.isSwiftJavaWrapper else { + throw JavaTranslationError.unhandledType(type) + } + + let javaType: JavaType = .class(package: nil, name: nominalType.nominalTypeDecl.qualifiedName) + return (javaType, .wrapMemoryAddressUnsafe(.placeholder, javaType)) + + default: + throw JavaTranslationError.unhandledType(type) + } + } + func translate( swiftType: SwiftType ) throws -> JavaType { @@ -876,6 +1014,15 @@ extension FFMSwift2JavaGenerator { /// Refer an exploded argument suffixed with `_\(name)`. indirect case readMemorySegment(JavaConversionStep, as: JavaType) + /// Use `placeholder` as the root when rendering `inner` (same idea as JNI `replacingPlaceholder`). + indirect case replacingPlaceholder(JavaConversionStep, placeholder: String) + + /// Build `org.swift.swiftkit.core.tuple.TupleN` from indirect `MemorySegment` out params (JNI `tupleFromOutParams`). + case tupleFromOutParams( + tupleClassName: String, + elements: [(outParamName: String, elementConversion: JavaConversionStep)] + ) + var isPlaceholder: Bool { if case .placeholder = self { true } else { false } } diff --git a/Tests/JExtractSwiftTests/FFM/FFMTupleTests.swift b/Tests/JExtractSwiftTests/FFM/FFMTupleTests.swift new file mode 100644 index 000000000..3da158bbb --- /dev/null +++ b/Tests/JExtractSwiftTests/FFM/FFMTupleTests.swift @@ -0,0 +1,133 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 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 + +/// FFM tuple binding tests, aligned with `JNITupleTests` where lowering allows it. +/// +/// JNI uses `(Int64, String)` for `returnPair` / `takePair`; FFM `lowerResult` does not support +/// `String` in tuple results yet, so those signatures use `(Int64, Int64)` here. +@Suite +struct FFMTupleTests { + let source = """ + public func returnPair() -> (Int64, Int64) + public func takePair(_ arg: (Int64, Int64)) + public func labeledTuple() -> (x: Int32, y: Int32) + """ + + @Test + func returnPair_javaBindings() throws { + try assertOutput( + input: source, + .ffm, + .java, + expectedChunks: [ + """ + public static org.swift.swiftkit.core.tuple.Tuple2 returnPair() { + try(var arena$ = Arena.ofConfined()) { + MemorySegment _result_0 = arena$.allocate(SwiftValueLayout.SWIFT_INT64); + MemorySegment _result_1 = arena$.allocate(SwiftValueLayout.SWIFT_INT64); + swiftjava_SwiftModule_returnPair.call(_result_0, _result_1); + return new org.swift.swiftkit.core.tuple.Tuple2<>(_result_0.get(SwiftValueLayout.SWIFT_INT64, 0), _result_1.get(SwiftValueLayout.SWIFT_INT64, 0)); + } + } + """, + ] + ) + } + + @Test + func takePair_javaBindings() throws { + try assertOutput( + input: source, + .ffm, + .java, + detectChunkByInitialLines: 2, + expectedChunks: [ + """ + public static void takePair(org.swift.swiftkit.core.tuple.Tuple2 arg) { + swiftjava_SwiftModule_takePair__.call(arg.$0, arg.$1); + } + """, + ] + ) + } + + @Test + func labeledTuple_javaBindings() throws { + try assertOutput( + input: source, + .ffm, + .java, + expectedChunks: [ + """ + public static org.swift.swiftkit.core.tuple.Tuple2 labeledTuple() { + """, + """ + return new org.swift.swiftkit.core.tuple.Tuple2<>(_result_0.get(SwiftValueLayout.SWIFT_INT32, 0), _result_1.get(SwiftValueLayout.SWIFT_INT32, 0)); + """, + ] + ) + } + + @Test + func returnPair_swiftThunks() throws { + try assertOutput( + input: source, + .ffm, + .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + let _result = returnPair() + _result_0.initialize(to: _result.0) + _result_1.initialize(to: _result.1) + """, + ] + ) + } + + @Test + func takePair_swiftThunks() throws { + try assertOutput( + input: source, + .ffm, + .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + takePair((arg_0, arg_1)) + """, + ] + ) + } + + @Test + func labeledTuple_swiftThunks() throws { + try assertOutput( + input: source, + .ffm, + .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + let _result = labeledTuple() + _result_0.initialize(to: _result.0) + _result_1.initialize(to: _result.1) + """, + ] + ) + } +} From 526bbce89ac82523061c27be0fe0dda84f018793 Mon Sep 17 00:00:00 2001 From: Aman Date: Thu, 19 Mar 2026 13:57:07 +0530 Subject: [PATCH 2/6] fix the format --- Tests/JExtractSwiftTests/FFM/FFMTupleTests.swift | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Tests/JExtractSwiftTests/FFM/FFMTupleTests.swift b/Tests/JExtractSwiftTests/FFM/FFMTupleTests.swift index 3da158bbb..eba64991c 100644 --- a/Tests/JExtractSwiftTests/FFM/FFMTupleTests.swift +++ b/Tests/JExtractSwiftTests/FFM/FFMTupleTests.swift @@ -43,7 +43,7 @@ struct FFMTupleTests { return new org.swift.swiftkit.core.tuple.Tuple2<>(_result_0.get(SwiftValueLayout.SWIFT_INT64, 0), _result_1.get(SwiftValueLayout.SWIFT_INT64, 0)); } } - """, + """ ] ) } @@ -60,7 +60,7 @@ struct FFMTupleTests { public static void takePair(org.swift.swiftkit.core.tuple.Tuple2 arg) { swiftjava_SwiftModule_takePair__.call(arg.$0, arg.$1); } - """, + """ ] ) } @@ -77,7 +77,7 @@ struct FFMTupleTests { """, """ return new org.swift.swiftkit.core.tuple.Tuple2<>(_result_0.get(SwiftValueLayout.SWIFT_INT32, 0), _result_1.get(SwiftValueLayout.SWIFT_INT32, 0)); - """, + """ ] ) } @@ -94,7 +94,7 @@ struct FFMTupleTests { let _result = returnPair() _result_0.initialize(to: _result.0) _result_1.initialize(to: _result.1) - """, + """ ] ) } @@ -109,7 +109,7 @@ struct FFMTupleTests { expectedChunks: [ """ takePair((arg_0, arg_1)) - """, + """ ] ) } @@ -126,7 +126,7 @@ struct FFMTupleTests { let _result = labeledTuple() _result_0.initialize(to: _result.0) _result_1.initialize(to: _result.1) - """, + """ ] ) } From 886beb91522efc220f0153cc2f09d7049464d890 Mon Sep 17 00:00:00 2001 From: Aman Date: Thu, 19 Mar 2026 14:09:14 +0530 Subject: [PATCH 3/6] implied the suggestions --- ...t2JavaGenerator+JavaBindingsPrinting.swift | 13 +++---- ...MSwift2JavaGenerator+JavaTranslation.swift | 17 +++------ .../FFM/FFMTupleTests.swift | 37 +++++++++++-------- 3 files changed, 31 insertions(+), 36 deletions(-) diff --git a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaBindingsPrinting.swift b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaBindingsPrinting.swift index 4f473f036..5eb47974b 100644 --- a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaBindingsPrinting.swift +++ b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaBindingsPrinting.swift @@ -771,14 +771,11 @@ extension FFMSwift2JavaGenerator.JavaConversionStep { return inner.render(&printer, root, placeholderForDowncall: placeholderForDowncall) case .tupleFromOutParams(let tupleClassName, let elements): - var args: [String] = [] - for element in elements { - args.append( - element.elementConversion.render( - &printer, - element.outParamName, - placeholderForDowncall: placeholderForDowncall - ) + let args = elements.map { element in + element.elementConversion.render( + &printer, + element.outParamName, + placeholderForDowncall: placeholderForDowncall ) } return "\(tupleClassName)(\(args.joined(separator: ", ")))" diff --git a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift index 185a6867b..221ef8891 100644 --- a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift +++ b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift @@ -474,7 +474,7 @@ extension FFMSwift2JavaGenerator { conversion: .placeholder ) - case .tuple(let elements) where !elements.isEmpty: + case .tuple(let elements): return try translateTupleParameter( elements: elements, convention: convention, @@ -484,9 +484,6 @@ extension FFMSwift2JavaGenerator { genericRequirements: genericRequirements ) - case .tuple: - throw JavaTranslationError.unhandledType(swiftType) - case .function: return TranslatedParameter( javaParameters: [ @@ -801,15 +798,12 @@ extension FFMSwift2JavaGenerator { conversion: .placeholder ) - case .tuple(let elements) where !elements.isEmpty: + case .tuple(let elements): return try translateTupleResult( elements: elements, resultAnnotations: resultAnnotations ) - case .tuple: - throw JavaTranslationError.unhandledType(swiftType) - case .array(let wrapped) where wrapped == knownTypes.uint8: return TranslatedResult( javaResultType: @@ -866,12 +860,11 @@ extension FFMSwift2JavaGenerator { } - /// Tuple results: indirect `MemorySegment` per element, then `new TupleN<>(…)` (mirrors JNI out-arrays). + /// Tuple results: indirect `MemorySegment` per element, then `new TupleN<…>(…)` (mirrors JNI out-arrays). func translateTupleResult( elements: [SwiftTupleElement], resultAnnotations: [JavaAnnotation] ) throws -> TranslatedResult { - let arity = elements.count var outParameters: [JavaParameter] = [] var tupleElements: [(outParamName: String, elementConversion: JavaConversionStep)] = [] var elementJavaTypes: [JavaType] = [] @@ -884,14 +877,14 @@ extension FFMSwift2JavaGenerator { } let javaResultType: JavaType = .tuple(elementTypes: elementJavaTypes) - let fullTupleClassName = "org.swift.swiftkit.core.tuple.Tuple\(arity)" + let fullTupleClassName = javaResultType.fullyQualifiedClassName! return TranslatedResult( javaResultType: javaResultType, annotations: resultAnnotations, outParameters: outParameters, conversion: .tupleFromOutParams( - tupleClassName: "new \(fullTupleClassName)<>", + tupleClassName: "new \(fullTupleClassName)", elements: tupleElements ) ) diff --git a/Tests/JExtractSwiftTests/FFM/FFMTupleTests.swift b/Tests/JExtractSwiftTests/FFM/FFMTupleTests.swift index eba64991c..055d1bb9c 100644 --- a/Tests/JExtractSwiftTests/FFM/FFMTupleTests.swift +++ b/Tests/JExtractSwiftTests/FFM/FFMTupleTests.swift @@ -15,10 +15,6 @@ import JExtractSwiftLib import Testing -/// FFM tuple binding tests, aligned with `JNITupleTests` where lowering allows it. -/// -/// JNI uses `(Int64, String)` for `returnPair` / `takePair`; FFM `lowerResult` does not support -/// `String` in tuple results yet, so those signatures use `(Int64, Int64)` here. @Suite struct FFMTupleTests { let source = """ @@ -40,7 +36,7 @@ struct FFMTupleTests { MemorySegment _result_0 = arena$.allocate(SwiftValueLayout.SWIFT_INT64); MemorySegment _result_1 = arena$.allocate(SwiftValueLayout.SWIFT_INT64); swiftjava_SwiftModule_returnPair.call(_result_0, _result_1); - return new org.swift.swiftkit.core.tuple.Tuple2<>(_result_0.get(SwiftValueLayout.SWIFT_INT64, 0), _result_1.get(SwiftValueLayout.SWIFT_INT64, 0)); + return new org.swift.swiftkit.core.tuple.Tuple2(_result_0.get(SwiftValueLayout.SWIFT_INT64, 0), _result_1.get(SwiftValueLayout.SWIFT_INT64, 0)); } } """ @@ -76,7 +72,7 @@ struct FFMTupleTests { public static org.swift.swiftkit.core.tuple.Tuple2 labeledTuple() { """, """ - return new org.swift.swiftkit.core.tuple.Tuple2<>(_result_0.get(SwiftValueLayout.SWIFT_INT32, 0), _result_1.get(SwiftValueLayout.SWIFT_INT32, 0)); + return new org.swift.swiftkit.core.tuple.Tuple2(_result_0.get(SwiftValueLayout.SWIFT_INT32, 0), _result_1.get(SwiftValueLayout.SWIFT_INT32, 0)); """ ] ) @@ -88,12 +84,15 @@ struct FFMTupleTests { input: source, .ffm, .swift, - detectChunkByInitialLines: 1, + detectChunkByInitialLines: 2, expectedChunks: [ """ - let _result = returnPair() - _result_0.initialize(to: _result.0) - _result_1.initialize(to: _result.1) + @_cdecl("swiftjava_SwiftModule_returnPair") + public func swiftjava_SwiftModule_returnPair(_ _result_0: UnsafeMutablePointer, _ _result_1: UnsafeMutablePointer) { + let _result = returnPair() + _result_0.initialize(to: _result.0) + _result_1.initialize(to: _result.1) + } """ ] ) @@ -105,10 +104,13 @@ struct FFMTupleTests { input: source, .ffm, .swift, - detectChunkByInitialLines: 1, + detectChunkByInitialLines: 2, expectedChunks: [ """ - takePair((arg_0, arg_1)) + @_cdecl("swiftjava_SwiftModule_takePair__") + public func swiftjava_SwiftModule_takePair__(_ arg_0: Int64, _ arg_1: Int64) { + takePair((arg_0, arg_1)) + } """ ] ) @@ -120,12 +122,15 @@ struct FFMTupleTests { input: source, .ffm, .swift, - detectChunkByInitialLines: 1, + detectChunkByInitialLines: 2, expectedChunks: [ """ - let _result = labeledTuple() - _result_0.initialize(to: _result.0) - _result_1.initialize(to: _result.1) + @_cdecl("swiftjava_SwiftModule_labeledTuple") + public func swiftjava_SwiftModule_labeledTuple(_ _result_0: UnsafeMutablePointer, _ _result_1: UnsafeMutablePointer) { + let _result = labeledTuple() + _result_0.initialize(to: _result.0) + _result_1.initialize(to: _result.1) + } """ ] ) From d29b03a0106a8bed56f05dba88d57aea2a2d33df Mon Sep 17 00:00:00 2001 From: Aman Date: Thu, 19 Mar 2026 14:49:22 +0530 Subject: [PATCH 4/6] added a runtime test and fixed the format --- .../MySwiftLibrary/MySwiftLibrary.swift | 13 +++++ .../java/com/example/swift/FFMTupleTest.java | 50 +++++++++++++++++++ .../FFM/FFMTupleTests.swift | 2 +- 3 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/FFMTupleTest.java diff --git a/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftLibrary.swift b/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftLibrary.swift index 95420d0ed..1e7f9b931 100644 --- a/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftLibrary.swift +++ b/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftLibrary.swift @@ -66,6 +66,19 @@ public func getArray() -> [UInt8] { [1, 2, 3] } +// Tuple round-trips for jextract FFM (homogeneous numeric pairs; see `FFMTupleTest` in the sample app). +public func ffmTupleReturnPair() -> (Int64, Int64) { + (42, 43) +} + +public func ffmTupleSumPair(_ arg: (Int64, Int64)) -> Int64 { + arg.0 + arg.1 +} + +public func ffmTupleLabeledPair() -> (x: Int32, y: Int32) { + (x: 10, y: 20) +} + public func sumAllByteArrayElements(actuallyAnArray: UnsafeRawPointer, count: Int) -> Int { let bufferPointer = UnsafeRawBufferPointer(start: actuallyAnArray, count: count) let array = Array(bufferPointer) diff --git a/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/FFMTupleTest.java b/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/FFMTupleTest.java new file mode 100644 index 000000000..f40bcc1b6 --- /dev/null +++ b/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/FFMTupleTest.java @@ -0,0 +1,50 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 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.tuple.Tuple2; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Runtime coverage for Swift tuples exported via jextract FFM (see {@code ffmTuple*} in {@link MySwiftLibrary}). + */ +public class FFMTupleTest { + + static { + System.loadLibrary(MySwiftLibrary.LIB_NAME); + } + + @Test + void ffmTupleReturnPair_roundTrip() { + Tuple2 result = MySwiftLibrary.ffmTupleReturnPair(); + assertEquals(42L, result.$0); + assertEquals(43L, result.$1); + } + + @Test + void ffmTupleSumPair_acceptsTupleFromJava() { + long sum = MySwiftLibrary.ffmTupleSumPair(new Tuple2<>(5L, 7L)); + assertEquals(12L, sum); + } + + @Test + void ffmTupleLabeledPair_preservesElementOrder() { + Tuple2 result = MySwiftLibrary.ffmTupleLabeledPair(); + assertEquals(10, result.$0); + assertEquals(20, result.$1); + } +} diff --git a/Tests/JExtractSwiftTests/FFM/FFMTupleTests.swift b/Tests/JExtractSwiftTests/FFM/FFMTupleTests.swift index 055d1bb9c..b83b04adb 100644 --- a/Tests/JExtractSwiftTests/FFM/FFMTupleTests.swift +++ b/Tests/JExtractSwiftTests/FFM/FFMTupleTests.swift @@ -73,7 +73,7 @@ struct FFMTupleTests { """, """ return new org.swift.swiftkit.core.tuple.Tuple2(_result_0.get(SwiftValueLayout.SWIFT_INT32, 0), _result_1.get(SwiftValueLayout.SWIFT_INT32, 0)); - """ + """, ] ) } From 8cac265957b781fe5ff42a4b296f97906286ef4b Mon Sep 17 00:00:00 2001 From: Konrad Malawski Date: Thu, 19 Mar 2026 22:29:03 +0900 Subject: [PATCH 5/6] change one of the tuples to mix types --- .../Sources/MySwiftLibrary/MySwiftLibrary.swift | 8 ++++---- .../src/test/java/com/example/swift/FFMTupleTest.java | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftLibrary.swift b/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftLibrary.swift index 1e7f9b931..f67e0bfc5 100644 --- a/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftLibrary.swift +++ b/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftLibrary.swift @@ -66,13 +66,13 @@ public func getArray() -> [UInt8] { [1, 2, 3] } -// Tuple round-trips for jextract FFM (homogeneous numeric pairs; see `FFMTupleTest` in the sample app). -public func ffmTupleReturnPair() -> (Int64, Int64) { +// Tuple round-trips for jextract FFM (see `FFMTupleTest` in the sample app). +public func ffmTupleReturnPair() -> (Int32, Int64) { (42, 43) } -public func ffmTupleSumPair(_ arg: (Int64, Int64)) -> Int64 { - arg.0 + arg.1 +public func ffmTupleSumPair(_ arg: (Int32, Int64)) -> Int64 { + Int64(arg.0) + arg.1 } public func ffmTupleLabeledPair() -> (x: Int32, y: Int32) { diff --git a/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/FFMTupleTest.java b/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/FFMTupleTest.java index f40bcc1b6..662dc846c 100644 --- a/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/FFMTupleTest.java +++ b/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/FFMTupleTest.java @@ -30,14 +30,14 @@ public class FFMTupleTest { @Test void ffmTupleReturnPair_roundTrip() { - Tuple2 result = MySwiftLibrary.ffmTupleReturnPair(); - assertEquals(42L, result.$0); + Tuple2 result = MySwiftLibrary.ffmTupleReturnPair(); + assertEquals(42, result.$0); assertEquals(43L, result.$1); } @Test void ffmTupleSumPair_acceptsTupleFromJava() { - long sum = MySwiftLibrary.ffmTupleSumPair(new Tuple2<>(5L, 7L)); + long sum = MySwiftLibrary.ffmTupleSumPair(new Tuple2<>(5, 7L)); assertEquals(12L, sum); } From a3d5068e8276ae5536d27d89bd648f3ceefbe26e Mon Sep 17 00:00:00 2001 From: Konrad Malawski Date: Thu, 19 Mar 2026 22:33:47 +0900 Subject: [PATCH 6/6] mark tuples as supported in feature list --- .../Documentation.docc/SupportedFeatures.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md b/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md index a3645e630..d38f6323a 100644 --- a/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md +++ b/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md @@ -65,7 +65,7 @@ SwiftJava's `swift-java jextract` tool automates generating Java bindings from S | Static functions or properties in generic type | ❌ | ❌ | | Generic parameters in functions: `func f(x: T)` | ❌ | ✅ | | Generic return values in functions: `func f() -> T` | ❌ | ❌ | -| Tuples: `(Int, String)`, `(A, B, C)` | ❌ | ❌ | +| Tuples: `(Int, String)`, `(A, B, C)` | ✅ | ✅ | | Protocols: `protocol` | ❌ | ✅ | | Protocols: `protocol` with associated types | ❌ | ❌ | | Protocols static requirements: `static func`, `init(rawValue:)` | ❌ | ❌ |