Skip to content

Commit c9b1630

Browse files
committed
SwiftArena.ofAuto() which uses GC to manage swift instances (and destroy them) (swiftlang#133)
1 parent 485c775 commit c9b1630

File tree

6 files changed

+309
-94
lines changed

6 files changed

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

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)