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
Expand Up @@ -19,8 +19,20 @@ import Dispatch
public final class Notifier {
private var handler: ((Item) -> Void)?

/// An optional closure property: a Kotlin property of the setter's functional interface.
public var onItem: ((Item) -> Void)?

public init() {}

public func emit(id: String) {
onItem?(Item(id: id, count: 1))
}

/// A closure returned to Kotlin.
public func makeFormatter(prefix: String) -> (Item) -> String {
{ "\(prefix)\($0.id)" }
}

public func setHandler(_ handler: @escaping (Item) -> Void) {
self.handler = handler
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import org.junit.jupiter.api.condition.OS
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotEquals
import kotlin.test.assertNull
import kotlin.test.assertTrue

// Kotlin lambdas passed as Swift escaping closures. Functional-interface names follow
Expand Down Expand Up @@ -49,6 +50,25 @@ class ClosuresTest {
assertEquals(2L, out.count)
}

@Test
fun optionalClosureProperty() {
val notifier = Notifier.init()
val seen = mutableListOf<String>()
assertNull(notifier.onItem)
notifier.onItem = { item -> seen += item.id }
notifier.emit("a")
notifier.onItem?.apply(Item.init("b", 1)) // the Swift closure, calling the lambda
notifier.onItem = null
notifier.emit("c")
assertEquals(listOf("a", "b"), seen)
}

@Test
fun returnedClosure() {
val format = Notifier.init().makeFormatter("#")
assertEquals(listOf("#1", "#2"), listOf("1", "2").map { format.apply(Item.init(it, 1)) })
}

@Test
fun sendableClosureCalledFromAnotherThread() {
val testThread = Thread.currentThread()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
//===----------------------------------------------------------------------===//
//
// 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
//
//===----------------------------------------------------------------------===//

/// Closures returned to Java, and an optional closure property.
public final class ClosureFactory {
public var onStruct: ((MySwiftStruct) -> Void)?

public init() {}

public func makeAdder(_ base: Int64) -> (Int64) -> Int64 {
{ base + $0 }
}

public func makeDescriber() -> (MySwiftStruct, String) -> String {
{ "\($1)\($0.getCapacity())" }
}

public func send(_ value: MySwiftStruct) {
onStruct?(value)
}

/// How many closures from `makeTracked()` were released.
nonisolated(unsafe) public static var releasedClosures: Int64 = 0

/// A closure holding an object that counts its release.
public static func makeTracked() -> () -> Int64 {
let tracker = ReleaseTracker()
return { tracker.value }
}
}

final class ReleaseTracker {
let value: Int64 = 1

deinit {
ClosureFactory.releasedClosures += 1
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2024 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.swift;

import org.junit.jupiter.api.Test;
import org.swift.swiftkit.core.SwiftArena;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

import static org.junit.jupiter.api.Assertions.*;

public class ClosureResultsTest {
@Test
void returnedPrimitiveClosure() {
try (var arena = SwiftArena.ofConfined()) {
ClosureFactory.makeAdder.result add = ClosureFactory.init(arena).makeAdder(10);
assertEquals(15, add.apply(5));
long sum = 0;
for (int i = 0; i < 1000; i++) {
sum += add.apply(i);
}
assertEquals(10 * 1000 + 999 * 1000 / 2, sum);
}
}

@Test
void returnedStructClosure() {
try (var arena = SwiftArena.ofConfined()) {
var describe = ClosureFactory.init(arena).makeDescriber();
assertEquals("cap: 3", describe.apply(MySwiftStruct.init(3, 1, arena), "cap: "));
}
}

@Test
void droppedClosureIsReleased() throws InterruptedException {
long before = ClosureFactory.getReleasedClosures();
for (int i = 0; i < 100; i++) {
assertEquals(1, ClosureFactory.makeTracked().apply());
}
long deadline = System.currentTimeMillis() + 10_000;
while (ClosureFactory.getReleasedClosures() == before && System.currentTimeMillis() < deadline) {
System.gc();
Thread.sleep(10);
}
assertTrue(ClosureFactory.getReleasedClosures() > before);
}

@Test
void optionalClosureProperty() {
try (var arena = SwiftArena.ofConfined()) {
ClosureFactory factory = ClosureFactory.init(arena);
assertTrue(factory.getOnStruct().isEmpty());

List<Long> seen = new ArrayList<>();
factory.setOnStruct(Optional.of(value -> seen.add(value.getCapacity())));
factory.send(MySwiftStruct.init(1, 0, arena));
// The getter returns the Swift closure, which calls the Java lambda set above.
factory.getOnStruct().get().apply(MySwiftStruct.init(2, 0, arena));
factory.setOnStruct(Optional.empty());
factory.send(MySwiftStruct.init(3, 0, arena));
assertEquals(List.of(1L, 2L), seen);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -974,19 +974,21 @@ extension JNISwift2JavaGenerator {
{
return
}
// Overloads of a method share its helper class; the first, with every parameter, prints it.
guard printedJavaClosureHelperClasses.insert("\(translated.parentName).\(translated.name)").inserted else {
// Overloads of a method share its helper class, and so do a closure property's getter and setter; the
// first one prints it.
let className = translated.closureHelperClassName
guard printedJavaClosureHelperClasses.insert("\(translated.parentName).\(className)").inserted else {
return
}

printer.printBraceBlock(
"""
public static class \(translated.name)
public static class \(className)
"""
) { printer in
// Only holds interfaces. Without a public constructor, Kotlin's `Type.method()` can't also mean
// `Type.method` constructed, when the method takes no arguments.
printer.print("private \(translated.name)() {}")
printer.print("private \(className)() {}")
for functionType in translated.functionTypes {
if KnownJavaFunctionalInterface.find(functionType.swiftType) != nil {
continue
Expand Down
Loading
Loading