From 912fe01e3a545c8023ef2f1cc318d1601be59455 Mon Sep 17 00:00:00 2001 From: Mads Odgaard Date: Thu, 7 Aug 2025 08:31:07 +0200 Subject: [PATCH 1/7] add auto arena to SwiftKit core that should work on Android --- .../com/example/swift/HelloJava2SwiftJNI.java | 4 +- .../com/example/swift/MySwiftClassTest.java | 30 +++++----- .../com/example/swift/MySwiftStructTest.java | 7 ++- .../java/com/example/swift/OptionalsTest.java | 6 +- .../swiftkit/core/AutoSwiftMemorySession.java | 56 ++++++++++++++++++ .../core/ConfinedSwiftMemorySession.java | 6 +- .../swift/swiftkit/core/JNISwiftInstance.java | 9 +-- .../org/swift/swiftkit/core/SwiftArena.java | 11 ++++ .../org/swift/swiftkit/core/ref/Cleaner.java | 51 +++++++++++++++++ .../swiftkit/core/ref/PhantomCleanable.java | 26 +++++++++ .../org/swift/swiftkit/AutoArenaTest.java | 57 +++++++++++++++++++ 11 files changed, 228 insertions(+), 35 deletions(-) create mode 100644 SwiftKitCore/src/main/java/org/swift/swiftkit/core/AutoSwiftMemorySession.java create mode 100644 SwiftKitCore/src/main/java/org/swift/swiftkit/core/ref/Cleaner.java create mode 100644 SwiftKitCore/src/main/java/org/swift/swiftkit/core/ref/PhantomCleanable.java create mode 100644 SwiftKitCore/src/test/java/org/swift/swiftkit/AutoArenaTest.java diff --git a/Samples/JExtractJNISampleApp/src/main/java/com/example/swift/HelloJava2SwiftJNI.java b/Samples/JExtractJNISampleApp/src/main/java/com/example/swift/HelloJava2SwiftJNI.java index 21cad317..3109f64e 100644 --- a/Samples/JExtractJNISampleApp/src/main/java/com/example/swift/HelloJava2SwiftJNI.java +++ b/Samples/JExtractJNISampleApp/src/main/java/com/example/swift/HelloJava2SwiftJNI.java @@ -18,8 +18,8 @@ // Import javakit/swiftkit support libraries +import org.swift.swiftkit.core.SwiftArena; import org.swift.swiftkit.core.SwiftLibraries; -import org.swift.swiftkit.core.ConfinedSwiftMemorySession; public class HelloJava2SwiftJNI { @@ -41,7 +41,7 @@ static void examples() { MySwiftClass.method(); - try (var arena = new ConfinedSwiftMemorySession()) { + try (var arena = SwiftArena.ofConfined()) { MySwiftClass myClass = MySwiftClass.init(10, 5, arena); MySwiftClass myClass2 = MySwiftClass.init(arena); diff --git a/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/MySwiftClassTest.java b/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/MySwiftClassTest.java index e7de03ad..2e9a7e62 100644 --- a/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/MySwiftClassTest.java +++ b/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/MySwiftClassTest.java @@ -15,7 +15,7 @@ package com.example.swift; import org.junit.jupiter.api.Test; -import org.swift.swiftkit.core.ConfinedSwiftMemorySession; +import org.swift.swiftkit.core.SwiftArena; import java.util.Optional; import java.util.OptionalInt; @@ -26,7 +26,7 @@ public class MySwiftClassTest { @Test void init_noParameters() { - try (var arena = new ConfinedSwiftMemorySession()) { + try (var arena = SwiftArena.ofConfined()) { MySwiftClass c = MySwiftClass.init(arena); assertNotNull(c); } @@ -34,7 +34,7 @@ void init_noParameters() { @Test void init_withParameters() { - try (var arena = new ConfinedSwiftMemorySession()) { + try (var arena = SwiftArena.ofConfined()) { MySwiftClass c = MySwiftClass.init(1337, 42, arena); assertNotNull(c); } @@ -42,7 +42,7 @@ void init_withParameters() { @Test void sum() { - try (var arena = new ConfinedSwiftMemorySession()) { + try (var arena = SwiftArena.ofConfined()) { MySwiftClass c = MySwiftClass.init(20, 10, arena); assertEquals(30, c.sum()); } @@ -50,7 +50,7 @@ void sum() { @Test void xMultiplied() { - try (var arena = new ConfinedSwiftMemorySession()) { + try (var arena = SwiftArena.ofConfined()) { MySwiftClass c = MySwiftClass.init(20, 10, arena); assertEquals(200, c.xMultiplied(10)); } @@ -58,7 +58,7 @@ void xMultiplied() { @Test void throwingFunction() { - try (var arena = new ConfinedSwiftMemorySession()) { + try (var arena = SwiftArena.ofConfined()) { MySwiftClass c = MySwiftClass.init(20, 10, arena); Exception exception = assertThrows(Exception.class, () -> c.throwingFunction()); @@ -68,7 +68,7 @@ void throwingFunction() { @Test void constant() { - try (var arena = new ConfinedSwiftMemorySession()) { + try (var arena = SwiftArena.ofConfined()) { MySwiftClass c = MySwiftClass.init(20, 10, arena); assertEquals(100, c.getConstant()); } @@ -76,7 +76,7 @@ void constant() { @Test void mutable() { - try (var arena = new ConfinedSwiftMemorySession()) { + try (var arena = SwiftArena.ofConfined()) { MySwiftClass c = MySwiftClass.init(20, 10, arena); assertEquals(0, c.getMutable()); c.setMutable(42); @@ -86,7 +86,7 @@ void mutable() { @Test void product() { - try (var arena = new ConfinedSwiftMemorySession()) { + try (var arena = SwiftArena.ofConfined()) { MySwiftClass c = MySwiftClass.init(20, 10, arena); assertEquals(200, c.getProduct()); } @@ -94,7 +94,7 @@ void product() { @Test void throwingVariable() { - try (var arena = new ConfinedSwiftMemorySession()) { + try (var arena = SwiftArena.ofConfined()) { MySwiftClass c = MySwiftClass.init(20, 10, arena); Exception exception = assertThrows(Exception.class, () -> c.getThrowingVariable()); @@ -105,7 +105,7 @@ void throwingVariable() { @Test void mutableDividedByTwo() { - try (var arena = new ConfinedSwiftMemorySession()) { + try (var arena = SwiftArena.ofConfined()) { MySwiftClass c = MySwiftClass.init(20, 10, arena); assertEquals(0, c.getMutableDividedByTwo()); c.setMutable(20); @@ -117,7 +117,7 @@ void mutableDividedByTwo() { @Test void isWarm() { - try (var arena = new ConfinedSwiftMemorySession()) { + try (var arena = SwiftArena.ofConfined()) { MySwiftClass c = MySwiftClass.init(20, 10, arena); assertFalse(c.isWarm()); } @@ -125,7 +125,7 @@ void isWarm() { @Test void sumWithX() { - try (var arena = new ConfinedSwiftMemorySession()) { + try (var arena = SwiftArena.ofConfined()) { MySwiftClass c1 = MySwiftClass.init(20, 10, arena); MySwiftClass c2 = MySwiftClass.init(50, 10, arena); assertEquals(70, c1.sumX(c2)); @@ -134,7 +134,7 @@ void sumWithX() { @Test void copy() { - try (var arena = new ConfinedSwiftMemorySession()) { + try (var arena = SwiftArena.ofConfined()) { MySwiftClass c1 = MySwiftClass.init(20, 10, arena); MySwiftClass c2 = c1.copy(arena); @@ -146,7 +146,7 @@ void copy() { @Test void addXWithJavaLong() { - try (var arena = new ConfinedSwiftMemorySession()) { + try (var arena = SwiftArena.ofConfined()) { MySwiftClass c1 = MySwiftClass.init(20, 10, arena); Long javaLong = 50L; assertEquals(70, c1.addXWithJavaLong(javaLong)); diff --git a/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/MySwiftStructTest.java b/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/MySwiftStructTest.java index 9eeaf029..c2c1170b 100644 --- a/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/MySwiftStructTest.java +++ b/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/MySwiftStructTest.java @@ -16,13 +16,14 @@ import org.junit.jupiter.api.Test; import org.swift.swiftkit.core.ConfinedSwiftMemorySession; +import org.swift.swiftkit.core.SwiftArena; import static org.junit.jupiter.api.Assertions.*; public class MySwiftStructTest { @Test void init() { - try (var arena = new ConfinedSwiftMemorySession()) { + try (var arena = SwiftArena.ofConfined()) { MySwiftStruct s = MySwiftStruct.init(1337, 42, arena); assertEquals(1337, s.getCapacity()); assertEquals(42, s.getLen()); @@ -31,7 +32,7 @@ void init() { @Test void getAndSetLen() { - try (var arena = new ConfinedSwiftMemorySession()) { + try (var arena = SwiftArena.ofConfined()) { MySwiftStruct s = MySwiftStruct.init(1337, 42, arena); s.setLen(100); assertEquals(100, s.getLen()); @@ -40,7 +41,7 @@ void getAndSetLen() { @Test void increaseCap() { - try (var arena = new ConfinedSwiftMemorySession()) { + try (var arena = SwiftArena.ofConfined()) { MySwiftStruct s = MySwiftStruct.init(1337, 42, arena); long newCap = s.increaseCap(10); assertEquals(1347, newCap); diff --git a/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/OptionalsTest.java b/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/OptionalsTest.java index f7262ad4..d60ff6d5 100644 --- a/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/OptionalsTest.java +++ b/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/OptionalsTest.java @@ -15,7 +15,7 @@ package com.example.swift; import org.junit.jupiter.api.Test; -import org.swift.swiftkit.core.ConfinedSwiftMemorySession; +import org.swift.swiftkit.core.SwiftArena; import java.util.Optional; import java.util.OptionalDouble; @@ -82,7 +82,7 @@ void optionalString() { @Test void optionalClass() { - try (var arena = new ConfinedSwiftMemorySession()) { + try (var arena = SwiftArena.ofConfined()) { MySwiftClass c = MySwiftClass.init(arena); assertEquals(Optional.empty(), MySwiftLibrary.optionalClass(Optional.empty(), arena)); Optional optionalClass = MySwiftLibrary.optionalClass(Optional.of(c), arena); @@ -99,7 +99,7 @@ void optionalJavaKitLong() { @Test void multipleOptionals() { - try (var arena = new ConfinedSwiftMemorySession()) { + try (var arena = SwiftArena.ofConfined()) { MySwiftClass c = MySwiftClass.init(arena); OptionalLong result = MySwiftLibrary.multipleOptionals( Optional.of((byte) 1), diff --git a/SwiftKitCore/src/main/java/org/swift/swiftkit/core/AutoSwiftMemorySession.java b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/AutoSwiftMemorySession.java new file mode 100644 index 00000000..ed00d47a --- /dev/null +++ b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/AutoSwiftMemorySession.java @@ -0,0 +1,56 @@ +//===----------------------------------------------------------------------===// +// +// 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 org.swift.swiftkit.core; + + +import org.swift.swiftkit.core.ref.Cleaner; + +import java.util.Objects; +import java.util.concurrent.ThreadFactory; + +/** + * A memory session which manages registered objects via the Garbage Collector. + * + *

When registered Java wrapper classes around native Swift instances {@link SwiftInstance}, + * are eligible for collection, this will trigger the cleanup of the native resources as well. + * + *

This memory session is LESS reliable than using a {@link ConfinedSwiftMemorySession} because + * the timing of when the native resources are cleaned up is somewhat undefined, and rely on the + * system GC. Meaning, that if an object nas been promoted to an old generation, there may be a + * long time between the resource no longer being referenced "in Java" and its native memory being released, + * and also the deinit of the Swift type being run. + * + *

This can be problematic for Swift applications which rely on quick release of resources, and may expect + * the deinits to run in expected and "quick" succession. + * + *

Whenever possible, prefer using an explicitly managed {@link SwiftArena}, such as {@link SwiftArena#ofConfined()}. + */ +final class AutoSwiftMemorySession implements SwiftArena { + private final Cleaner cleaner; + + public AutoSwiftMemorySession(ThreadFactory cleanerThreadFactory) { + this.cleaner = Cleaner.create(cleanerThreadFactory); + } + + @Override + public void register(SwiftInstance instance) { + Objects.requireNonNull(instance, "value"); + + // We make sure we don't capture `instance` in the + // cleanup action, so we can ignore the warning below. + var cleanupAction = instance.$createCleanup(); + cleaner.register(instance, cleanupAction); + } +} \ No newline at end of file diff --git a/SwiftKitCore/src/main/java/org/swift/swiftkit/core/ConfinedSwiftMemorySession.java b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/ConfinedSwiftMemorySession.java index 7c6e80fb..4383a6fe 100644 --- a/SwiftKitCore/src/main/java/org/swift/swiftkit/core/ConfinedSwiftMemorySession.java +++ b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/ConfinedSwiftMemorySession.java @@ -28,17 +28,13 @@ public class ConfinedSwiftMemorySession implements ClosableSwiftArena { final ConfinedResourceList resources; - public ConfinedSwiftMemorySession() { - this(Thread.currentThread()); - } - public ConfinedSwiftMemorySession(Thread owner) { this.owner = owner; this.state = new AtomicInteger(ACTIVE); this.resources = new ConfinedResourceList(); } - public void checkValid() throws RuntimeException { + void checkValid() throws RuntimeException { if (this.owner != null && this.owner != Thread.currentThread()) { throw new WrongThreadException(String.format("ConfinedSwift arena is confined to %s but was closed from %s!", this.owner, Thread.currentThread())); } else if (this.state.get() < ACTIVE) { diff --git a/SwiftKitCore/src/main/java/org/swift/swiftkit/core/JNISwiftInstance.java b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/JNISwiftInstance.java index 6b30ed2f..95f1e5a0 100644 --- a/SwiftKitCore/src/main/java/org/swift/swiftkit/core/JNISwiftInstance.java +++ b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/JNISwiftInstance.java @@ -56,13 +56,8 @@ protected JNISwiftInstance(long selfPointer, SwiftArena arena) { @Override public SwiftInstanceCleanup $createCleanup() { - final AtomicBoolean statusDestroyedFlag = $statusDestroyedFlag(); - Runnable markAsDestroyed = new Runnable() { - @Override - public void run() { - statusDestroyedFlag.set(true); - } - }; + var statusDestroyedFlag = $statusDestroyedFlag(); + Runnable markAsDestroyed = () -> statusDestroyedFlag.set(true); return new JNISwiftInstanceCleanup(this.$createDestroyFunction(), markAsDestroyed); } diff --git a/SwiftKitCore/src/main/java/org/swift/swiftkit/core/SwiftArena.java b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/SwiftArena.java index ed16d250..0914ab47 100644 --- a/SwiftKitCore/src/main/java/org/swift/swiftkit/core/SwiftArena.java +++ b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/SwiftArena.java @@ -14,6 +14,8 @@ package org.swift.swiftkit.core; +import java.util.concurrent.ThreadFactory; + /** * A Swift arena manages Swift allocated memory for classes, structs, enums etc. * When an arena is closed, it will destroy all managed swift objects in a way appropriate to their type. @@ -27,6 +29,15 @@ public interface SwiftArena { * Its memory should be considered managed by this arena, and be destroyed when the arena is closed. */ void register(SwiftInstance instance); + + static ClosableSwiftArena ofConfined() { + return new ConfinedSwiftMemorySession(Thread.currentThread()); + } + + static SwiftArena ofAuto() { + ThreadFactory cleanerThreadFactory = r -> new Thread(r, "AutoSwiftArenaCleanerThread"); + return new AutoSwiftMemorySession(cleanerThreadFactory); + } } /** diff --git a/SwiftKitCore/src/main/java/org/swift/swiftkit/core/ref/Cleaner.java b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/ref/Cleaner.java new file mode 100644 index 00000000..42b0afc8 --- /dev/null +++ b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/ref/Cleaner.java @@ -0,0 +1,51 @@ +package org.swift.swiftkit.core.ref; + +import java.lang.ref.ReferenceQueue; +import java.util.LinkedList; +import java.util.Objects; +import java.util.concurrent.ThreadFactory; + +public class Cleaner implements Runnable { + final ReferenceQueue referenceQueue; + final LinkedList list; + + private Cleaner() { + this.referenceQueue = new ReferenceQueue<>(); + this.list = new LinkedList<>(); + } + + public static Cleaner create(ThreadFactory threadFactory) { + Cleaner cleaner = new Cleaner(); + cleaner.start(threadFactory); + return cleaner; + } + + void start(ThreadFactory threadFactory) { + // This makes sure the linked list is not empty when the thread starts, + // and the thread will run at least until the cleaner itself can be GCed. + new PhantomCleanable(this, this, () -> {}); + + Thread thread = threadFactory.newThread(this); + thread.setDaemon(true); + thread.start(); + } + + public void register(Object resourceHolder, Runnable cleaningAction) { + Objects.requireNonNull(resourceHolder, "resourceHolder"); + Objects.requireNonNull(cleaningAction, "cleaningAction"); + new PhantomCleanable(resourceHolder, this, cleaningAction); + } + + @Override + public void run() { + while (!list.isEmpty()) { + try { + PhantomCleanable removed = (PhantomCleanable) referenceQueue.remove(60 * 1000L); + removed.cleanup(); + } catch (Throwable e) { + // ignore exceptions from the cleanup action + // (including interruption of cleanup thread) + } + } + } +} diff --git a/SwiftKitCore/src/main/java/org/swift/swiftkit/core/ref/PhantomCleanable.java b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/ref/PhantomCleanable.java new file mode 100644 index 00000000..7320db2d --- /dev/null +++ b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/ref/PhantomCleanable.java @@ -0,0 +1,26 @@ +package org.swift.swiftkit.core.ref; + +import java.lang.ref.PhantomReference; + +/** + * PhantomCleanableRef + * + * @author Mads Odgaard (202206257) + */ +public class PhantomCleanable extends PhantomReference { + private final Runnable cleanupAction; + private final Cleaner cleaner; + + public PhantomCleanable(Object referent, Cleaner cleaner, Runnable cleanupAction) { + super(referent, cleaner.referenceQueue); + this.cleanupAction = cleanupAction; + this.cleaner = cleaner; + cleaner.list.add(this); + } + + public void cleanup() { + if (cleaner.list.remove(this)) { + cleanupAction.run(); + } + } +} diff --git a/SwiftKitCore/src/test/java/org/swift/swiftkit/AutoArenaTest.java b/SwiftKitCore/src/test/java/org/swift/swiftkit/AutoArenaTest.java new file mode 100644 index 00000000..a6e1d788 --- /dev/null +++ b/SwiftKitCore/src/test/java/org/swift/swiftkit/AutoArenaTest.java @@ -0,0 +1,57 @@ +//===----------------------------------------------------------------------===// +// +// 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 org.swift.swiftkit; + +import org.junit.jupiter.api.Test; +import org.swift.swiftkit.core.JNISwiftInstance; +import org.swift.swiftkit.core.SwiftArena; + +public class AutoArenaTest { + + @Test + @SuppressWarnings("removal") // System.runFinalization() will be removed + public void cleaner_releases_native_resource() { + SwiftArena arena = SwiftArena.ofAuto(); + + // This object is registered to the arena. + var object = new FakeSwiftInstance(arena); + var statusDestroyedFlag = object.$statusDestroyedFlag(); + + // Release the object and hope it gets GC-ed soon + + // noinspection UnusedAssignment + object = null; + + var i = 1_000; + while (!statusDestroyedFlag.get()) { + System.runFinalization(); + System.gc(); + + if (i-- < 1) { + throw new RuntimeException("Reference was not cleaned up! Did Cleaner not pick up the release?"); + } + } + } + + private static class FakeSwiftInstance extends JNISwiftInstance { + public FakeSwiftInstance(SwiftArena arena) { + super(1, arena); + } + + protected Runnable $createDestroyFunction() { + return () -> {}; + } + } +} From 8bc70964f24520d12f84820d10e7e678ca1c2d51 Mon Sep 17 00:00:00 2001 From: Mads Odgaard Date: Thu, 7 Aug 2025 08:46:15 +0200 Subject: [PATCH 2/7] add `memory-management-mode` config --- .../Configuration.swift | 5 +++++ .../GenerationMode.swift | 15 +++++++++++++++ .../SwiftJavaTool/Commands/JExtractCommand.swift | 9 +++++++++ 3 files changed, 29 insertions(+) diff --git a/Sources/JavaKitConfigurationShared/Configuration.swift b/Sources/JavaKitConfigurationShared/Configuration.swift index 0ff089da..64c5efd9 100644 --- a/Sources/JavaKitConfigurationShared/Configuration.swift +++ b/Sources/JavaKitConfigurationShared/Configuration.swift @@ -51,6 +51,11 @@ public struct Configuration: Codable { minimumInputAccessLevelMode ?? .default } + public var memoryManagementMode: JExtractMemoryManagementMode? + public var effectiveMemoryManagementMode: JExtractMemoryManagementMode { + memoryManagementMode ?? .forceExplicit + } + // ==== java 2 swift --------------------------------------------------------- /// The Java class path that should be passed along to the swift-java tool. diff --git a/Sources/JavaKitConfigurationShared/GenerationMode.swift b/Sources/JavaKitConfigurationShared/GenerationMode.swift index 1feac411..46050dbf 100644 --- a/Sources/JavaKitConfigurationShared/GenerationMode.swift +++ b/Sources/JavaKitConfigurationShared/GenerationMode.swift @@ -76,3 +76,18 @@ extension JExtractMinimumAccessLevelMode { .public } } + + +/// Configures how memory should be managed by the user +public enum JExtractMemoryManagementMode: String, Codable { + /// Force users to provide an explicit `SwiftArena` to all calls that require them. + case forceExplicit + + /// Provide both explicit `SwiftArena` support + /// and a default global automatic `SwiftArena` that will deallocate memory when the GC decides to. + case allowAutomatic + + /// Force all memory management to a default global automatic `SwiftArena` + /// that will deallocate memory when the GC decides to. + case forceAutomatic +} diff --git a/Sources/SwiftJavaTool/Commands/JExtractCommand.swift b/Sources/SwiftJavaTool/Commands/JExtractCommand.swift index 54a99708..e9579b7f 100644 --- a/Sources/SwiftJavaTool/Commands/JExtractCommand.swift +++ b/Sources/SwiftJavaTool/Commands/JExtractCommand.swift @@ -67,6 +67,9 @@ extension SwiftJava { @Option(help: "The lowest access level of Swift declarations that should be extracted, defaults to 'public'.") var minimumInputAccessLevel: JExtractMinimumAccessLevelMode = .default + @Option(help: "The memory management mode to use for the generated code. By default, the user must explicitly provide `SwiftArena` to all calls that require it. By choosing `allow-automatic`, user can omit this parameter and a global GC-based arena will be used. `force-automatic` removes all explicit memory management.") + var memoryManagementMode: JExtractMemoryManagementMode = .forceExplicit + @Option( help: """ A swift-java configuration file for a given Swift module name on which this module depends, @@ -89,6 +92,7 @@ extension SwiftJava.JExtractCommand { config.writeEmptyFiles = writeEmptyFiles config.unsignedNumbersMode = unsignedNumbers config.minimumInputAccessLevelMode = minimumInputAccessLevel + config.memoryManagementMode = memoryManagementMode try checkModeCompatibility() @@ -117,6 +121,10 @@ extension SwiftJava.JExtractCommand { case .wrapGuava: () // OK } + } else if self.mode == .ffm { + guard self.memoryManagementMode == .forceExplicit else { + throw IllegalModeCombinationError("FFM mode does not support '\(self.memoryManagementMode)' memory management mode! \(Self.helpMessage)") + } } } } @@ -148,3 +156,4 @@ struct IllegalModeCombinationError: Error { extension JExtractGenerationMode: ExpressibleByArgument {} extension JExtractUnsignedIntegerMode: ExpressibleByArgument {} extension JExtractMinimumAccessLevelMode: ExpressibleByArgument {} +extension JExtractMemoryManagementMode: ExpressibleByArgument {} From b1f50bc550cce133c0cb39f22c1fc4d4a554a6e5 Mon Sep 17 00:00:00 2001 From: Mads Odgaard Date: Thu, 7 Aug 2025 10:08:41 +0200 Subject: [PATCH 3/7] generate wrappers that respect mode --- ...t2JavaGenerator+JavaBindingsPrinting.swift | 42 +++++++++++++++---- .../Configuration.swift | 2 +- .../GenerationMode.swift | 11 +++++ .../Commands/JExtractCommand.swift | 2 +- 4 files changed, 48 insertions(+), 9 deletions(-) diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift index 0daf14de..6ae061aa 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift @@ -16,6 +16,8 @@ import JavaTypes // MARK: Defaults +private let globalArenaName = "GLOBAL_ARENA" + extension JNISwift2JavaGenerator { /// Default set Java imports for every generated file static let defaultJavaImports: Array = [ @@ -72,6 +74,11 @@ extension JNISwift2JavaGenerator { printImports(&printer) printModuleClass(&printer) { printer in + if config.effectiveMemoryManagementMode.requiresGlobalArena { + printer.print("static final SwiftArena \(globalArenaName) = SwiftArena.ofAuto();") + printer.println() + } + printer.print( """ static final String LIB_NAME = "\(swiftModuleName)"; @@ -247,29 +254,50 @@ extension JNISwift2JavaGenerator { guard let translatedDecl = translatedDecl(for: decl) else { fatalError("Decl was not translated, \(decl)") } + let translatedSignature = translatedDecl.translatedFunctionSignature var modifiers = ["public"] + if decl.isStatic || decl.isInitializer || !decl.hasParent { modifiers.append("static") } - let translatedSignature = translatedDecl.translatedFunctionSignature let resultType = translatedSignature.resultType.javaType - var parameters = translatedDecl.translatedFunctionSignature.parameters.map({ $0.parameter.renderParameter() }) - if translatedSignature.requiresSwiftArena { - parameters.append("SwiftArena swiftArena$") - } + var parameters = translatedDecl.translatedFunctionSignature.parameters.map { $0.parameter.renderParameter() } let throwsClause = decl.isThrowing ? " throws Exception" : "" var annotationsStr = translatedSignature.annotations.map({ $0.render() }).joined(separator: "\n") if !annotationsStr.isEmpty { annotationsStr += "\n" } - let modifiersStr = modifiers.joined(separator: " ") let parametersStr = parameters.joined(separator: ", ") + // Print default global arena variation + if config.effectiveMemoryManagementMode.requiresGlobalArena && translatedSignature.requiresSwiftArena { + printDeclDocumentation(&printer, decl) + printer.printBraceBlock( + "\(annotationsStr)\(modifiers.joined(separator: " ")) \(resultType) \(translatedDecl.name)(\(parametersStr))\(throwsClause)" + ) { printer in + let arguments = translatedDecl.translatedFunctionSignature.parameters.map(\.parameter.name) + ["\(swiftModuleName).\(globalArenaName)"] + let call = "\(translatedDecl.name)(\(arguments.joined(separator: ", ")))" + if translatedDecl.translatedFunctionSignature.resultType.javaType.isVoid { + printer.print("\(call);") + } else { + printer.print("return \(call);") + } + } + printer.println() + } + + // Make any function with explicit arena private if we force automatic. + if config.effectiveMemoryManagementMode == .forceAutomatic && translatedSignature.requiresSwiftArena { + modifiers[0] = "private" + } + if translatedSignature.requiresSwiftArena { + parameters.append("SwiftArena swiftArena$") + } printDeclDocumentation(&printer, decl) printer.printBraceBlock( - "\(annotationsStr)\(modifiersStr) \(resultType) \(translatedDecl.name)(\(parametersStr))\(throwsClause)" + "\(annotationsStr)\(modifiers.joined(separator: " ")) \(resultType) \(translatedDecl.name)(\(parameters.joined(separator: ", ")))\(throwsClause)" ) { printer in printDowncall(&printer, decl) } diff --git a/Sources/JavaKitConfigurationShared/Configuration.swift b/Sources/JavaKitConfigurationShared/Configuration.swift index 64c5efd9..2d9b4311 100644 --- a/Sources/JavaKitConfigurationShared/Configuration.swift +++ b/Sources/JavaKitConfigurationShared/Configuration.swift @@ -53,7 +53,7 @@ public struct Configuration: Codable { public var memoryManagementMode: JExtractMemoryManagementMode? public var effectiveMemoryManagementMode: JExtractMemoryManagementMode { - memoryManagementMode ?? .forceExplicit + memoryManagementMode ?? .default } // ==== java 2 swift --------------------------------------------------------- diff --git a/Sources/JavaKitConfigurationShared/GenerationMode.swift b/Sources/JavaKitConfigurationShared/GenerationMode.swift index 46050dbf..fa383471 100644 --- a/Sources/JavaKitConfigurationShared/GenerationMode.swift +++ b/Sources/JavaKitConfigurationShared/GenerationMode.swift @@ -90,4 +90,15 @@ public enum JExtractMemoryManagementMode: String, Codable { /// Force all memory management to a default global automatic `SwiftArena` /// that will deallocate memory when the GC decides to. case forceAutomatic + + public static var `default`: Self { + .forceExplicit + } + + public var requiresGlobalArena: Bool { + switch self { + case .forceExplicit: false + case .allowAutomatic, .forceAutomatic: true + } + } } diff --git a/Sources/SwiftJavaTool/Commands/JExtractCommand.swift b/Sources/SwiftJavaTool/Commands/JExtractCommand.swift index e9579b7f..bdac586b 100644 --- a/Sources/SwiftJavaTool/Commands/JExtractCommand.swift +++ b/Sources/SwiftJavaTool/Commands/JExtractCommand.swift @@ -68,7 +68,7 @@ extension SwiftJava { var minimumInputAccessLevel: JExtractMinimumAccessLevelMode = .default @Option(help: "The memory management mode to use for the generated code. By default, the user must explicitly provide `SwiftArena` to all calls that require it. By choosing `allow-automatic`, user can omit this parameter and a global GC-based arena will be used. `force-automatic` removes all explicit memory management.") - var memoryManagementMode: JExtractMemoryManagementMode = .forceExplicit + var memoryManagementMode: JExtractMemoryManagementMode = .default @Option( help: """ From 659f888eb6a67af3f7e272806d1ac041ae90bded Mon Sep 17 00:00:00 2001 From: Mads Odgaard Date: Thu, 7 Aug 2025 10:13:47 +0200 Subject: [PATCH 4/7] make cleaner thread-safe --- .../src/main/java/org/swift/swiftkit/core/ref/Cleaner.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/SwiftKitCore/src/main/java/org/swift/swiftkit/core/ref/Cleaner.java b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/ref/Cleaner.java index 42b0afc8..f8ca0b74 100644 --- a/SwiftKitCore/src/main/java/org/swift/swiftkit/core/ref/Cleaner.java +++ b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/ref/Cleaner.java @@ -1,17 +1,19 @@ package org.swift.swiftkit.core.ref; import java.lang.ref.ReferenceQueue; +import java.util.Collections; import java.util.LinkedList; +import java.util.List; import java.util.Objects; import java.util.concurrent.ThreadFactory; public class Cleaner implements Runnable { final ReferenceQueue referenceQueue; - final LinkedList list; + final List list; private Cleaner() { this.referenceQueue = new ReferenceQueue<>(); - this.list = new LinkedList<>(); + this.list = Collections.synchronizedList(new LinkedList<>()); } public static Cleaner create(ThreadFactory threadFactory) { From 01c742769a382bd6d02cb6edd2430e5d9549d56c Mon Sep 17 00:00:00 2001 From: Mads Odgaard Date: Thu, 7 Aug 2025 10:15:53 +0200 Subject: [PATCH 5/7] update license headers --- .../swiftkit/core/AutoSwiftMemorySession.java | 2 +- .../org/swift/swiftkit/core/ref/Cleaner.java | 14 ++++++++++++++ .../swiftkit/core/ref/PhantomCleanable.java | 19 ++++++++++++++----- .../org/swift/swiftkit/AutoArenaTest.java | 2 +- 4 files changed, 30 insertions(+), 7 deletions(-) diff --git a/SwiftKitCore/src/main/java/org/swift/swiftkit/core/AutoSwiftMemorySession.java b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/AutoSwiftMemorySession.java index ed00d47a..c6b916fd 100644 --- a/SwiftKitCore/src/main/java/org/swift/swiftkit/core/AutoSwiftMemorySession.java +++ b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/AutoSwiftMemorySession.java @@ -2,7 +2,7 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information diff --git a/SwiftKitCore/src/main/java/org/swift/swiftkit/core/ref/Cleaner.java b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/ref/Cleaner.java index f8ca0b74..56dd7dbd 100644 --- a/SwiftKitCore/src/main/java/org/swift/swiftkit/core/ref/Cleaner.java +++ b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/ref/Cleaner.java @@ -1,3 +1,17 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + package org.swift.swiftkit.core.ref; import java.lang.ref.ReferenceQueue; diff --git a/SwiftKitCore/src/main/java/org/swift/swiftkit/core/ref/PhantomCleanable.java b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/ref/PhantomCleanable.java index 7320db2d..5cc0e398 100644 --- a/SwiftKitCore/src/main/java/org/swift/swiftkit/core/ref/PhantomCleanable.java +++ b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/ref/PhantomCleanable.java @@ -1,12 +1,21 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + package org.swift.swiftkit.core.ref; import java.lang.ref.PhantomReference; -/** - * PhantomCleanableRef - * - * @author Mads Odgaard (202206257) - */ public class PhantomCleanable extends PhantomReference { private final Runnable cleanupAction; private final Cleaner cleaner; diff --git a/SwiftKitCore/src/test/java/org/swift/swiftkit/AutoArenaTest.java b/SwiftKitCore/src/test/java/org/swift/swiftkit/AutoArenaTest.java index a6e1d788..b414962f 100644 --- a/SwiftKitCore/src/test/java/org/swift/swiftkit/AutoArenaTest.java +++ b/SwiftKitCore/src/test/java/org/swift/swiftkit/AutoArenaTest.java @@ -2,7 +2,7 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information From 628b22311549a420a7365d4e76ad669919af8d15 Mon Sep 17 00:00:00 2001 From: Mads Odgaard Date: Thu, 7 Aug 2025 18:34:32 +0200 Subject: [PATCH 6/7] PR feedback --- ...t2JavaGenerator+JavaBindingsPrinting.swift | 14 ++------------ .../GenerationMode.swift | 14 +++++--------- .../Commands/JExtractCommand.swift | 2 +- .../swiftkit/core/AutoSwiftMemorySession.java | 8 ++++---- .../org/swift/swiftkit/core/SwiftArena.java | 2 +- .../swiftkit/core/SwiftMemoryManagement.java | 19 +++++++++++++++++++ .../swiftkit/core/ref/PhantomCleanable.java | 12 ++++++------ .../ref/{Cleaner.java => SwiftCleaner.java} | 12 ++++++------ 8 files changed, 44 insertions(+), 39 deletions(-) create mode 100644 SwiftKitCore/src/main/java/org/swift/swiftkit/core/SwiftMemoryManagement.java rename SwiftKitCore/src/main/java/org/swift/swiftkit/core/ref/{Cleaner.java => SwiftCleaner.java} (88%) diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift index 6ae061aa..0920a89b 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift @@ -16,8 +16,6 @@ import JavaTypes // MARK: Defaults -private let globalArenaName = "GLOBAL_ARENA" - extension JNISwift2JavaGenerator { /// Default set Java imports for every generated file static let defaultJavaImports: Array = [ @@ -74,11 +72,6 @@ extension JNISwift2JavaGenerator { printImports(&printer) printModuleClass(&printer) { printer in - if config.effectiveMemoryManagementMode.requiresGlobalArena { - printer.print("static final SwiftArena \(globalArenaName) = SwiftArena.ofAuto();") - printer.println() - } - printer.print( """ static final String LIB_NAME = "\(swiftModuleName)"; @@ -277,7 +270,8 @@ extension JNISwift2JavaGenerator { printer.printBraceBlock( "\(annotationsStr)\(modifiers.joined(separator: " ")) \(resultType) \(translatedDecl.name)(\(parametersStr))\(throwsClause)" ) { printer in - let arguments = translatedDecl.translatedFunctionSignature.parameters.map(\.parameter.name) + ["\(swiftModuleName).\(globalArenaName)"] + let globalArenaName = "SwiftMemoryManagement.GLOBAL_SWIFT_JAVA_ARENA" + let arguments = translatedDecl.translatedFunctionSignature.parameters.map(\.parameter.name) + [globalArenaName] let call = "\(translatedDecl.name)(\(arguments.joined(separator: ", ")))" if translatedDecl.translatedFunctionSignature.resultType.javaType.isVoid { printer.print("\(call);") @@ -288,10 +282,6 @@ extension JNISwift2JavaGenerator { printer.println() } - // Make any function with explicit arena private if we force automatic. - if config.effectiveMemoryManagementMode == .forceAutomatic && translatedSignature.requiresSwiftArena { - modifiers[0] = "private" - } if translatedSignature.requiresSwiftArena { parameters.append("SwiftArena swiftArena$") } diff --git a/Sources/JavaKitConfigurationShared/GenerationMode.swift b/Sources/JavaKitConfigurationShared/GenerationMode.swift index fa383471..22fdd5f5 100644 --- a/Sources/JavaKitConfigurationShared/GenerationMode.swift +++ b/Sources/JavaKitConfigurationShared/GenerationMode.swift @@ -81,24 +81,20 @@ extension JExtractMinimumAccessLevelMode { /// Configures how memory should be managed by the user public enum JExtractMemoryManagementMode: String, Codable { /// Force users to provide an explicit `SwiftArena` to all calls that require them. - case forceExplicit + case explicit /// Provide both explicit `SwiftArena` support /// and a default global automatic `SwiftArena` that will deallocate memory when the GC decides to. - case allowAutomatic - - /// Force all memory management to a default global automatic `SwiftArena` - /// that will deallocate memory when the GC decides to. - case forceAutomatic + case allowGlobalAutomatic public static var `default`: Self { - .forceExplicit + .explicit } public var requiresGlobalArena: Bool { switch self { - case .forceExplicit: false - case .allowAutomatic, .forceAutomatic: true + case .explicit: false + case .allowGlobalAutomatic: true } } } diff --git a/Sources/SwiftJavaTool/Commands/JExtractCommand.swift b/Sources/SwiftJavaTool/Commands/JExtractCommand.swift index bdac586b..5eb9486b 100644 --- a/Sources/SwiftJavaTool/Commands/JExtractCommand.swift +++ b/Sources/SwiftJavaTool/Commands/JExtractCommand.swift @@ -122,7 +122,7 @@ extension SwiftJava.JExtractCommand { () // OK } } else if self.mode == .ffm { - guard self.memoryManagementMode == .forceExplicit else { + guard self.memoryManagementMode == .explicit else { throw IllegalModeCombinationError("FFM mode does not support '\(self.memoryManagementMode)' memory management mode! \(Self.helpMessage)") } } diff --git a/SwiftKitCore/src/main/java/org/swift/swiftkit/core/AutoSwiftMemorySession.java b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/AutoSwiftMemorySession.java index c6b916fd..36e73209 100644 --- a/SwiftKitCore/src/main/java/org/swift/swiftkit/core/AutoSwiftMemorySession.java +++ b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/AutoSwiftMemorySession.java @@ -15,7 +15,7 @@ package org.swift.swiftkit.core; -import org.swift.swiftkit.core.ref.Cleaner; +import org.swift.swiftkit.core.ref.SwiftCleaner; import java.util.Objects; import java.util.concurrent.ThreadFactory; @@ -38,10 +38,10 @@ *

Whenever possible, prefer using an explicitly managed {@link SwiftArena}, such as {@link SwiftArena#ofConfined()}. */ final class AutoSwiftMemorySession implements SwiftArena { - private final Cleaner cleaner; + private final SwiftCleaner swiftCleaner; public AutoSwiftMemorySession(ThreadFactory cleanerThreadFactory) { - this.cleaner = Cleaner.create(cleanerThreadFactory); + this.swiftCleaner = SwiftCleaner.create(cleanerThreadFactory); } @Override @@ -51,6 +51,6 @@ public void register(SwiftInstance instance) { // We make sure we don't capture `instance` in the // cleanup action, so we can ignore the warning below. var cleanupAction = instance.$createCleanup(); - cleaner.register(instance, cleanupAction); + swiftCleaner.register(instance, cleanupAction); } } \ No newline at end of file diff --git a/SwiftKitCore/src/main/java/org/swift/swiftkit/core/SwiftArena.java b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/SwiftArena.java index 0914ab47..3b6c4626 100644 --- a/SwiftKitCore/src/main/java/org/swift/swiftkit/core/SwiftArena.java +++ b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/SwiftArena.java @@ -23,7 +23,7 @@ *

A confined arena has an associated owner thread that confines some operations to * associated owner thread such as {@link ClosableSwiftArena#close()}. */ -public interface SwiftArena { +public interface SwiftArena { /** * Register a Swift object. * Its memory should be considered managed by this arena, and be destroyed when the arena is closed. diff --git a/SwiftKitCore/src/main/java/org/swift/swiftkit/core/SwiftMemoryManagement.java b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/SwiftMemoryManagement.java new file mode 100644 index 00000000..2b9a1209 --- /dev/null +++ b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/SwiftMemoryManagement.java @@ -0,0 +1,19 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +package org.swift.swiftkit.core; + +public class SwiftMemoryManagement { + public static final SwiftArena GLOBAL_SWIFT_JAVA_ARENA = SwiftArena.ofAuto(); +} diff --git a/SwiftKitCore/src/main/java/org/swift/swiftkit/core/ref/PhantomCleanable.java b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/ref/PhantomCleanable.java index 5cc0e398..2efcdae7 100644 --- a/SwiftKitCore/src/main/java/org/swift/swiftkit/core/ref/PhantomCleanable.java +++ b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/ref/PhantomCleanable.java @@ -18,17 +18,17 @@ public class PhantomCleanable extends PhantomReference { private final Runnable cleanupAction; - private final Cleaner cleaner; + private final SwiftCleaner swiftCleaner; - public PhantomCleanable(Object referent, Cleaner cleaner, Runnable cleanupAction) { - super(referent, cleaner.referenceQueue); + public PhantomCleanable(Object referent, SwiftCleaner swiftCleaner, Runnable cleanupAction) { + super(referent, swiftCleaner.referenceQueue); this.cleanupAction = cleanupAction; - this.cleaner = cleaner; - cleaner.list.add(this); + this.swiftCleaner = swiftCleaner; + swiftCleaner.list.add(this); } public void cleanup() { - if (cleaner.list.remove(this)) { + if (swiftCleaner.list.remove(this)) { cleanupAction.run(); } } diff --git a/SwiftKitCore/src/main/java/org/swift/swiftkit/core/ref/Cleaner.java b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/ref/SwiftCleaner.java similarity index 88% rename from SwiftKitCore/src/main/java/org/swift/swiftkit/core/ref/Cleaner.java rename to SwiftKitCore/src/main/java/org/swift/swiftkit/core/ref/SwiftCleaner.java index 56dd7dbd..2a5b49f5 100644 --- a/SwiftKitCore/src/main/java/org/swift/swiftkit/core/ref/Cleaner.java +++ b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/ref/SwiftCleaner.java @@ -21,19 +21,19 @@ import java.util.Objects; import java.util.concurrent.ThreadFactory; -public class Cleaner implements Runnable { +public class SwiftCleaner implements Runnable { final ReferenceQueue referenceQueue; final List list; - private Cleaner() { + private SwiftCleaner() { this.referenceQueue = new ReferenceQueue<>(); this.list = Collections.synchronizedList(new LinkedList<>()); } - public static Cleaner create(ThreadFactory threadFactory) { - Cleaner cleaner = new Cleaner(); - cleaner.start(threadFactory); - return cleaner; + public static SwiftCleaner create(ThreadFactory threadFactory) { + SwiftCleaner swiftCleaner = new SwiftCleaner(); + swiftCleaner.start(threadFactory); + return swiftCleaner; } void start(ThreadFactory threadFactory) { From 41b6dab844d5e0a4772c5dc01e19a93b018fca50 Mon Sep 17 00:00:00 2001 From: Mads Odgaard Date: Thu, 7 Aug 2025 18:41:52 +0200 Subject: [PATCH 7/7] add tests --- .../MemoryManagementModeTests.swift | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 Tests/JExtractSwiftTests/MemoryManagementModeTests.swift diff --git a/Tests/JExtractSwiftTests/MemoryManagementModeTests.swift b/Tests/JExtractSwiftTests/MemoryManagementModeTests.swift new file mode 100644 index 00000000..4edf59c2 --- /dev/null +++ b/Tests/JExtractSwiftTests/MemoryManagementModeTests.swift @@ -0,0 +1,77 @@ +//===----------------------------------------------------------------------===// +// +// 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 JavaKitConfigurationShared +import Testing + +@Suite +struct MemoryManagementModeTests { + let text = + """ + class MyClass {} + + public func f() -> MyClass + """ + + @Test + func explicit() throws { + var config = Configuration() + config.memoryManagementMode = .explicit + + try assertOutput( + input: text, + config: config, + .jni, .java, + expectedChunks: [ + """ + /** + * Downcall to Swift: + * {@snippet lang=swift : + * public func f() -> MyClass + * } + */ + public static MyClass f(SwiftArena swiftArena$) { + return new MyClass(SwiftModule.$f(), swiftArena$); + } + """, + ] + ) + } + + @Test + func allowGlobalAutomatic() throws { + var config = Configuration() + config.memoryManagementMode = .allowGlobalAutomatic + + try assertOutput( + input: text, + config: config, + .jni, .java, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + public static MyClass f() { + return f(SwiftMemoryManagement.GLOBAL_SWIFT_JAVA_ARENA); + } + """, + """ + public static MyClass f(SwiftArena swiftArena$) { + return new MyClass(SwiftModule.$f(), swiftArena$); + } + """, + ] + ) + } +}