Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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 @@ -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 })
}
}

Expand Down Expand Up @@ -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 })
}
}

Expand Down Expand Up @@ -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 {
Comment thread
amanthatdoescares marked this conversation as resolved.
Outdated
args.append(
element.elementConversion.render(
&printer,
element.outParamName,
placeholderForDowncall: placeholderForDowncall
)
)
}
return "\(tupleClassName)(\(args.joined(separator: ", ")))"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Comment thread
amanthatdoescares marked this conversation as resolved.
Outdated
return try translateTupleParameter(
elements: elements,
convention: convention,
parameterName: parameterName,
methodName: methodName,
genericParameters: genericParameters,
genericRequirements: genericRequirements
)

case .tuple:
// TODO: Implement.
throw JavaTranslationError.unhandledType(swiftType)
Comment thread
amanthatdoescares marked this conversation as resolved.
Outdated

case .function:
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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)"
Comment thread
amanthatdoescares marked this conversation as resolved.
Outdated

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 {
Expand Down Expand Up @@ -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 }
}
Expand Down
133 changes: 133 additions & 0 deletions Tests/JExtractSwiftTests/FFM/FFMTupleTests.swift
Original file line number Diff line number Diff line change
@@ -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.
Comment thread
amanthatdoescares marked this conversation as resolved.
Outdated
@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<Long, Long> 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<Long, Long> 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<Integer, Integer> 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: [
"""
Comment thread
amanthatdoescares marked this conversation as resolved.
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)
""",
]
)
}
}
Loading