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,83 @@
//===----------------------------------------------------------------------===//
//
// 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
//
//===----------------------------------------------------------------------===//

public class CallbackManager {
private var callback: (() -> Void)?
private var intCallback: ((Int64) -> Int64)?

public init() {}

public func setCallback(callback: @escaping () -> Void) {
self.callback = callback
}

public func triggerCallback() {
callback?()
}

public func clearCallback() {
callback = nil
}

public func setIntCallback(callback: @escaping (Int64) -> Int64) {
self.intCallback = callback
}

public func triggerIntCallback(value: Int64) -> Int64? {
return intCallback?(value)
}
}

// public func delayedExecution(closure: @escaping (Int64) -> Int64, input: Int64) -> Int64 {
// // In a real implementation, this might be async
// // For testing purposes, we just call it synchronously
// return closure(input)
// }

public class ClosureStore {
private var closures: [() -> Void] = []

public init() {}

public func addClosure(closure: @escaping () -> Void) {
closures.append(closure)
}

public func executeAll() {
for closure in closures {
closure()
}
}

public func clear() {
closures.removeAll()
}

public func count() -> Int64 {
return Int64(closures.count)
}
}

public func multipleEscapingClosures(
onSuccess: @escaping (Int64) -> Void,
onFailure: @escaping (Int64) -> Void,
condition: Bool
) {
if condition {
onSuccess(42)
} else {
onFailure(-1)
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
//===----------------------------------------------------------------------===//
//
// 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.OptionalLong;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;

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

public class EscapingClosuresTest {

@Test
void testCallbackManager_singleCallback() {
try (var arena = SwiftArena.ofConfined()) {
CallbackManager manager = CallbackManager.init(arena);

AtomicBoolean wasCalled = new AtomicBoolean(false);

// Create an escaping closure (no try-with-resources needed - cleanup is automatic via Swift ARC)
CallbackManager.setCallback.callback callback = () -> {
wasCalled.set(true);
};

// Set the callback
manager.setCallback(callback);

// Trigger it
manager.triggerCallback();
assertTrue(wasCalled.get(), "Callback should have been called");

// Trigger again to ensure it's still stored
wasCalled.set(false);
manager.triggerCallback();
assertTrue(wasCalled.get(), "Callback should be called multiple times");

// Clear the callback - this releases the closure on Swift side, triggering GlobalRef cleanup
manager.clearCallback();
}
}

@Test
void testCallbackManager_intCallback() {
try (var arena = SwiftArena.ofConfined()) {
CallbackManager manager = CallbackManager.init(arena);

CallbackManager.setIntCallback.callback callback = (value) -> {
return value * 2;
};

manager.setIntCallback(callback);

// Trigger the callback - returns OptionalLong since Swift returns Int64?
OptionalLong result = manager.triggerIntCallback(21);
assertTrue(result.isPresent(), "Result should be present");
assertEquals(42, result.getAsLong(), "Callback should double the input");
}
}

@Test
void testClosureStore() {
try (var arena = SwiftArena.ofConfined()) {
ClosureStore store = ClosureStore.init(arena);

AtomicLong counter = new AtomicLong(0);

// Add multiple closures
ClosureStore.addClosure.closure closure1 = () -> {
counter.incrementAndGet();
};
ClosureStore.addClosure.closure closure2 = () -> {
counter.addAndGet(10);
};
ClosureStore.addClosure.closure closure3 = () -> {
counter.addAndGet(100);
};

store.addClosure(closure1);
store.addClosure(closure2);
store.addClosure(closure3);

assertEquals(3, store.count(), "Should have 3 closures stored");

// Execute all closures
store.executeAll();
assertEquals(111, counter.get(), "All closures should be executed");

// Execute again
counter.set(0);
store.executeAll();
assertEquals(111, counter.get(), "Closures should be reusable");

// Clear - this releases closures on Swift side, triggering GlobalRef cleanup
store.clear();
assertEquals(0, store.count(), "Store should be empty after clear");
}
}

@Test
void testMultipleEscapingClosures() {
AtomicLong successValue = new AtomicLong(0);
AtomicLong failureValue = new AtomicLong(0);

MySwiftLibrary.multipleEscapingClosures.onSuccess onSuccess = (value) -> {
successValue.set(value);
};
MySwiftLibrary.multipleEscapingClosures.onFailure onFailure = (value) -> {
failureValue.set(value);
};

// Test success case
MySwiftLibrary.multipleEscapingClosures(onSuccess, onFailure, true);
assertEquals(42, successValue.get(), "Success callback should be called");
assertEquals(0, failureValue.get(), "Failure callback should not be called");

// Reset and test failure case
successValue.set(0);
failureValue.set(0);
MySwiftLibrary.multipleEscapingClosures(onSuccess, onFailure, false);
assertEquals(0, successValue.get(), "Success callback should not be called");
assertEquals(-1, failureValue.get(), "Failure callback should be called");
}

@Test
void testMultipleManagersWithDifferentClosures() {
try (var arena = SwiftArena.ofConfined()) {
CallbackManager manager1 = CallbackManager.init(arena);
CallbackManager manager2 = CallbackManager.init(arena);

AtomicBoolean called1 = new AtomicBoolean(false);
AtomicBoolean called2 = new AtomicBoolean(false);

CallbackManager.setCallback.callback callback1 = () -> {
called1.set(true);
};
CallbackManager.setCallback.callback callback2 = () -> {
called2.set(true);
};

manager1.setCallback(callback1);
manager2.setCallback(callback2);

// Trigger first manager
manager1.triggerCallback();
assertTrue(called1.get(), "First callback should be called");
assertFalse(called2.get(), "Second callback should not be called");

// Trigger second manager
manager2.triggerCallback();
assertTrue(called2.get(), "Second callback should be called");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,8 @@ extension JNISwift2JavaGenerator {
)

case .function(let fn):

// @Sendable is not supported yet as "environment" is later captured inside the closure.
var parameters = [NativeParameter]()
for (i, parameter) in fn.parameters.enumerated() {
let parameterName = parameter.parameterName ?? "_\(i)"
Expand All @@ -163,15 +165,28 @@ extension JNISwift2JavaGenerator {

let result = try translateClosureResult(fn.resultType)

return NativeParameter(
parameters: [
JavaParameter(name: parameterName, type: .class(package: javaPackage, name: "\(parentName).\(methodName).\(parameterName)"))
],
conversion: .closureLowering(
parameters: parameters,
result: result
if fn.isEscaping {
return NativeParameter(
parameters: [
JavaParameter(name: parameterName, type: .class(package: javaPackage, name: "\(parentName).\(methodName).\(parameterName)"))
],
conversion: .escapingClosureLowering(
parameters: parameters,
result: result,
closureName: parameterName
)
)
)
} else {
return NativeParameter(
parameters: [
JavaParameter(name: parameterName, type: .class(package: javaPackage, name: "\(parentName).\(methodName).\(parameterName)"))
],
conversion: .closureLowering(
parameters: parameters,
result: result
)
)
}

case .optional(let wrapped):
return try translateOptionalParameter(
Expand Down Expand Up @@ -407,6 +422,15 @@ extension JNISwift2JavaGenerator {
switch type {
case .nominal(let nominal):
if let knownType = nominal.nominalTypeDecl.knownTypeKind {

if knownType == .void {
return NativeResult(
javaType: .void,
conversion: .placeholder,
outParameters: []
)
}

guard let javaType = JNIJavaTypeTranslator.translate(knownType: knownType, config: self.config),
javaType.implementsJavaValue else {
throw JavaTranslationError.unsupportedSwiftType(type)
Expand Down Expand Up @@ -692,6 +716,8 @@ extension JNISwift2JavaGenerator {
indirect case pointee(NativeSwiftConversionStep)

indirect case closureLowering(parameters: [NativeParameter], result: NativeResult)

indirect case escapingClosureLowering(parameters: [NativeParameter], result: NativeResult, closureName: String)

indirect case initializeSwiftJavaWrapper(NativeSwiftConversionStep, wrapperName: String)

Expand Down Expand Up @@ -917,6 +943,60 @@ extension JNISwift2JavaGenerator {
printer.print("}")

return printer.finalize()

case .escapingClosureLowering(let parameters, let nativeResult, let closureName):
var printer = CodePrinter()

let methodSignature = MethodSignature(
resultType: nativeResult.javaType,
parameterTypes: parameters.flatMap {
$0.parameters.map { parameter in
guard case .concrete(let type) = parameter.type else {
fatalError("Closures do not support Java generics")
}
return type
}
}
)

let arguments = parameters.map {
$0.conversion.render(&printer, $0.parameters.first!.name)
}

let closureParameters = parameters.flatMap { $0.parameters.map(\.name) }.joined(separator: ", ")

let upcall = "env$.interface.\(nativeResult.javaType.jniCallMethodAName)(env$, closureContext_\(closureName)$.object!, methodID$, arguments$)"
let result = nativeResult.conversion.render(&printer, upcall)
let returnResult = if nativeResult.javaType.isVoid { result } else { "return \(result)" }

printer.print(
"""
{
guard let \(placeholder) else {
fatalError(\"\(placeholder) is null")
}

let closureContext_\(closureName)$ = JavaObjectHolder(object: \(placeholder), environment: environment)
return { \(parameters.isEmpty ? "" : "\(closureParameters) in")
guard let env$ = try? JavaVirtualMachine.shared().environment() else {
fatalError(\"Failed to get JNI environment for escaping closure call\")
}

// Call the Java closure
let class$ = env$.interface.GetObjectClass(env$, closureContext_\(closureName)$.object!)
guard let methodID$ = env$.interface.GetMethodID(env$, class$, \"apply\", \"\(methodSignature.mangledName)\") else {
fatalError(\"Failed to find apply method on closure\")
}

let arguments$: [jvalue] = [\(arguments.joined(separator: ", "))]

\(returnResult)
}
}()
"""
)

return printer.finalize()

case .initializeSwiftJavaWrapper(let inner, let wrapperName):
let inner = inner.render(&printer, placeholder)
Expand Down
Loading