Skip to content

Commit c37a07c

Browse files
committed
Tuple support in jextract/jni
1 parent cdffc6c commit c37a07c

11 files changed

Lines changed: 578 additions & 13 deletions

File tree

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
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+
public func returnPair() -> (Int64, String) {
16+
(42, "hello")
17+
}
18+
19+
public func takePair(pair: (Int64, String)) -> String {
20+
"\(pair.0):\(pair.1)"
21+
}
22+
23+
public func labeledTuple() -> (x: Int32, y: Int32) {
24+
(x: 10, y: 20)
25+
}
26+
27+
public func echoTriple(triple: (Bool, Double, Int64)) -> (Bool, Double, Int64) {
28+
triple
29+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
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 com.example.swift.MySwiftLibrary;
18+
import org.junit.jupiter.api.Test;
19+
import org.swift.swiftkit.core.tuple.Tuple2;
20+
import org.swift.swiftkit.core.tuple.Tuple3;
21+
22+
import static org.junit.jupiter.api.Assertions.*;
23+
24+
public class TupleTest {
25+
@Test
26+
void returnPair() {
27+
Tuple2<Long, String> result = MySwiftLibrary.returnPair();
28+
assertEquals(42L, result.$0());
29+
assertEquals("hello", result.$1());
30+
}
31+
32+
@Test
33+
void takePair() {
34+
String result = MySwiftLibrary.takePair(new Tuple2<>(99L, "world"));
35+
assertEquals("99:world", result);
36+
}
37+
38+
@Test
39+
void labeledTuple() {
40+
Tuple2<Integer, Integer> result = MySwiftLibrary.labeledTuple();
41+
assertEquals(10, result.$0());
42+
assertEquals(20, result.$1());
43+
}
44+
45+
@Test
46+
void echoTriple() {
47+
Tuple3<Boolean, Double, Long> input = new Tuple3<>(true, 3.14, 100L);
48+
Tuple3<Boolean, Double, Long> result = MySwiftLibrary.echoTriple(input);
49+
assertEquals(true, result.$0());
50+
assertEquals(3.14, result.$1(), 0.001);
51+
assertEquals(100L, result.$2());
52+
}
53+
}

Sources/JExtractSwiftLib/Convenience/JavaType+Extensions.swift

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,20 @@ extension JavaType {
102102
}
103103
}
104104

105+
var jniSetArrayRegionMethodName: String {
106+
switch self {
107+
case .boolean: "SetBooleanArrayRegion"
108+
case .byte: "SetByteArrayRegion"
109+
case .char: "SetCharArrayRegion"
110+
case .short: "SetShortArrayRegion"
111+
case .int: "SetIntArrayRegion"
112+
case .long: "SetLongArrayRegion"
113+
case .float: "SetFloatArrayRegion"
114+
case .double: "SetDoubleArrayRegion"
115+
default: fatalError("Set*ArrayRegion is only available for JNI primitive types, was: \(self)")
116+
}
117+
}
118+
105119
/// Returns whether this type returns `JavaValue` from SwiftJava
106120
var implementsJavaValue: Bool {
107121
switch self {
@@ -146,4 +160,22 @@ extension JavaType {
146160
false
147161
}
148162
}
163+
164+
/// The boxed class name for this type, suitable for use as a generic type argument.
165+
var boxedName: String {
166+
switch self {
167+
case .boolean: "Boolean"
168+
case .byte: "Byte"
169+
case .char: "Character"
170+
case .short: "Short"
171+
case .int: "Integer"
172+
case .long: "Long"
173+
case .float: "Float"
174+
case .double: "Double"
175+
case .void: "Void"
176+
case .javaLangString: "String"
177+
case .class(_, let name, _): name
178+
case .array: description
179+
}
180+
}
149181
}

Sources/JExtractSwiftLib/FFM/CDeclLowering/FFMSwift2JavaGenerator+FunctionLowering.swift

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -325,7 +325,7 @@ struct CdeclLowering {
325325
case .tuple(let tuple):
326326
if tuple.count == 1 {
327327
return try lowerParameter(
328-
tuple[0],
328+
tuple[0].type,
329329
convention: convention,
330330
parameterName: parameterName,
331331
genericParameters: genericParameters,
@@ -341,7 +341,7 @@ struct CdeclLowering {
341341
// FIXME: Use tuple element label.
342342
let cdeclName = "\(parameterName)_\(idx)"
343343
let lowered = try lowerParameter(
344-
element,
344+
element.type,
345345
convention: convention,
346346
parameterName: cdeclName,
347347
genericParameters: genericParameters,
@@ -518,7 +518,7 @@ struct CdeclLowering {
518518
case .tuple(let tuple):
519519
if tuple.count == 1 {
520520
return try lowerOptionalParameter(
521-
tuple[0],
521+
tuple[0].type,
522522
convention: convention,
523523
parameterName: parameterName,
524524
genericParameters: genericParameters,
@@ -697,8 +697,8 @@ struct CdeclLowering {
697697
let isMutable = knownType == .unsafeMutableBufferPointer
698698
return try lowerResult(
699699
.tuple([
700-
isMutable ? knownTypes.unsafeMutableRawPointer : knownTypes.unsafeRawPointer,
701-
knownTypes.int,
700+
SwiftTupleElement(label: nil, type: isMutable ? knownTypes.unsafeMutableRawPointer : knownTypes.unsafeRawPointer),
701+
SwiftTupleElement(label: nil, type: knownTypes.int),
702702
]),
703703
outParameterName: outParameterName
704704
)
@@ -755,14 +755,14 @@ struct CdeclLowering {
755755

756756
case .tuple(let tuple):
757757
if tuple.count == 1 {
758-
return try lowerResult(tuple[0], outParameterName: outParameterName)
758+
return try lowerResult(tuple[0].type, outParameterName: outParameterName)
759759
}
760760

761761
var parameters: [SwiftParameter] = []
762762
var conversions: [ConversionStep] = []
763763
for (idx, element) in tuple.enumerated() {
764764
let outName = "\(outParameterName)_\(idx)"
765-
let lowered = try lowerResult(element, outParameterName: outName)
765+
let lowered = try lowerResult(element.type, outParameterName: outName)
766766

767767
// Convert direct return values to typed mutable pointers.
768768
// E.g. (Int8, Int8) is lowered to '_ result_0: UnsafePointer<Int8>, _ result_1: UnsafePointer<Int8>'

Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -585,7 +585,7 @@ extension FFMSwift2JavaGenerator {
585585
case .tuple(let tuple):
586586
if tuple.count == 1 {
587587
return try translateOptionalParameter(
588-
wrappedType: tuple[0],
588+
wrappedType: tuple[0].type,
589589
convention: convention,
590590
parameterName: parameterName,
591591
loweredParam: loweredParam,

Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -579,11 +579,68 @@ extension JNISwift2JavaGenerator {
579579
conversion: .typeMetadataAddress(.placeholder)
580580
)
581581

582+
case .tuple(let elements) where !elements.isEmpty:
583+
return try translateTupleParameter(
584+
elements: elements,
585+
parameterName: parameterName,
586+
methodName: methodName,
587+
parentName: parentName,
588+
genericParameters: genericParameters,
589+
genericRequirements: genericRequirements,
590+
parameterPosition: parameterPosition
591+
)
592+
582593
case .tuple, .composite:
583594
throw JavaTranslationError.unsupportedSwiftType(swiftType)
584595
}
585596
}
586597

598+
func translateTupleParameter(
599+
elements: [SwiftTupleElement],
600+
parameterName: String,
601+
methodName: String,
602+
parentName: String,
603+
genericParameters: [SwiftGenericParameterDeclaration],
604+
genericRequirements: [SwiftGenericRequirement],
605+
parameterPosition: Int?
606+
) throws -> TranslatedParameter {
607+
let arity = elements.count
608+
var elementBoxedTypes: [String] = []
609+
610+
// Generate a conversion that extracts each element from the Tuple record
611+
var elementConversions: [JavaNativeConversionStep] = []
612+
for (idx, element) in elements.enumerated() {
613+
let elementTranslated = try translateParameter(
614+
swiftType: element.type,
615+
parameterName: "\(parameterName)_\(idx)",
616+
methodName: methodName,
617+
parentName: parentName,
618+
genericParameters: genericParameters,
619+
genericRequirements: genericRequirements,
620+
parameterPosition: parameterPosition
621+
)
622+
623+
// Extract the element from the tuple using .$N() accessor
624+
let extraction = JavaNativeConversionStep.replacingPlaceholder(
625+
elementTranslated.conversion,
626+
placeholder: "\(parameterName).$\(idx)()"
627+
)
628+
elementConversions.append(extraction)
629+
elementBoxedTypes.append(elementTranslated.parameter.type.javaType.boxedName)
630+
}
631+
632+
let genericParams = elementBoxedTypes.joined(separator: ", ")
633+
let javaType: JavaType = .class(package: "org.swift.swiftkit.core.tuple", name: "Tuple\(arity)<\(genericParams)>")
634+
635+
return TranslatedParameter(
636+
parameter: JavaParameter(
637+
name: parameterName,
638+
type: javaType
639+
),
640+
conversion: .commaSeparated(elementConversions)
641+
)
642+
}
643+
587644
func convertToAsync(
588645
translatedFunctionSignature: inout TranslatedFunctionSignature,
589646
nativeFunctionSignature: inout NativeFunctionSignature,
@@ -887,11 +944,83 @@ extension JNISwift2JavaGenerator {
887944
elementType: elementType
888945
)
889946

947+
case .tuple(let elements) where !elements.isEmpty:
948+
return try translateTupleResult(elements: elements, resultName: resultName)
949+
890950
case .metatype, .tuple, .function, .existential, .opaque, .genericParameter, .composite:
891951
throw JavaTranslationError.unsupportedSwiftType(swiftType)
892952
}
893953
}
894954

955+
func translateTupleResult(
956+
elements: [SwiftTupleElement],
957+
resultName: String = "result"
958+
) throws -> TranslatedResult {
959+
let arity = elements.count
960+
var outParameters: [OutParameter] = []
961+
var elementOutParamNames: [String] = []
962+
var elementConversions: [JavaNativeConversionStep] = []
963+
var elementBoxedTypes: [String] = []
964+
965+
for (idx, element) in elements.enumerated() {
966+
let outParamName = "\(resultName)_\(idx)$"
967+
968+
// Determine the Java type for this element
969+
let (javaType, elementConversion) = try translateTupleElementResult(type: element.type)
970+
let arrayType: JavaType = .array(javaType)
971+
972+
outParameters.append(
973+
OutParameter(name: outParamName, type: arrayType, allocation: .newArray(javaType, size: 1))
974+
)
975+
elementOutParamNames.append(outParamName)
976+
elementConversions.append(elementConversion)
977+
elementBoxedTypes.append(javaType.boxedName)
978+
}
979+
980+
let genericParams = elementBoxedTypes.joined(separator: ", ")
981+
let tupleClassName = "Tuple\(arity)<\(genericParams)>"
982+
let fullTupleClassName = "org.swift.swiftkit.core.tuple.Tuple\(arity)"
983+
let javaResultType: JavaType = .class(package: "org.swift.swiftkit.core.tuple", name: tupleClassName)
984+
985+
let tupleElements: [(outParamName: String, elementConversion: JavaNativeConversionStep)] =
986+
zip(elementOutParamNames, elementConversions).map { ($0, $1) }
987+
988+
return TranslatedResult(
989+
javaType: javaResultType,
990+
outParameters: outParameters,
991+
conversion: .tupleFromOutParams(
992+
tupleClassName: "new \(fullTupleClassName)<>",
993+
elements: tupleElements
994+
)
995+
)
996+
}
997+
998+
/// Translate a single element type for tuple results on the Java side.
999+
private func translateTupleElementResult(type: SwiftType) throws -> (JavaType, JavaNativeConversionStep) {
1000+
switch type {
1001+
case .nominal(let nominalType):
1002+
if let knownType = nominalType.nominalTypeDecl.knownTypeKind {
1003+
guard let javaType = JNIJavaTypeTranslator.translate(knownType: knownType, config: self.config) else {
1004+
throw JavaTranslationError.unsupportedSwiftType(type)
1005+
}
1006+
// Primitives: just read from array
1007+
return (javaType, .placeholder)
1008+
}
1009+
1010+
let nominalTypeName = nominalType.nominalTypeDecl.name
1011+
guard !nominalType.isSwiftJavaWrapper else {
1012+
throw JavaTranslationError.unsupportedSwiftType(type)
1013+
}
1014+
1015+
// JExtract class: wrap memory address
1016+
let javaType: JavaType = .class(package: nil, name: nominalTypeName)
1017+
return (.long, .constructSwiftValue(.placeholder, javaType))
1018+
1019+
default:
1020+
throw JavaTranslationError.unsupportedSwiftType(type)
1021+
}
1022+
}
1023+
8951024
func translateOptionalResult(
8961025
wrappedType swiftType: SwiftType,
8971026
resultName: String = "result"
@@ -1323,6 +1452,10 @@ extension JNISwift2JavaGenerator {
13231452

13241453
indirect case requireNonNull(JavaNativeConversionStep, message: String)
13251454

1455+
/// Constructs a TupleN from out-parameter arrays.
1456+
/// E.g. `new Tuple2<>(result_0$[0], result_1$[0])`
1457+
case tupleFromOutParams(tupleClassName: String, elements: [(outParamName: String, elementConversion: JavaNativeConversionStep)])
1458+
13261459
/// `Arrays.stream(args)`
13271460
static func arraysStream(_ argument: JavaNativeConversionStep) -> JavaNativeConversionStep {
13281461
.method(.constant("Arrays"), function: "stream", arguments: [argument])
@@ -1473,6 +1606,16 @@ extension JNISwift2JavaGenerator {
14731606
case .requireNonNull(let inner, let message):
14741607
let inner = inner.render(&printer, placeholder)
14751608
return #"Objects.requireNonNull(\#(inner), "\#(message)")"#
1609+
1610+
case .tupleFromOutParams(let tupleClassName, let elements):
1611+
// Execute the native call first (the placeholder is the downcall expression)
1612+
printer.print("\(placeholder);")
1613+
var args: [String] = []
1614+
for element in elements {
1615+
let converted = element.elementConversion.render(&printer, "\(element.outParamName)[0]")
1616+
args.append(converted)
1617+
}
1618+
return "\(tupleClassName)(\(args.joined(separator: ", ")))"
14761619
}
14771620
}
14781621

@@ -1535,6 +1678,9 @@ extension JNISwift2JavaGenerator {
15351678

15361679
case .requireNonNull(let inner, _):
15371680
return inner.requiresSwiftArena
1681+
1682+
case .tupleFromOutParams(_, let elements):
1683+
return elements.contains(where: { $0.elementConversion.requiresSwiftArena })
15381684
}
15391685
}
15401686
}

0 commit comments

Comments
 (0)