Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@

package com.example.swift;

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

@Test
void labeledTuple() {
Tuple2<Integer, Integer> result = MySwiftLibrary.labeledTuple();
var result = MySwiftLibrary.labeledTuple();
// Access via named accessors
assertEquals(10, result.x());
assertEquals(20, result.y());
// Positional access still works (inherited from Tuple2)
assertEquals(10, result.$0);
assertEquals(20, result.$1);

// The labelled tuple is a subclass of Tuple2
assertInstanceOf(Tuple2.class, result);
// And the generic types match positionally as well
@SuppressWarnings("unused")
Tuple2<Integer, Integer> check = result;
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -388,10 +388,10 @@ extension FFMSwift2JavaGenerator {
// If a Swift function is 'throws' we throw a checked error for the Java side
// TODO: When we support typed throws on Swift side we'll want to throw the right type here instead
if translatedSignature.isThrowing {
throwsClauses.append(JavaType.swiftJavaErrorException.simpleClassName)
throwsClauses.append(JavaType.swiftJavaErrorException.className!)
Copy link
Copy Markdown
Collaborator Author

@ktoso ktoso Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

simpleClassName was crashing on misuse anyway

}
if translatedSignature.canThrowSwiftIntegerOverflowException {
throwsClauses.append(JavaType.swiftIntegerOverflowException.simpleClassName)
throwsClauses.append(JavaType.swiftIntegerOverflowException.className!)
}
let throwsClause = throwsClauses.isEmpty ? "" : " throws \(throwsClauses.joined(separator: ", "))"

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,8 @@ extension FFMSwift2JavaGenerator {
// Result.
let result = try self.translateResult(
swiftResult: swiftSignature.result,
loweredResult: loweredFunctionSignature.result
loweredResult: loweredFunctionSignature.result,
methodName: methodName
)

return TranslatedFunctionSignature(
Expand Down Expand Up @@ -620,8 +621,7 @@ extension FFMSwift2JavaGenerator {
case .char: ("Optional<Character>", "toOptionalSegmentCharacter")
case .short: ("Optional<Short>", "toOptionalSegmentShort")
case .float: ("Optional<Float>", "toOptionalSegmentFloat")
default:
throw JavaTranslationError.unhandledType(known: .optional(swiftType))
default: throw JavaTranslationError.unhandledType(known: .optional(swiftType))
}
return TranslatedParameter(
javaParameters: [
Expand Down Expand Up @@ -689,7 +689,8 @@ extension FFMSwift2JavaGenerator {
/// Translate a Swift API result to the user-facing Java API result.
func translateResult(
swiftResult: SwiftResult,
loweredResult: LoweredResult
loweredResult: LoweredResult,
methodName: String
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we need the method name to form the adhoc types per method

) throws -> TranslatedResult {
let swiftType = swiftResult.type
// If the result type should cause any annotations on the method, include them here.
Expand Down Expand Up @@ -844,6 +845,7 @@ extension FFMSwift2JavaGenerator {

case .tuple(let elements):
return try translateTupleResult(
methodName: methodName,
elements: elements,
resultAnnotations: resultAnnotations
)
Expand All @@ -856,6 +858,7 @@ extension FFMSwift2JavaGenerator {

/// Tuple results: indirect `MemorySegment` per element, then `new TupleN<…>(…)` (mirrors JNI out-arrays).
func translateTupleResult(
methodName: String,
elements: [SwiftTupleElement],
resultAnnotations: [JavaAnnotation]
) throws -> TranslatedResult {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ extension FFMSwift2JavaGenerator {
/// Returns the Java class name for a nominal type, applying known-type overrides
func javaClassName(for decl: ImportedNominalType) -> String {
if decl.swiftNominal.knownTypeKind == .swiftJavaError {
return JavaType.swiftJavaErrorException.simpleClassName
return JavaType.swiftJavaErrorException.className!
}
return decl.swiftNominal.name
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -524,6 +524,10 @@ extension JNISwift2JavaGenerator {
printJavaBindingWrapperHelperClass(&printer, decl)

printJavaBindingWrapperMethod(&printer, decl, skipMethodBody: skipMethodBody)

// Print any additional types we may need to emit, e.g. named tuples are emitted as static classes
// right next to the func that is using them.
printNecessarySupportTypes(&printer, decl)
}

/// Print the helper type container for a user-facing Java API.
Expand Down Expand Up @@ -566,6 +570,17 @@ extension JNISwift2JavaGenerator {
)
}

private func printNecessarySupportTypes(
_ printer: inout CodePrinter,
_ decl: ImportedFunc
) {
let translatedDecl = translatedDecl(for: decl)!

for labeledTuple in translatedDecl.usedLabeledTuples {
printAdHocLabeledTupleStaticClass(&printer, labeledTuple)
}
}

private func printJavaBindingWrapperMethod(
_ printer: inout CodePrinter,
_ decl: ImportedFunc,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,12 +115,12 @@ extension JNISwift2JavaGenerator {
let conversions = try enumCase.parameters.enumerated().map { idx, parameter in
let resultName = parameter.name ?? "arg\(idx)"
let result = SwiftResult(convention: .direct, type: parameter.type)
var translatedResult = try self.translate(swiftResult: result, resultName: resultName)
var translatedResult = try self.translate(swiftResult: result, methodName: methodName, resultName: resultName)
translatedResult.conversion = .replacingPlaceholder(
translatedResult.conversion,
placeholder: "$nativeParameters.\(resultName)",
)
let nativeResult = try nativeTranslation.translate(swiftResult: result, resultName: resultName)
let nativeResult = try nativeTranslation.translate(swiftResult: result, methodName: methodName, resultName: resultName)
return (translated: translatedResult, native: nativeResult)
}

Expand Down Expand Up @@ -319,7 +319,7 @@ extension JNISwift2JavaGenerator {
)
}

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

return TranslatedFunctionType(
name: name,
Expand Down Expand Up @@ -367,6 +367,7 @@ extension JNISwift2JavaGenerator {

let resultType = try translate(
swiftResult: functionSignature.result,
methodName: methodName,
genericParameters: functionSignature.genericParameters,
genericRequirements: functionSignature.genericRequirements,
)
Expand Down Expand Up @@ -628,7 +629,9 @@ extension JNISwift2JavaGenerator {
parameterPosition: parameterPosition,
)

case .tuple, .composite:
case .tuple:
throw JavaTranslationError.emptyTuple()
case .composite:
throw JavaTranslationError.unsupportedSwiftType(swiftType)
}
}
Expand Down Expand Up @@ -885,6 +888,7 @@ extension JNISwift2JavaGenerator {

func translate(
swiftResult: SwiftResult,
methodName: String,
resultName: String = "result",
genericParameters: [SwiftGenericParameterDeclaration] = [],
genericRequirements: [SwiftGenericRequirement] = [],
Expand Down Expand Up @@ -1017,6 +1021,7 @@ extension JNISwift2JavaGenerator {

case .tuple(let elements) where !elements.isEmpty:
return try translateTupleResult(
methodName: methodName,
elements: elements,
resultName: resultName,
genericParameters: genericParameters,
Expand Down Expand Up @@ -1149,7 +1154,9 @@ extension JNISwift2JavaGenerator {
}
}

/// - Parameter: methodName is necessary because we may need to form an ad-hoc one off type if e.g. named tuples are used.
func translateTupleResult(
methodName: String,
elements: [SwiftTupleElement],
resultName: String = "result",
genericParameters: [SwiftGenericParameterDeclaration],
Expand All @@ -1167,18 +1174,33 @@ extension JNISwift2JavaGenerator {
// Determine the Java type for this element
let elementResult = try translate(
swiftResult: .init(convention: .indirect, type: element.type),
methodName: methodName,
resultName: outParamName,
genericParameters: genericParameters,
genericRequirements: genericRequirements,
)

// out names are always ...$N, no need to use real named tuple names here, this is just for the thunk
elementOutParamNames.append(outParamName)

// FIXME: More accurate determination of whether the result is direct or indirect
if elementResult.outParameters.isEmpty {
// Convert direct result to indirect result
let arrayType: JavaType = .array(elementResult.javaType)
// Convert direct result to indirect result.
// For most class types (Swift wrapper classes), the JNI native representation
// is 'long' (a memory address). However, String is a native JNI reference
// type and must keep its original type so the out-parameter array matches
// the native method signature (String[] not long[])
let nativeElementType: JavaType
if elementResult.javaType.isString {
nativeElementType = elementResult.javaType
} else if case .class = elementResult.javaType {
nativeElementType = .long
} else {
nativeElementType = elementResult.javaType
}
let arrayType: JavaType = .array(nativeElementType)
outParameters.append(
OutParameter(name: outParamName, type: arrayType, allocation: .newArray(elementResult.javaType, size: 1))
OutParameter(name: outParamName, type: arrayType, allocation: .newArray(nativeElementType, size: 1))
)
elementConversions.append(elementResult.conversion)
} else {
Expand All @@ -1188,19 +1210,36 @@ extension JNISwift2JavaGenerator {
elementJavaTypes.append(elementResult.javaType)
}

let javaResultType: JavaType = .tuple(elementTypes: elementJavaTypes)
let fullTupleClassName = "org.swift.swiftkit.core.tuple.Tuple\(arity)"
let isNamedTuple = elements.contains { $0.label != nil }
let names = elements.enumerated().map { idx, element in
if let label = element.label {
label
} else {
"$\(idx)"
}
}

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

let javaResultType: JavaType =
if isNamedTuple {
.labeledTuple(methodName, names: names, elementTypes: elementJavaTypes)
} else {
.tuple(elementTypes: elementJavaTypes)
}

let javaNativeConversionStep: JavaNativeConversionStep =
.tupleFromOutParams(
// try!-safe, because we know the result type is a class here (a Tuple of some form)
tupleClassName: "\(javaResultType)",
elements: tupleElements
)

return TranslatedResult(
javaType: javaResultType,
outParameters: outParameters,
conversion: .tupleFromOutParams(
tupleClassName: fullTupleClassName,
elements: tupleElements
)
conversion: javaNativeConversionStep
)
}

Expand Down Expand Up @@ -1285,6 +1324,7 @@ extension JNISwift2JavaGenerator {

let wrappedValueResult = try translate(
swiftResult: SwiftResult(convention: .direct, type: swiftType),
methodName: "",
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Double check this one

resultName: resultName + "Wrapped$",
genericParameters: genericParameters,
genericRequirements: genericRequirements,
Expand Down Expand Up @@ -1945,7 +1985,7 @@ extension JNISwift2JavaGenerator {
let converted = element.elementConversion.render(&printer, "\(element.outParamName)[0]")
args.append(converted)
}
return "new \(tupleClassName)<>(\(args.joined(separator: ", ")))"
return "new \(tupleClassName)(\(args.joined(separator: ", ")))"

case .placeToVar(let inner, let name):
let inner = inner.render(&printer, placeholder)
Expand Down Expand Up @@ -2025,6 +2065,7 @@ extension JNISwift2JavaGenerator {

enum JavaTranslationError: Error {
case unsupportedSwiftType(SwiftType, fileID: String, line: Int)

static func unsupportedSwiftType(
_ type: SwiftType,
_fileID: String = #fileID,
Expand Down Expand Up @@ -2060,5 +2101,14 @@ extension JNISwift2JavaGenerator {

/// Set type requires exactly one generic type argument (element).
case setRequiresElementType(SwiftType)

/// Empty tuples are not supported in lowering, they should be treated as Void
case emptyTuple(file: String, line: Int)
static func emptyTuple(
_file: String = #fileID,
_line: Int = #line
) -> JavaTranslationError {
.emptyTuple(file: _file, line: _line)
}
}
}
Loading
Loading