Skip to content
Open
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
@@ -0,0 +1,30 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2026 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
//
//===----------------------------------------------------------------------===//

/// Arrays whose elements are optionals or dictionaries.
public enum Watchlist {
/// A row per symbol; a symbol without a quote yet is `nil`.
public static func quotes() -> [Item?] {
[Item(id: "a", count: 1), nil, Item(id: "c", count: 3)]
}

/// Positions per page.
public static func pages() -> [[String: Int64]] {
[["a": 1], ["b": 2, "c": 3]]
}

public static func total(counts: [Int64?]) -> Int64 {
counts.compactMap { $0 }.reduce(0, +)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2026 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
//
//===----------------------------------------------------------------------===//

package com.example.kotlin

import java.util.Optional
import kotlin.jvm.optionals.getOrNull
import kotlin.test.Test
import kotlin.test.assertEquals

class CollectionsTest {
@Test
fun arrayOfOptionals() {
val ids = Watchlist.quotes().map { it.getOrNull()?.id }
assertEquals(listOf("a", null, "c"), ids)
}

@Test
fun arrayOfDictionaries() {
val pages = Watchlist.pages()
assertEquals(listOf(1, 2), pages.map { it.size })
assertEquals(3L, pages[1]["c"])
}

@Test
fun passAnArrayOfOptionals() {
assertEquals(4L, Watchlist.total(arrayOf(Optional.of(1L), Optional.empty(), Optional.of(3L))))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,20 @@ public func nestedStringArray(array: [[String]]) -> [[String]] {
public func nestedLongArray(array: [[Int64]]) -> [[Int64]] {
array
}

// Elements converted one by one: optionals, dictionaries and sets
public func optionalLongArray(array: [Int64?]) -> [Int64?] {
array
}

public func optionalObjectArray(array: [MySwiftClass?]) -> [MySwiftClass?] {
array
}

public func dictionaryArray() -> [[String: Int64]] {
[["a": 1], ["b": 2, "c": 3]]
}

public func setArray() -> [Set<String>] {
[["a", "b"], ["c"]]
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import org.junit.jupiter.api.Test;
import org.swift.swiftkit.core.SwiftArena;

import java.util.Optional;
import java.util.OptionalLong;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
Expand Down Expand Up @@ -184,4 +185,39 @@ void nestedStringArray_emptyInner() {
assertArrayEquals(new String[] { "a" }, result[1]);
assertArrayEquals(new String[] {}, result[2]);
}

@Test
void optionalLongArray() {
@SuppressWarnings("unchecked")
Optional<Long>[] input = new Optional[] { Optional.of(1L), Optional.empty(), Optional.of(3L) };
assertArrayEquals(input, MySwiftLibrary.optionalLongArray(input));
}

@Test
void optionalObjectArray() {
try (var arena = SwiftArena.ofConfined()) {
@SuppressWarnings("unchecked")
Optional<MySwiftClass>[] input = new Optional[] { Optional.of(MySwiftClass.init(7, 8, arena)), Optional.empty() };
Optional<MySwiftClass>[] result = MySwiftLibrary.optionalObjectArray(input);
assertEquals(2, result.length);
assertEquals(7, result[0].orElseThrow().getX());
assertTrue(result[1].isEmpty());
}
}

@Test
void dictionaryArray() {
var result = MySwiftLibrary.dictionaryArray();
assertEquals(2, result.length);
assertEquals(1L, result[0].get("a"));
assertEquals(3L, result[1].get("c"));
}

@Test
void setArray() {
var result = MySwiftLibrary.setArray();
assertEquals(2, result.length);
assertTrue(result[0].contains("b"));
assertEquals(1, result[1].size());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1568,6 +1568,17 @@ extension JNISwift2JavaGenerator {
conversion: .requireNonNull(.placeholder, message: "\(parameterName) must not be null")
)

case .nominal(let nominalType) where nominalType.isBridgedArrayElement:
let javaType = try bridgedArrayElementJavaType(
elementType,
genericParameters: genericParameters,
genericRequirements: genericRequirements
)
return TranslatedParameter(
parameter: JavaParameter(name: parameterName, type: .array(javaType), annotations: parameterAnnotations),
conversion: .requireNonNull(.placeholder, message: "\(parameterName) must not be null")
)

case .nominal(let nominalType):
if let knownType = nominalType.nominalTypeDecl.knownTypeKind {
guard let javaType = JNIJavaTypeTranslator.translate(knownType: knownType, config: self.config) else {
Expand Down Expand Up @@ -1642,6 +1653,21 @@ extension JNISwift2JavaGenerator {
conversion: .placeholder
)

case .nominal(let nominalType) where nominalType.isBridgedArrayElement:
let javaType = try bridgedArrayElementJavaType(
elementType,
genericParameters: genericParameters,
genericRequirements: genericRequirements
)
// The native method returns the array `ArrayBridge` created, whose static type is `Object[]`.
return TranslatedResult(
javaType: .array(javaType),
nativeJavaType: .array(.javaLangObject),
annotations: annotations,
outParameters: [],
conversion: .cast(.placeholder, .array(javaType))
)

case .nominal(let nominalType):
let nominalTypeName = nominalType.nominalTypeDecl.qualifiedName
if let knownType = nominalType.nominalTypeDecl.knownTypeKind {
Expand Down Expand Up @@ -1704,6 +1730,28 @@ extension JNISwift2JavaGenerator {
}
}

/// The Java type of an element converted by `ArrayBridge`. Its `OptionalBridge` always creates a
/// `java.util.Optional`, also for primitives (`Optional<Long>`, not `OptionalLong`).
func bridgedArrayElementJavaType(
_ swiftType: SwiftType,
genericParameters: [SwiftGenericParameterDeclaration],
genericRequirements: [SwiftGenericRequirement],
) throws -> JavaType {
if case .nominal(let nominalType) = swiftType, case .optional(let wrapped) = nominalType.asKnownType {
let wrappedJavaType = try bridgedArrayElementJavaType(
wrapped,
genericParameters: genericParameters,
genericRequirements: genericRequirements
)
return .optional(wrappedJavaType.boxedType)
}
return try translateGenericTypeParameter(
swiftType,
genericParameters: genericParameters,
genericRequirements: genericRequirements,
)
}

func javaTypeForDictionaryComponent(
_ swiftType: SwiftType,
genericParameters: [SwiftGenericParameterDeclaration],
Expand Down Expand Up @@ -2076,6 +2124,9 @@ extension JNISwift2JavaGenerator {

indirect case requireNonNull(JavaNativeConversionStep, message: String)

/// `(type) inner`
indirect case cast(JavaNativeConversionStep, JavaType)

/// Constructs a TupleN from out-parameter arrays.
/// E.g. `new Tuple2<>(result_0$[0], result_1$[0])`
case tupleFromOutParams(tupleClassName: String, elements: [(outParamName: String, elementConversion: JavaNativeConversionStep)])
Expand Down Expand Up @@ -2242,6 +2293,9 @@ extension JNISwift2JavaGenerator {
let inner = inner.render(&printer, placeholder)
return #"Objects.requireNonNull(\#(inner), "\#(message)")"#

case .cast(let inner, let type):
return "(\(type)) \(inner.render(&printer, placeholder))"

case .tupleFromOutParams(let tupleClassName, let elements):
var args: [String] = []
for element in elements {
Expand Down Expand Up @@ -2315,7 +2369,7 @@ extension JNISwift2JavaGenerator {
case .print(let inner):
return inner.requiresSwiftArena

case .requireNonNull(let inner, _):
case .requireNonNull(let inner, _), .cast(let inner, _):
return inner.requiresSwiftArena

case .tupleFromOutParams(_, let elements):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1025,6 +1025,19 @@ extension JNISwift2JavaGenerator {
outParameters: []
)

case .nominal(let nominalType) where nominalType.isBridgedArrayElement:
// Optionals, dictionaries and sets are converted element by element with their bridges.
let arrayBridge = try bridgeTypeName(for: knownTypes.arraySugar(elementType))
return NativeResult(
javaType: .array(.javaLangObject),
conversion: .method(
.constant(arrayBridge),
function: "toJavaObject",
arguments: [(nil, .placeholder), ("in", .constant("environment"))]
),
outParameters: []
)

case .nominal(let nominalType):
let nominalTypeName = nominalType.nominalTypeDecl.qualifiedName
if let knownType = nominalType.nominalTypeDecl.knownTypeKind {
Expand Down Expand Up @@ -1107,6 +1120,21 @@ extension JNISwift2JavaGenerator {
conversionCheck: nil
)

case .nominal(let nominalType) where nominalType.isBridgedArrayElement:
let arrayBridge = try bridgeTypeName(for: knownTypes.arraySugar(elementType))
return NativeParameter(
parameters: [
JavaParameter(name: parameterName, type: .array(.javaLangObject))
],
conversion: .method(
.constant(arrayBridge),
function: "fromJavaObject",
arguments: [(nil, .placeholder), ("in", .constant("environment"))]
),
indirectConversion: nil,
conversionCheck: nil
)

case .nominal(let nominalType):
if let knownType = nominalType.nominalTypeDecl.knownTypeKind {
guard let javaType = JNIJavaTypeTranslator.translate(knownType: knownType, config: self.config),
Expand Down Expand Up @@ -2230,3 +2258,13 @@ extension JNISwift2JavaGenerator {
}
}
}

extension SwiftNominalType {
/// Array elements converted with `ArrayBridge` rather than to a primitive or pointer array.
var isBridgedArrayElement: Bool {
switch nominalTypeDecl.knownTypeKind {
case .optional, .dictionary, .set: true
default: false
}
}
}
84 changes: 84 additions & 0 deletions Tests/JExtractSwiftTests/JNI/JNIBridgedArrayElementsTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2026 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

/// Arrays of optionals, dictionaries and sets are converted element by element with `ArrayBridge`.
@Suite
struct JNIBridgedArrayElementsTests {
@Test
func optionalElements_java() throws {
try assertOutput(
input: "public func f(array: [Int64?]) -> [Int64?] {}",
.jni,
.java,
detectChunkByInitialLines: 1,
expectedChunks: [
"""
public static java.util.Optional<java.lang.Long>[] f(java.util.Optional<java.lang.Long>[] array) {
return (java.util.Optional<java.lang.Long>[]) SwiftModule.$f(Objects.requireNonNull(array, "array must not be null"));
}
""",
"private static native java.lang.Object[] $f(java.lang.Object[] array);",
]
)
}

@Test
func optionalElements_swift() throws {
try assertOutput(
input: "public func f(array: [Int64?]) -> [Int64?] {}",
.jni,
.swift,
detectChunkByInitialLines: 1,
expectedChunks: [
"return ArrayBridge<OptionalBridge<JavaBoxableBridge<Int64>>>.toJavaObject(SwiftModule.f(array: ArrayBridge<OptionalBridge<JavaBoxableBridge<Int64>>>.fromJavaObject(array, in: environment)), in: environment)"
]
)
}

@Test
func dictionaryElements_java() throws {
try assertOutput(
input: "public func f() -> [[String: Int64]] {}",
.jni,
.java,
detectChunkByInitialLines: 1,
expectedChunks: [
"""
public static org.swift.swiftkit.core.collections.SwiftDictionaryMap<java.lang.String, java.lang.Long>[] f() {
return (org.swift.swiftkit.core.collections.SwiftDictionaryMap<java.lang.String, java.lang.Long>[]) SwiftModule.$f();
}
"""
]
)
}

@Test
func jextractedOptionalElements_java() throws {
try assertOutput(
input: """
public struct Tag {}
public func f() -> [Tag?] {}
""",
.jni,
.java,
detectChunkByInitialLines: 1,
expectedChunks: [
"public static java.util.Optional<Tag>[] f() {"
]
)
}
}
Loading
Loading