Skip to content

Commit 98efb49

Browse files
committed
Initial commit to support primitive escaping closures
1 parent 4fb6ca2 commit 98efb49

File tree

9 files changed

+579
-22
lines changed

9 files changed

+579
-22
lines changed
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2025 Apple Inc. and the Swift.org project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of Swift.org project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
public class CallbackManager {
16+
private var callback: (() -> Void)?
17+
private var intCallback: ((Int64) -> Int64)?
18+
19+
public init() {}
20+
21+
public func setCallback(callback: @escaping () -> Void) {
22+
self.callback = callback
23+
}
24+
25+
public func triggerCallback() {
26+
callback?()
27+
}
28+
29+
public func clearCallback() {
30+
callback = nil
31+
}
32+
33+
public func setIntCallback(callback: @escaping (Int64) -> Int64) {
34+
self.intCallback = callback
35+
}
36+
37+
public func triggerIntCallback(value: Int64) -> Int64? {
38+
return intCallback?(value)
39+
}
40+
}
41+
42+
// public func delayedExecution(closure: @escaping (Int64) -> Int64, input: Int64) -> Int64 {
43+
// // In a real implementation, this might be async
44+
// // For testing purposes, we just call it synchronously
45+
// return closure(input)
46+
// }
47+
48+
public class ClosureStore {
49+
private var closures: [() -> Void] = []
50+
51+
public init() {}
52+
53+
public func addClosure(closure: @escaping () -> Void) {
54+
closures.append(closure)
55+
}
56+
57+
public func executeAll() {
58+
for closure in closures {
59+
closure()
60+
}
61+
}
62+
63+
public func clear() {
64+
closures.removeAll()
65+
}
66+
67+
public func count() -> Int64 {
68+
return Int64(closures.count)
69+
}
70+
}
71+
72+
public func multipleEscapingClosures(
73+
onSuccess: @escaping (Int64) -> Void,
74+
onFailure: @escaping (Int64) -> Void,
75+
condition: Bool
76+
) {
77+
if condition {
78+
onSuccess(42)
79+
} else {
80+
onFailure(-1)
81+
}
82+
}
83+
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2024 Apple Inc. and the Swift.org project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of Swift.org project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
package com.example.swift;
16+
17+
import org.junit.jupiter.api.Test;
18+
import org.swift.swiftkit.core.SwiftArena;
19+
import java.util.OptionalLong;
20+
import java.util.concurrent.atomic.AtomicBoolean;
21+
import java.util.concurrent.atomic.AtomicLong;
22+
23+
import static org.junit.jupiter.api.Assertions.*;
24+
25+
public class EscapingClosuresTest {
26+
27+
@Test
28+
void testCallbackManager_singleCallback() {
29+
try (var arena = SwiftArena.ofConfined()) {
30+
CallbackManager manager = CallbackManager.init(arena);
31+
32+
AtomicBoolean wasCalled = new AtomicBoolean(false);
33+
34+
// Create an escaping closure (no try-with-resources needed - cleanup is automatic via Swift ARC)
35+
CallbackManager.setCallback.callback callback = () -> {
36+
wasCalled.set(true);
37+
};
38+
39+
// Set the callback
40+
manager.setCallback(callback);
41+
42+
// Trigger it
43+
manager.triggerCallback();
44+
assertTrue(wasCalled.get(), "Callback should have been called");
45+
46+
// Trigger again to ensure it's still stored
47+
wasCalled.set(false);
48+
manager.triggerCallback();
49+
assertTrue(wasCalled.get(), "Callback should be called multiple times");
50+
51+
// Clear the callback - this releases the closure on Swift side, triggering GlobalRef cleanup
52+
manager.clearCallback();
53+
}
54+
}
55+
56+
@Test
57+
void testCallbackManager_intCallback() {
58+
try (var arena = SwiftArena.ofConfined()) {
59+
CallbackManager manager = CallbackManager.init(arena);
60+
61+
CallbackManager.setIntCallback.callback callback = (value) -> {
62+
return value * 2;
63+
};
64+
65+
manager.setIntCallback(callback);
66+
67+
// Trigger the callback - returns OptionalLong since Swift returns Int64?
68+
OptionalLong result = manager.triggerIntCallback(21);
69+
assertTrue(result.isPresent(), "Result should be present");
70+
assertEquals(42, result.getAsLong(), "Callback should double the input");
71+
}
72+
}
73+
74+
@Test
75+
void testClosureStore() {
76+
try (var arena = SwiftArena.ofConfined()) {
77+
ClosureStore store = ClosureStore.init(arena);
78+
79+
AtomicLong counter = new AtomicLong(0);
80+
81+
// Add multiple closures
82+
ClosureStore.addClosure.closure closure1 = () -> {
83+
counter.incrementAndGet();
84+
};
85+
ClosureStore.addClosure.closure closure2 = () -> {
86+
counter.addAndGet(10);
87+
};
88+
ClosureStore.addClosure.closure closure3 = () -> {
89+
counter.addAndGet(100);
90+
};
91+
92+
store.addClosure(closure1);
93+
store.addClosure(closure2);
94+
store.addClosure(closure3);
95+
96+
assertEquals(3, store.count(), "Should have 3 closures stored");
97+
98+
// Execute all closures
99+
store.executeAll();
100+
assertEquals(111, counter.get(), "All closures should be executed");
101+
102+
// Execute again
103+
counter.set(0);
104+
store.executeAll();
105+
assertEquals(111, counter.get(), "Closures should be reusable");
106+
107+
// Clear - this releases closures on Swift side, triggering GlobalRef cleanup
108+
store.clear();
109+
assertEquals(0, store.count(), "Store should be empty after clear");
110+
}
111+
}
112+
113+
@Test
114+
void testMultipleEscapingClosures() {
115+
AtomicLong successValue = new AtomicLong(0);
116+
AtomicLong failureValue = new AtomicLong(0);
117+
118+
MySwiftLibrary.multipleEscapingClosures.onSuccess onSuccess = (value) -> {
119+
successValue.set(value);
120+
};
121+
MySwiftLibrary.multipleEscapingClosures.onFailure onFailure = (value) -> {
122+
failureValue.set(value);
123+
};
124+
125+
// Test success case
126+
MySwiftLibrary.multipleEscapingClosures(onSuccess, onFailure, true);
127+
assertEquals(42, successValue.get(), "Success callback should be called");
128+
assertEquals(0, failureValue.get(), "Failure callback should not be called");
129+
130+
// Reset and test failure case
131+
successValue.set(0);
132+
failureValue.set(0);
133+
MySwiftLibrary.multipleEscapingClosures(onSuccess, onFailure, false);
134+
assertEquals(0, successValue.get(), "Success callback should not be called");
135+
assertEquals(-1, failureValue.get(), "Failure callback should be called");
136+
}
137+
138+
@Test
139+
void testMultipleManagersWithDifferentClosures() {
140+
try (var arena = SwiftArena.ofConfined()) {
141+
CallbackManager manager1 = CallbackManager.init(arena);
142+
CallbackManager manager2 = CallbackManager.init(arena);
143+
144+
AtomicBoolean called1 = new AtomicBoolean(false);
145+
AtomicBoolean called2 = new AtomicBoolean(false);
146+
147+
CallbackManager.setCallback.callback callback1 = () -> {
148+
called1.set(true);
149+
};
150+
CallbackManager.setCallback.callback callback2 = () -> {
151+
called2.set(true);
152+
};
153+
154+
manager1.setCallback(callback1);
155+
manager2.setCallback(callback2);
156+
157+
// Trigger first manager
158+
manager1.triggerCallback();
159+
assertTrue(called1.get(), "First callback should be called");
160+
assertFalse(called2.get(), "Second callback should not be called");
161+
162+
// Trigger second manager
163+
manager2.triggerCallback();
164+
assertTrue(called2.get(), "Second callback should be called");
165+
}
166+
}
167+
}

Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -472,14 +472,14 @@ extension JNISwift2JavaGenerator {
472472
) {
473473
let apiParams = functionType.parameters.map({ $0.parameter.renderParameter() })
474474

475-
printer.print(
475+
printer.print(
476476
"""
477477
@FunctionalInterface
478478
public interface \(functionType.name) {
479479
\(functionType.result.javaType) apply(\(apiParams.joined(separator: ", ")));
480480
}
481481
"""
482-
)
482+
)
483483
}
484484

485485
private func printJavaBindingWrapperMethod(

0 commit comments

Comments
 (0)