Skip to content

Commit 5e4bd76

Browse files
implemented tuple support for ffm (#632)
* implemented tuple support for ffm * fix the format * implied the suggestions * added a runtime test and fixed the format * change one of the tuples to mix types * mark tuples as supported in feature list --------- Co-authored-by: Konrad Malawski <ktoso@apple.com>
1 parent 96e1548 commit 5e4bd76

6 files changed

Lines changed: 374 additions & 8 deletions

File tree

Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftLibrary.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,19 @@ public func getArray() -> [UInt8] {
6666
[1, 2, 3]
6767
}
6868

69+
// Tuple round-trips for jextract FFM (see `FFMTupleTest` in the sample app).
70+
public func ffmTupleReturnPair() -> (Int32, Int64) {
71+
(42, 43)
72+
}
73+
74+
public func ffmTupleSumPair(_ arg: (Int32, Int64)) -> Int64 {
75+
Int64(arg.0) + arg.1
76+
}
77+
78+
public func ffmTupleLabeledPair() -> (x: Int32, y: Int32) {
79+
(x: 10, y: 20)
80+
}
81+
6982
public func sumAllByteArrayElements(actuallyAnArray: UnsafeRawPointer, count: Int) -> Int {
7083
let bufferPointer = UnsafeRawBufferPointer(start: actuallyAnArray, count: count)
7184
let array = Array(bufferPointer)
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2025 Apple Inc. and the Swift.org project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of Swift.org project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
package com.example.swift;
16+
17+
import org.junit.jupiter.api.Test;
18+
import org.swift.swiftkit.core.tuple.Tuple2;
19+
20+
import static org.junit.jupiter.api.Assertions.*;
21+
22+
/**
23+
* Runtime coverage for Swift tuples exported via jextract FFM (see {@code ffmTuple*} in {@link MySwiftLibrary}).
24+
*/
25+
public class FFMTupleTest {
26+
27+
static {
28+
System.loadLibrary(MySwiftLibrary.LIB_NAME);
29+
}
30+
31+
@Test
32+
void ffmTupleReturnPair_roundTrip() {
33+
Tuple2<Integer, Long> result = MySwiftLibrary.ffmTupleReturnPair();
34+
assertEquals(42, result.$0);
35+
assertEquals(43L, result.$1);
36+
}
37+
38+
@Test
39+
void ffmTupleSumPair_acceptsTupleFromJava() {
40+
long sum = MySwiftLibrary.ffmTupleSumPair(new Tuple2<>(5, 7L));
41+
assertEquals(12L, sum);
42+
}
43+
44+
@Test
45+
void ffmTupleLabeledPair_preservesElementOrder() {
46+
Tuple2<Integer, Integer> result = MySwiftLibrary.ffmTupleLabeledPair();
47+
assertEquals(10, result.$0);
48+
assertEquals(20, result.$1);
49+
}
50+
}

Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaBindingsPrinting.swift

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -622,6 +622,12 @@ extension FFMSwift2JavaGenerator.JavaConversionStep {
622622

623623
case .commaSeparated(let list, _):
624624
return list.contains(where: { $0.requiresSwiftArena })
625+
626+
case .replacingPlaceholder(let inner, _):
627+
return inner.requiresSwiftArena
628+
629+
case .tupleFromOutParams(_, let elements):
630+
return elements.contains(where: { $0.elementConversion.requiresSwiftArena })
625631
}
626632
}
627633

@@ -654,6 +660,12 @@ extension FFMSwift2JavaGenerator.JavaConversionStep {
654660
return inner.requiresTemporaryArena
655661
case .commaSeparated(let list, _):
656662
return list.contains(where: { $0.requiresTemporaryArena })
663+
664+
case .replacingPlaceholder(let inner, _):
665+
return inner.requiresTemporaryArena
666+
667+
case .tupleFromOutParams(_, let elements):
668+
return elements.contains(where: { $0.elementConversion.requiresTemporaryArena })
657669
}
658670
}
659671

@@ -754,6 +766,19 @@ extension FFMSwift2JavaGenerator.JavaConversionStep {
754766
case .readMemorySegment(let inner, let javaType):
755767
let inner = inner.render(&printer, placeholder)
756768
return "\(inner).get(\(ForeignValueLayout(javaType: javaType)!), 0)"
769+
770+
case .replacingPlaceholder(let inner, let root):
771+
return inner.render(&printer, root, placeholderForDowncall: placeholderForDowncall)
772+
773+
case .tupleFromOutParams(let tupleClassName, let elements):
774+
let args = elements.map { element in
775+
element.elementConversion.render(
776+
&printer,
777+
element.outParamName,
778+
placeholderForDowncall: placeholderForDowncall
779+
)
780+
}
781+
return "\(tupleClassName)(\(args.joined(separator: ", ")))"
757782
}
758783
}
759784
}

Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift

Lines changed: 147 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ extension FFMSwift2JavaGenerator {
191191
let translatedClosure = try translateFunctionType(name: paramName, swiftType: funcTy, cdeclType: cdeclTy)
192192
funcTypes.append(translatedClosure)
193193
case .tuple:
194-
// TODO: Implement
194+
// Tuple-typed closure parameters are not supported (same as JNI / lowering).
195195
break
196196
default:
197197
break
@@ -462,9 +462,27 @@ extension FFMSwift2JavaGenerator {
462462
conversion: .swiftValueSelfSegment(.placeholder)
463463
)
464464

465-
case .tuple:
466-
// TODO: Implement.
467-
throw JavaTranslationError.unhandledType(swiftType)
465+
case .tuple([]):
466+
return TranslatedParameter(
467+
javaParameters: [
468+
JavaParameter(
469+
name: parameterName,
470+
type: .void,
471+
annotations: parameterAnnotations
472+
)
473+
],
474+
conversion: .placeholder
475+
)
476+
477+
case .tuple(let elements):
478+
return try translateTupleParameter(
479+
elements: elements,
480+
convention: convention,
481+
parameterName: parameterName,
482+
methodName: methodName,
483+
genericParameters: genericParameters,
484+
genericRequirements: genericRequirements
485+
)
468486

469487
case .function:
470488
return TranslatedParameter(
@@ -539,6 +557,56 @@ extension FFMSwift2JavaGenerator {
539557
}
540558
}
541559

560+
/// Tuple parameters: one `TupleN<…>` on the Java API; conversion reads `.$0`, `.$1`, … (mirrors JNI).
561+
func translateTupleParameter(
562+
elements: [SwiftTupleElement],
563+
convention: SwiftParameterConvention,
564+
parameterName: String,
565+
methodName: String,
566+
genericParameters: [SwiftGenericParameterDeclaration],
567+
genericRequirements: [SwiftGenericRequirement]
568+
) throws -> TranslatedParameter {
569+
let lowering = CdeclLowering(knownTypes: knownTypes)
570+
var elementJavaTypes: [JavaType] = []
571+
var elementConversions: [JavaConversionStep] = []
572+
573+
for (idx, element) in elements.enumerated() {
574+
let subLowered = try lowering.lowerParameter(
575+
element.type,
576+
convention: convention,
577+
parameterName: "\(parameterName)_\(idx)",
578+
genericParameters: genericParameters,
579+
genericRequirements: genericRequirements
580+
)
581+
let elementTranslated = try translateParameter(
582+
type: element.type,
583+
convention: convention,
584+
parameterName: "\(parameterName)_\(idx)",
585+
loweredParam: subLowered,
586+
methodName: methodName,
587+
genericParameters: genericParameters,
588+
genericRequirements: genericRequirements
589+
)
590+
guard elementTranslated.javaParameters.count == 1 else {
591+
throw JavaTranslationError.unhandledType(element.type)
592+
}
593+
let extraction = JavaConversionStep.replacingPlaceholder(
594+
elementTranslated.conversion,
595+
placeholder: "\(parameterName).$\(idx)"
596+
)
597+
elementConversions.append(extraction)
598+
elementJavaTypes.append(elementTranslated.javaParameters[0].type.javaType)
599+
}
600+
601+
let javaType: JavaType = .tuple(elementTypes: elementJavaTypes)
602+
return TranslatedParameter(
603+
javaParameters: [
604+
JavaParameter(name: parameterName, type: javaType)
605+
],
606+
conversion: .commaSeparated(elementConversions)
607+
)
608+
}
609+
542610
/// Translate an Optional Swift API parameter to the user-facing Java API parameter.
543611
func translateOptionalParameter(
544612
wrappedType swiftType: SwiftType,
@@ -722,9 +790,19 @@ extension FFMSwift2JavaGenerator {
722790
conversion: .wrapMemoryAddressUnsafe(.placeholder, javaType)
723791
)
724792

725-
case .tuple:
726-
// TODO: Implement.
727-
throw JavaTranslationError.unhandledType(swiftType)
793+
case .tuple([]):
794+
return TranslatedResult(
795+
javaResultType: .void,
796+
annotations: resultAnnotations,
797+
outParameters: [],
798+
conversion: .placeholder
799+
)
800+
801+
case .tuple(let elements):
802+
return try translateTupleResult(
803+
elements: elements,
804+
resultAnnotations: resultAnnotations
805+
)
728806

729807
case .array(let wrapped) where wrapped == knownTypes.uint8:
730808
return TranslatedResult(
@@ -782,6 +860,59 @@ extension FFMSwift2JavaGenerator {
782860

783861
}
784862

863+
/// Tuple results: indirect `MemorySegment` per element, then `new TupleN<…>(…)` (mirrors JNI out-arrays).
864+
func translateTupleResult(
865+
elements: [SwiftTupleElement],
866+
resultAnnotations: [JavaAnnotation]
867+
) throws -> TranslatedResult {
868+
var outParameters: [JavaParameter] = []
869+
var tupleElements: [(outParamName: String, elementConversion: JavaConversionStep)] = []
870+
var elementJavaTypes: [JavaType] = []
871+
872+
for (idx, element) in elements.enumerated() {
873+
let (javaType, elementConversion) = try translateTupleElementResult(type: element.type)
874+
outParameters.append(JavaParameter(name: "\(idx)", type: javaType))
875+
tupleElements.append((outParamName: "_result_\(idx)", elementConversion: elementConversion))
876+
elementJavaTypes.append(javaType)
877+
}
878+
879+
let javaResultType: JavaType = .tuple(elementTypes: elementJavaTypes)
880+
let fullTupleClassName = javaResultType.fullyQualifiedClassName!
881+
882+
return TranslatedResult(
883+
javaResultType: javaResultType,
884+
annotations: resultAnnotations,
885+
outParameters: outParameters,
886+
conversion: .tupleFromOutParams(
887+
tupleClassName: "new \(fullTupleClassName)",
888+
elements: tupleElements
889+
)
890+
)
891+
}
892+
893+
/// Single tuple element for the Java result (mirrors JNI `translateTupleElementResult`).
894+
private func translateTupleElementResult(type: SwiftType) throws -> (JavaType, JavaConversionStep) {
895+
switch type {
896+
case .nominal(let nominalType):
897+
if nominalType.nominalTypeDecl.knownTypeKind != nil {
898+
if let cType = try? CType(cdeclType: type) {
899+
return (cType.javaType, .readMemorySegment(.placeholder, as: cType.javaType))
900+
}
901+
throw JavaTranslationError.unhandledType(type)
902+
}
903+
904+
guard !nominalType.isSwiftJavaWrapper else {
905+
throw JavaTranslationError.unhandledType(type)
906+
}
907+
908+
let javaType: JavaType = .class(package: nil, name: nominalType.nominalTypeDecl.qualifiedName)
909+
return (javaType, .wrapMemoryAddressUnsafe(.placeholder, javaType))
910+
911+
default:
912+
throw JavaTranslationError.unhandledType(type)
913+
}
914+
}
915+
785916
func translate(
786917
swiftType: SwiftType
787918
) throws -> JavaType {
@@ -876,6 +1007,15 @@ extension FFMSwift2JavaGenerator {
8761007
/// Refer an exploded argument suffixed with `_\(name)`.
8771008
indirect case readMemorySegment(JavaConversionStep, as: JavaType)
8781009

1010+
/// Use `placeholder` as the root when rendering `inner` (same idea as JNI `replacingPlaceholder`).
1011+
indirect case replacingPlaceholder(JavaConversionStep, placeholder: String)
1012+
1013+
/// Build `org.swift.swiftkit.core.tuple.TupleN` from indirect `MemorySegment` out params (JNI `tupleFromOutParams`).
1014+
case tupleFromOutParams(
1015+
tupleClassName: String,
1016+
elements: [(outParamName: String, elementConversion: JavaConversionStep)]
1017+
)
1018+
8791019
var isPlaceholder: Bool {
8801020
if case .placeholder = self { true } else { false }
8811021
}

Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ SwiftJava's `swift-java jextract` tool automates generating Java bindings from S
6565
| Static functions or properties in generic type |||
6666
| Generic parameters in functions: `func f<T: A & B>(x: T)` |||
6767
| Generic return values in functions: `func f<T: A & B>() -> T` |||
68-
| Tuples: `(Int, String)`, `(A, B, C)` | | |
68+
| Tuples: `(Int, String)`, `(A, B, C)` | | |
6969
| Protocols: `protocol` |||
7070
| Protocols: `protocol` with associated types |||
7171
| Protocols static requirements: `static func`, `init(rawValue:)` |||

0 commit comments

Comments
 (0)