Skip to content

Commit dfce805

Browse files
committed
SwiftArena.ofAuto() which uses GC to manage swift instances (and destroy them)
1 parent e16ef46 commit dfce805

File tree

6 files changed

+281
-94
lines changed

6 files changed

+281
-94
lines changed
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package org.swift.swiftkit;
2+
3+
import java.lang.foreign.MemorySegment;
4+
import java.lang.ref.Cleaner;
5+
import java.util.Objects;
6+
import java.util.concurrent.ThreadFactory;
7+
8+
/**
9+
* A memory session which manages registered objects via the Garbage Collector.
10+
*
11+
* <p> When registered Java wrapper classes around native Swift instances {@link SwiftInstance},
12+
* are eligible for collection, this will trigger the cleanup of the native resources as well.
13+
*
14+
* <p> This memory session is LESS reliable than using a {@link ConfinedSwiftMemorySession} because
15+
* the timing of when the native resources are cleaned up is somewhat undefined, and rely on the
16+
* system GC. Meaning, that if an object nas been promoted to an old generation, there may be a
17+
* long time between the resource no longer being referenced "in Java" and its native memory being released,
18+
* and also the deinit of the Swift type being run.
19+
*
20+
* <p> This can be problematic for Swift applications which rely on quick release of resources, and may expect
21+
* the deinits to run in expected and "quick" succession.
22+
*
23+
* <p> Whenever possible, prefer using an explicitly managed {@link SwiftArena}, such as {@link SwiftArena#ofConfined()}.
24+
*/
25+
final class AutoSwiftMemorySession implements SwiftArena {
26+
27+
private final Cleaner cleaner;
28+
29+
public AutoSwiftMemorySession(ThreadFactory cleanerThreadFactory) {
30+
this.cleaner = Cleaner.create(cleanerThreadFactory);
31+
}
32+
33+
@Override
34+
public void register(SwiftHeapObject object) {
35+
SwiftHeapObjectCleanup cleanupAction = new SwiftHeapObjectCleanup(object.$memorySegment(), object.$swiftType());
36+
register(object, cleanupAction);
37+
}
38+
39+
// visible for testing
40+
void register(SwiftHeapObject object, SwiftHeapObjectCleanup cleanupAction) {
41+
Objects.requireNonNull(object, "obj");
42+
Objects.requireNonNull(cleanupAction, "cleanupAction");
43+
44+
45+
cleaner.register(object, cleanupAction);
46+
}
47+
48+
@Override
49+
public void register(SwiftValue value) {
50+
Objects.requireNonNull(value, "value");
51+
MemorySegment resource = value.$memorySegment();
52+
var cleanupAction = new SwiftValueCleanup(resource);
53+
cleaner.register(value, cleanupAction);
54+
}
55+
56+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
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 org.swift.swiftkit;
16+
17+
/**
18+
* Auto-closable version of {@link SwiftArena}.
19+
*/
20+
public interface ClosableSwiftArena extends SwiftArena, AutoCloseable {
21+
22+
/**
23+
* Close the arena and make sure all objects it managed are released.
24+
* Throws if unable to verify all resources have been release (e.g. over retained Swift classes)
25+
*/
26+
void close();
27+
28+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package org.swift.swiftkit;
2+
3+
import java.util.LinkedList;
4+
import java.util.List;
5+
import java.util.concurrent.atomic.AtomicInteger;
6+
7+
final class ConfinedSwiftMemorySession implements ClosableSwiftArena {
8+
9+
final static int CLOSED = 0;
10+
final static int ACTIVE = 1;
11+
12+
final Thread owner;
13+
final AtomicInteger state;
14+
15+
final ConfinedResourceList resources;
16+
17+
public ConfinedSwiftMemorySession(Thread owner) {
18+
this.owner = owner;
19+
this.state = new AtomicInteger(ACTIVE);
20+
this.resources = new ConfinedResourceList();
21+
}
22+
23+
public void checkValid() throws RuntimeException {
24+
if (this.owner != null && this.owner != Thread.currentThread()) {
25+
throw new WrongThreadException("ConfinedSwift arena is confined to %s but was closed from %s!".formatted(this.owner, Thread.currentThread()));
26+
} else if (this.state.get() < ACTIVE) {
27+
throw new RuntimeException("SwiftArena is already closed!");
28+
}
29+
}
30+
31+
@Override
32+
public void close() {
33+
checkValid();
34+
35+
// Cleanup all resources
36+
if (this.state.compareAndExchange(ACTIVE, CLOSED) == ACTIVE) {
37+
this.resources.runCleanup();
38+
} // else, was already closed; do nothing
39+
}
40+
41+
@Override
42+
public void register(SwiftHeapObject object) {
43+
checkValid();
44+
45+
var cleanup = new SwiftHeapObjectCleanup(object.$memorySegment(), object.$swiftType());
46+
this.resources.add(cleanup);
47+
}
48+
49+
@Override
50+
public void register(SwiftValue value) {
51+
checkValid();
52+
53+
var cleanup = new SwiftValueCleanup(value.$memorySegment());
54+
this.resources.add(cleanup);
55+
}
56+
57+
static final class ConfinedResourceList implements SwiftResourceList {
58+
// TODO: Could use intrusive linked list to avoid one indirection here
59+
final List<SwiftInstanceCleanup> resourceCleanups = new LinkedList<>();
60+
61+
void add(SwiftInstanceCleanup cleanup) {
62+
resourceCleanups.add(cleanup);
63+
}
64+
65+
@Override
66+
public void runCleanup() {
67+
for (SwiftInstanceCleanup cleanup : resourceCleanups) {
68+
cleanup.run();
69+
}
70+
}
71+
}
72+
}

SwiftKit/src/main/java/org/swift/swiftkit/SwiftArena.java

Lines changed: 18 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -14,23 +14,26 @@
1414

1515
package org.swift.swiftkit;
1616

17-
import java.util.LinkedList;
18-
import java.util.List;
19-
import java.util.concurrent.atomic.AtomicInteger;
17+
import java.util.concurrent.ThreadFactory;
2018

2119
/**
2220
* A Swift arena manages Swift allocated memory for classes, structs, enums etc.
2321
* When an arena is closed, it will destroy all managed swift objects in a way appropriate to their type.
24-
* <p>
25-
* A confined arena has an associated owner thread that confines some operations to
26-
* associated owner thread such as {@link #close()}.
22+
*
23+
* <p> A confined arena has an associated owner thread that confines some operations to
24+
* associated owner thread such as {@link ClosableSwiftArena#close()}.
2725
*/
28-
public interface SwiftArena extends AutoCloseable {
26+
public interface SwiftArena {
2927

30-
static SwiftArena ofConfined() {
28+
static ClosableSwiftArena ofConfined() {
3129
return new ConfinedSwiftMemorySession(Thread.currentThread());
3230
}
3331

32+
static SwiftArena ofAuto() {
33+
ThreadFactory cleanerThreadFactory = r -> new Thread(r, "AutoSwiftArenaCleanerThread");
34+
return new AutoSwiftMemorySession(cleanerThreadFactory);
35+
}
36+
3437
/**
3538
* Register a Swift reference counted heap object with this arena (such as a {@code class} or {@code actor}).
3639
* Its memory should be considered managed by this arena, and be destroyed when the arena is closed.
@@ -43,95 +46,23 @@ static SwiftArena ofConfined() {
4346
*/
4447
void register(SwiftValue value);
4548

46-
/**
47-
* Close the arena and make sure all objects it managed are released.
48-
* Throws if unable to verify all resources have been release (e.g. over retained Swift classes)
49-
*/
50-
void close();
51-
5249
}
5350

54-
final class ConfinedSwiftMemorySession implements SwiftArena {
55-
56-
// final Arena underlying;
57-
final Thread owner;
58-
final SwiftResourceList resources;
59-
60-
final int CLOSED = 0;
61-
final int ACTIVE = 1;
62-
final AtomicInteger state;
63-
64-
public ConfinedSwiftMemorySession(Thread owner) {
65-
this.owner = owner;
66-
resources = new ConfinedResourceList();
67-
state = new AtomicInteger(ACTIVE);
68-
}
69-
70-
public void checkValid() throws RuntimeException {
71-
if (this.owner != null && this.owner != Thread.currentThread()) {
72-
throw new WrongThreadException("ConfinedSwift arena is confined to %s but was closed from %s!".formatted(this.owner, Thread.currentThread()));
73-
} else if (this.state.get() < ACTIVE) {
74-
throw new RuntimeException("Arena is already closed!");
75-
}
76-
}
77-
78-
@Override
79-
public void register(SwiftHeapObject object) {
80-
this.resources.add(new SwiftHeapObjectCleanup(object));
81-
}
82-
83-
@Override
84-
public void register(SwiftValue value) {
85-
this.resources.add(new SwiftValueCleanup(value.$memorySegment()));
86-
}
87-
88-
@Override
89-
public void close() {
90-
checkValid();
91-
92-
// Cleanup all resources
93-
if (this.state.compareAndExchange(ACTIVE, CLOSED) == ACTIVE) {
94-
this.resources.cleanup();
95-
} // else, was already closed; do nothing
96-
}
97-
98-
/**
99-
* Represents a list of resources that need a cleanup, e.g. allocated classes/structs.
100-
*/
101-
static abstract class SwiftResourceList implements Runnable {
102-
// TODO: Could use intrusive linked list to avoid one indirection here
103-
final List<SwiftInstanceCleanup> resourceCleanups = new LinkedList<>();
104-
105-
abstract void add(SwiftInstanceCleanup cleanup);
106-
107-
public abstract void cleanup();
108-
109-
public final void run() {
110-
cleanup(); // cleaner interop
111-
}
112-
}
113-
114-
static final class ConfinedResourceList extends SwiftResourceList {
115-
@Override
116-
void add(SwiftInstanceCleanup cleanup) {
117-
resourceCleanups.add(cleanup);
118-
}
119-
120-
@Override
121-
public void cleanup() {
122-
for (SwiftInstanceCleanup cleanup : resourceCleanups) {
123-
cleanup.run();
124-
}
125-
}
126-
}
51+
/**
52+
* Represents a list of resources that need a cleanup, e.g. allocated classes/structs.
53+
*/
54+
interface SwiftResourceList {
12755

56+
void runCleanup();
12857

12958
}
13059

60+
13161
final class UnexpectedRetainCountException extends RuntimeException {
13262
public UnexpectedRetainCountException(Object resource, long retainCount, int expectedRetainCount) {
13363
super(("Attempting to cleanup managed memory segment %s, but it's retain count was different than [%d] (was %d)! " +
13464
"This would result in destroying a swift object that is still retained by other code somewhere."
13565
).formatted(resource, expectedRetainCount, retainCount));
13666
}
13767
}
68+

SwiftKit/src/main/java/org/swift/swiftkit/SwiftInstanceCleanup.java

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,23 +19,43 @@
1919
/**
2020
* A Swift memory instance cleanup, e.g. count-down a reference count and destroy a class, or destroy struct/enum etc.
2121
*/
22-
sealed interface SwiftInstanceCleanup extends Runnable {
22+
interface SwiftInstanceCleanup extends Runnable {
2323
}
2424

25-
record SwiftHeapObjectCleanup(SwiftHeapObject instance) implements SwiftInstanceCleanup {
25+
/**
26+
* Implements cleaning up a Swift {@link SwiftHeapObject}.
27+
* <p>
28+
* This class does not store references to the Java wrapper class, and therefore the wrapper may be subject to GC,
29+
* which may trigger a cleanup (using this class), which will clean up its underlying native memory resource.
30+
*/
31+
// non-final for testing
32+
class SwiftHeapObjectCleanup implements SwiftInstanceCleanup {
33+
34+
final MemorySegment selfPointer;
35+
final SwiftAnyType selfType;
36+
37+
/**
38+
* This constructor on purpose does not just take a {@link SwiftHeapObject} in order to make it very
39+
* clear that it does not take ownership of it, but we ONLY manage the native resource here.
40+
*
41+
* This is important for {@link AutoSwiftMemorySession} which relies on the wrapper type to be GC-able,
42+
* when no longer "in use" on the Java side.
43+
*/
44+
SwiftHeapObjectCleanup(MemorySegment selfPointer, SwiftAnyType selfType) {
45+
this.selfPointer = selfPointer;
46+
this.selfType = selfType;
47+
}
2648

2749
@Override
2850
public void run() throws UnexpectedRetainCountException {
2951
// Verify we're only destroying an object that's indeed not retained by anyone else:
30-
long retainedCount = SwiftKit.retainCount(this.instance);
52+
long retainedCount = SwiftKit.retainCount(selfPointer);
3153
if (retainedCount > 1) {
32-
throw new UnexpectedRetainCountException(this.instance, retainedCount, 1);
54+
throw new UnexpectedRetainCountException(selfPointer, retainedCount, 1);
3355
}
3456

3557
// Destroy (and deinit) the object:
36-
var ty = this.instance.$swiftType();
37-
38-
SwiftValueWitnessTable.destroy(ty, this.instance.$memorySegment());
58+
SwiftValueWitnessTable.destroy(selfType, selfPointer);
3959

4060
// Invalidate the Java wrapper class, in order to prevent effectively use-after-free issues.
4161
// FIXME: some trouble with setting the pointer to null, need to figure out an appropriate way to do this

0 commit comments

Comments
 (0)