Skip to content

Commit 7c0ec17

Browse files
committed
jextract: LabeledTuple support!
We now support passing and returing labelled tuples like (a: Int, b: Stirng) etc. This is important for being able to quickly port over code from swift to Java using the same libraries as we don't have to special case the labelled tuples and move them over to positional use. These ad hoc tuple types are printed per method. They would conflict if you use the same shape twice in a method right now... I'm not seeing much of that use so ignored it for now. The new tuples inherit from TupleN so Java code can just use them as positional when necessary, also for passing them along to other methods. The change is large because I also cleaned up type printing, I think printing full qualified type names is good here, better nor risk clashes.
1 parent 0bdba49 commit 7c0ec17

42 files changed

Lines changed: 432 additions & 125 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/TupleTest.java

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414

1515
package com.example.swift;
1616

17-
import com.example.swift.MySwiftLibrary;
1817
import org.junit.jupiter.api.Test;
1918
import org.swift.swiftkit.core.tuple.Tuple2;
2019
import org.swift.swiftkit.core.tuple.Tuple3;
@@ -38,9 +37,19 @@ void takePair() {
3837

3938
@Test
4039
void labeledTuple() {
41-
Tuple2<Integer, Integer> result = MySwiftLibrary.labeledTuple();
40+
var result = MySwiftLibrary.labeledTuple();
41+
// Access via named accessors
42+
assertEquals(10, result.x());
43+
assertEquals(20, result.y());
44+
// Positional access still works (inherited from Tuple2)
4245
assertEquals(10, result.$0);
4346
assertEquals(20, result.$1);
47+
48+
// The labelled tuple is a subclass of Tuple2
49+
assertInstanceOf(Tuple2.class, result);
50+
// And the generic types match positionally as well
51+
@SuppressWarnings("unused")
52+
Tuple2<Integer, Integer> check = result;
4453
}
4554

4655
@Test

Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaBindingsPrinting.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -388,10 +388,10 @@ extension FFMSwift2JavaGenerator {
388388
// If a Swift function is 'throws' we throw a checked error for the Java side
389389
// TODO: When we support typed throws on Swift side we'll want to throw the right type here instead
390390
if translatedSignature.isThrowing {
391-
throwsClauses.append(JavaType.swiftJavaErrorException.simpleClassName)
391+
throwsClauses.append(JavaType.swiftJavaErrorException.className!)
392392
}
393393
if translatedSignature.canThrowSwiftIntegerOverflowException {
394-
throwsClauses.append(JavaType.swiftIntegerOverflowException.simpleClassName)
394+
throwsClauses.append(JavaType.swiftIntegerOverflowException.className!)
395395
}
396396
let throwsClause = throwsClauses.isEmpty ? "" : " throws \(throwsClauses.joined(separator: ", "))"
397397

@@ -522,7 +522,7 @@ extension FFMSwift2JavaGenerator {
522522
func printErrorCheck(_ printer: inout CodePrinter) {
523523
guard translatedSignature.isThrowing else { return }
524524
printer.printIfBlock("!result$throws.get(ValueLayout.ADDRESS, 0).equals(MemorySegment.NULL)") { printer in
525-
printer.print("throw new \(JavaType.swiftJavaErrorException.simpleClassName)(result$throws.get(ValueLayout.ADDRESS, 0), AllocatingSwiftArena.ofAuto());")
525+
printer.print("throw new \(JavaType.swiftJavaErrorException.className!)(result$throws.get(ValueLayout.ADDRESS, 0), AllocatingSwiftArena.ofAuto());")
526526
}
527527
}
528528

Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -335,7 +335,8 @@ extension FFMSwift2JavaGenerator {
335335
// Result.
336336
let result = try self.translateResult(
337337
swiftResult: swiftSignature.result,
338-
loweredResult: loweredFunctionSignature.result
338+
loweredResult: loweredFunctionSignature.result,
339+
methodName: methodName
339340
)
340341

341342
return TranslatedFunctionSignature(
@@ -620,8 +621,7 @@ extension FFMSwift2JavaGenerator {
620621
case .char: ("Optional<Character>", "toOptionalSegmentCharacter")
621622
case .short: ("Optional<Short>", "toOptionalSegmentShort")
622623
case .float: ("Optional<Float>", "toOptionalSegmentFloat")
623-
default:
624-
throw JavaTranslationError.unhandledType(known: .optional(swiftType))
624+
default: throw JavaTranslationError.unhandledType(known: .optional(swiftType))
625625
}
626626
return TranslatedParameter(
627627
javaParameters: [
@@ -689,7 +689,8 @@ extension FFMSwift2JavaGenerator {
689689
/// Translate a Swift API result to the user-facing Java API result.
690690
func translateResult(
691691
swiftResult: SwiftResult,
692-
loweredResult: LoweredResult
692+
loweredResult: LoweredResult,
693+
methodName: String
693694
) throws -> TranslatedResult {
694695
let swiftType = swiftResult.type
695696
// If the result type should cause any annotations on the method, include them here.
@@ -844,6 +845,7 @@ extension FFMSwift2JavaGenerator {
844845

845846
case .tuple(let elements):
846847
return try translateTupleResult(
848+
methodName: methodName,
847849
elements: elements,
848850
resultAnnotations: resultAnnotations
849851
)
@@ -856,6 +858,7 @@ extension FFMSwift2JavaGenerator {
856858

857859
/// Tuple results: indirect `MemorySegment` per element, then `new TupleN<…>(…)` (mirrors JNI out-arrays).
858860
func translateTupleResult(
861+
methodName: String,
859862
elements: [SwiftTupleElement],
860863
resultAnnotations: [JavaAnnotation]
861864
) throws -> TranslatedResult {

Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ extension FFMSwift2JavaGenerator {
151151
/// Returns the Java class name for a nominal type, applying known-type overrides
152152
func javaClassName(for decl: ImportedNominalType) -> String {
153153
if decl.swiftNominal.knownTypeKind == .swiftJavaError {
154-
return JavaType.swiftJavaErrorException.simpleClassName
154+
return JavaType.swiftJavaErrorException.className!
155155
}
156156
return decl.swiftNominal.name
157157
}

Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -524,6 +524,10 @@ extension JNISwift2JavaGenerator {
524524
printJavaBindingWrapperHelperClass(&printer, decl)
525525

526526
printJavaBindingWrapperMethod(&printer, decl, skipMethodBody: skipMethodBody)
527+
528+
// Print any additional types we may need to emit, e.g. named tuples are emitted as static classes
529+
// right next to the func that is using them.
530+
printNecessarySupportTypes(&printer, decl)
527531
}
528532

529533
/// Print the helper type container for a user-facing Java API.
@@ -566,6 +570,17 @@ extension JNISwift2JavaGenerator {
566570
)
567571
}
568572

573+
private func printNecessarySupportTypes(
574+
_ printer: inout CodePrinter,
575+
_ decl: ImportedFunc
576+
) {
577+
let translatedDecl = translatedDecl(for: decl)!
578+
579+
for labeledTuple in translatedDecl.usedLabeledTuples {
580+
printAdHocLabeledTupleStaticClass(&printer, labeledTuple)
581+
}
582+
}
583+
569584
private func printJavaBindingWrapperMethod(
570585
_ printer: inout CodePrinter,
571586
_ decl: ImportedFunc,

Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift

Lines changed: 61 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -115,12 +115,12 @@ extension JNISwift2JavaGenerator {
115115
let conversions = try enumCase.parameters.enumerated().map { idx, parameter in
116116
let resultName = parameter.name ?? "arg\(idx)"
117117
let result = SwiftResult(convention: .direct, type: parameter.type)
118-
var translatedResult = try self.translate(swiftResult: result, resultName: resultName)
118+
var translatedResult = try self.translate(swiftResult: result, methodName: methodName, resultName: resultName)
119119
translatedResult.conversion = .replacingPlaceholder(
120120
translatedResult.conversion,
121121
placeholder: "$nativeParameters.\(resultName)",
122122
)
123-
let nativeResult = try nativeTranslation.translate(swiftResult: result, resultName: resultName)
123+
let nativeResult = try nativeTranslation.translate(swiftResult: result, methodName: methodName, resultName: resultName)
124124
return (translated: translatedResult, native: nativeResult)
125125
}
126126

@@ -319,7 +319,7 @@ extension JNISwift2JavaGenerator {
319319
)
320320
}
321321

322-
let translatedResult = try translate(swiftResult: SwiftResult(convention: .direct, type: swiftType.resultType))
322+
let translatedResult = try translate(swiftResult: SwiftResult(convention: .direct, type: swiftType.resultType), methodName: name)
323323

324324
return TranslatedFunctionType(
325325
name: name,
@@ -367,6 +367,7 @@ extension JNISwift2JavaGenerator {
367367

368368
let resultType = try translate(
369369
swiftResult: functionSignature.result,
370+
methodName: methodName,
370371
genericParameters: functionSignature.genericParameters,
371372
genericRequirements: functionSignature.genericRequirements,
372373
)
@@ -628,7 +629,9 @@ extension JNISwift2JavaGenerator {
628629
parameterPosition: parameterPosition,
629630
)
630631

631-
case .tuple, .composite:
632+
case .tuple:
633+
throw JavaTranslationError.emptyTuple()
634+
case .composite:
632635
throw JavaTranslationError.unsupportedSwiftType(swiftType)
633636
}
634637
}
@@ -885,6 +888,7 @@ extension JNISwift2JavaGenerator {
885888

886889
func translate(
887890
swiftResult: SwiftResult,
891+
methodName: String,
888892
resultName: String = "result",
889893
genericParameters: [SwiftGenericParameterDeclaration] = [],
890894
genericRequirements: [SwiftGenericRequirement] = [],
@@ -1017,6 +1021,7 @@ extension JNISwift2JavaGenerator {
10171021

10181022
case .tuple(let elements) where !elements.isEmpty:
10191023
return try translateTupleResult(
1024+
methodName: methodName,
10201025
elements: elements,
10211026
resultName: resultName,
10221027
genericParameters: genericParameters,
@@ -1149,7 +1154,9 @@ extension JNISwift2JavaGenerator {
11491154
}
11501155
}
11511156

1157+
/// - Parameter: methodName is necessary because we may need to form an ad-hoc one off type if e.g. named tuples are used.
11521158
func translateTupleResult(
1159+
methodName: String,
11531160
elements: [SwiftTupleElement],
11541161
resultName: String = "result",
11551162
genericParameters: [SwiftGenericParameterDeclaration],
@@ -1167,18 +1174,30 @@ extension JNISwift2JavaGenerator {
11671174
// Determine the Java type for this element
11681175
let elementResult = try translate(
11691176
swiftResult: .init(convention: .indirect, type: element.type),
1177+
methodName: methodName,
11701178
resultName: outParamName,
11711179
genericParameters: genericParameters,
11721180
genericRequirements: genericRequirements,
11731181
)
11741182

1183+
// out names are always ...$N, no need to use real named tuple names here, this is just for the thunk
11751184
elementOutParamNames.append(outParamName)
1185+
11761186
// FIXME: More accurate determination of whether the result is direct or indirect
11771187
if elementResult.outParameters.isEmpty {
1178-
// Convert direct result to indirect result
1179-
let arrayType: JavaType = .array(elementResult.javaType)
1188+
// Convert direct result to indirect result.
1189+
// For class types (nominal types), the JNI native representation is 'long'
1190+
// (a memory address), not the wrapper class type
1191+
let nativeElementType: JavaType
1192+
switch elementResult.javaType {
1193+
case .class:
1194+
nativeElementType = .long
1195+
default:
1196+
nativeElementType = elementResult.javaType
1197+
}
1198+
let arrayType: JavaType = .array(nativeElementType)
11801199
outParameters.append(
1181-
OutParameter(name: outParamName, type: arrayType, allocation: .newArray(elementResult.javaType, size: 1))
1200+
OutParameter(name: outParamName, type: arrayType, allocation: .newArray(nativeElementType, size: 1))
11821201
)
11831202
elementConversions.append(elementResult.conversion)
11841203
} else {
@@ -1188,19 +1207,36 @@ extension JNISwift2JavaGenerator {
11881207
elementJavaTypes.append(elementResult.javaType)
11891208
}
11901209

1191-
let javaResultType: JavaType = .tuple(elementTypes: elementJavaTypes)
1192-
let fullTupleClassName = "org.swift.swiftkit.core.tuple.Tuple\(arity)"
1210+
let isNamedTuple = elements.contains { $0.label != nil }
1211+
let names = elements.enumerated().map { idx, element in
1212+
if let label = element.label {
1213+
label
1214+
} else {
1215+
"$\(idx)"
1216+
}
1217+
}
11931218

11941219
let tupleElements: [(outParamName: String, elementConversion: JavaNativeConversionStep)] =
11951220
zip(elementOutParamNames, elementConversions).map { ($0, $1) }
11961221

1222+
let javaResultType: JavaType =
1223+
if isNamedTuple {
1224+
.labeledTuple(methodName, names: names, elementTypes: elementJavaTypes)
1225+
} else {
1226+
.tuple(elementTypes: elementJavaTypes)
1227+
}
1228+
1229+
let javaNativeConversionStep: JavaNativeConversionStep =
1230+
.tupleFromOutParams(
1231+
// try!-safe, because we know the result type is a class here (a Tuple of some form)
1232+
tupleClassName: "\(javaResultType)",
1233+
elements: tupleElements
1234+
)
1235+
11971236
return TranslatedResult(
11981237
javaType: javaResultType,
11991238
outParameters: outParameters,
1200-
conversion: .tupleFromOutParams(
1201-
tupleClassName: fullTupleClassName,
1202-
elements: tupleElements
1203-
)
1239+
conversion: javaNativeConversionStep
12041240
)
12051241
}
12061242

@@ -1285,6 +1321,7 @@ extension JNISwift2JavaGenerator {
12851321

12861322
let wrappedValueResult = try translate(
12871323
swiftResult: SwiftResult(convention: .direct, type: swiftType),
1324+
methodName: "",
12881325
resultName: resultName + "Wrapped$",
12891326
genericParameters: genericParameters,
12901327
genericRequirements: genericRequirements,
@@ -1945,7 +1982,7 @@ extension JNISwift2JavaGenerator {
19451982
let converted = element.elementConversion.render(&printer, "\(element.outParamName)[0]")
19461983
args.append(converted)
19471984
}
1948-
return "new \(tupleClassName)<>(\(args.joined(separator: ", ")))"
1985+
return "new \(tupleClassName)(\(args.joined(separator: ", ")))"
19491986

19501987
case .placeToVar(let inner, let name):
19511988
let inner = inner.render(&printer, placeholder)
@@ -2025,6 +2062,7 @@ extension JNISwift2JavaGenerator {
20252062

20262063
enum JavaTranslationError: Error {
20272064
case unsupportedSwiftType(SwiftType, fileID: String, line: Int)
2065+
20282066
static func unsupportedSwiftType(
20292067
_ type: SwiftType,
20302068
_fileID: String = #fileID,
@@ -2060,5 +2098,14 @@ extension JNISwift2JavaGenerator {
20602098

20612099
/// Set type requires exactly one generic type argument (element).
20622100
case setRequiresElementType(SwiftType)
2101+
2102+
/// Empty tuples are not supported in lowering, they should be treated as Void
2103+
case emptyTuple(file: String, line: Int)
2104+
static func emptyTuple(
2105+
_file: String = #fileID,
2106+
_line: Int = #line
2107+
) -> JavaTranslationError {
2108+
.emptyTuple(file: _file, line: _line)
2109+
}
20632110
}
20642111
}

0 commit comments

Comments
 (0)