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 21cad317a..3109f64e9 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 e7de03add..2e9a7e623 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 9eeaf0297..c2c1170b7 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 f7262ad4d..d60ff6d5b 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/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift index 0daf14de6..0920a89b0 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift @@ -247,29 +247,47 @@ 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 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);") + } else { + printer.print("return \(call);") + } + } + printer.println() + } + + 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 0ff089da0..2d9b43118 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 ?? .default + } + // ==== 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 1feac411b..22fdd5f5a 100644 --- a/Sources/JavaKitConfigurationShared/GenerationMode.swift +++ b/Sources/JavaKitConfigurationShared/GenerationMode.swift @@ -76,3 +76,25 @@ 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 explicit + + /// Provide both explicit `SwiftArena` support + /// and a default global automatic `SwiftArena` that will deallocate memory when the GC decides to. + case allowGlobalAutomatic + + public static var `default`: Self { + .explicit + } + + public var requiresGlobalArena: Bool { + switch self { + case .explicit: false + case .allowGlobalAutomatic: true + } + } +} diff --git a/Sources/SwiftJavaTool/Commands/JExtractCommand.swift b/Sources/SwiftJavaTool/Commands/JExtractCommand.swift index 54a997083..5eb9486b7 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 = .default + @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 == .explicit 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 {} 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 000000000..36e732097 --- /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) 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; + + +import org.swift.swiftkit.core.ref.SwiftCleaner; + +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 SwiftCleaner swiftCleaner; + + public AutoSwiftMemorySession(ThreadFactory cleanerThreadFactory) { + this.swiftCleaner = SwiftCleaner.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(); + swiftCleaner.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 7c6e80fb9..4383a6fe2 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 6b30ed2fc..95f1e5a0f 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 ed16d2501..3b6c46269 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. @@ -21,12 +23,21 @@ *

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. */ 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/SwiftMemoryManagement.java b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/SwiftMemoryManagement.java new file mode 100644 index 000000000..2b9a12092 --- /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 new file mode 100644 index 000000000..2efcdae7d --- /dev/null +++ b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/ref/PhantomCleanable.java @@ -0,0 +1,35 @@ +//===----------------------------------------------------------------------===// +// +// 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; + +public class PhantomCleanable extends PhantomReference { + private final Runnable cleanupAction; + private final SwiftCleaner swiftCleaner; + + public PhantomCleanable(Object referent, SwiftCleaner swiftCleaner, Runnable cleanupAction) { + super(referent, swiftCleaner.referenceQueue); + this.cleanupAction = cleanupAction; + this.swiftCleaner = swiftCleaner; + swiftCleaner.list.add(this); + } + + public void cleanup() { + if (swiftCleaner.list.remove(this)) { + cleanupAction.run(); + } + } +} diff --git a/SwiftKitCore/src/main/java/org/swift/swiftkit/core/ref/SwiftCleaner.java b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/ref/SwiftCleaner.java new file mode 100644 index 000000000..2a5b49f5e --- /dev/null +++ b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/ref/SwiftCleaner.java @@ -0,0 +1,67 @@ +//===----------------------------------------------------------------------===// +// +// 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; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.ThreadFactory; + +public class SwiftCleaner implements Runnable { + final ReferenceQueue referenceQueue; + final List list; + + private SwiftCleaner() { + this.referenceQueue = new ReferenceQueue<>(); + this.list = Collections.synchronizedList(new LinkedList<>()); + } + + public static SwiftCleaner create(ThreadFactory threadFactory) { + SwiftCleaner swiftCleaner = new SwiftCleaner(); + swiftCleaner.start(threadFactory); + return swiftCleaner; + } + + 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/test/java/org/swift/swiftkit/AutoArenaTest.java b/SwiftKitCore/src/test/java/org/swift/swiftkit/AutoArenaTest.java new file mode 100644 index 000000000..b414962f5 --- /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) 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; + +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 () -> {}; + } + } +} diff --git a/Tests/JExtractSwiftTests/MemoryManagementModeTests.swift b/Tests/JExtractSwiftTests/MemoryManagementModeTests.swift new file mode 100644 index 000000000..4edf59c27 --- /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$); + } + """, + ] + ) + } +}