Skip to content

Commit 912fe01

Browse files
committed
add auto arena to SwiftKit core that should work on Android
1 parent a3791cf commit 912fe01

File tree

11 files changed

+228
-35
lines changed

11 files changed

+228
-35
lines changed

Samples/JExtractJNISampleApp/src/main/java/com/example/swift/HelloJava2SwiftJNI.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@
1818

1919
// Import javakit/swiftkit support libraries
2020

21+
import org.swift.swiftkit.core.SwiftArena;
2122
import org.swift.swiftkit.core.SwiftLibraries;
22-
import org.swift.swiftkit.core.ConfinedSwiftMemorySession;
2323

2424
public class HelloJava2SwiftJNI {
2525

@@ -41,7 +41,7 @@ static void examples() {
4141

4242
MySwiftClass.method();
4343

44-
try (var arena = new ConfinedSwiftMemorySession()) {
44+
try (var arena = SwiftArena.ofConfined()) {
4545
MySwiftClass myClass = MySwiftClass.init(10, 5, arena);
4646
MySwiftClass myClass2 = MySwiftClass.init(arena);
4747

Samples/JExtractJNISampleApp/src/test/java/com/example/swift/MySwiftClassTest.java

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
package com.example.swift;
1616

1717
import org.junit.jupiter.api.Test;
18-
import org.swift.swiftkit.core.ConfinedSwiftMemorySession;
18+
import org.swift.swiftkit.core.SwiftArena;
1919

2020
import java.util.Optional;
2121
import java.util.OptionalInt;
@@ -26,39 +26,39 @@
2626
public class MySwiftClassTest {
2727
@Test
2828
void init_noParameters() {
29-
try (var arena = new ConfinedSwiftMemorySession()) {
29+
try (var arena = SwiftArena.ofConfined()) {
3030
MySwiftClass c = MySwiftClass.init(arena);
3131
assertNotNull(c);
3232
}
3333
}
3434

3535
@Test
3636
void init_withParameters() {
37-
try (var arena = new ConfinedSwiftMemorySession()) {
37+
try (var arena = SwiftArena.ofConfined()) {
3838
MySwiftClass c = MySwiftClass.init(1337, 42, arena);
3939
assertNotNull(c);
4040
}
4141
}
4242

4343
@Test
4444
void sum() {
45-
try (var arena = new ConfinedSwiftMemorySession()) {
45+
try (var arena = SwiftArena.ofConfined()) {
4646
MySwiftClass c = MySwiftClass.init(20, 10, arena);
4747
assertEquals(30, c.sum());
4848
}
4949
}
5050

5151
@Test
5252
void xMultiplied() {
53-
try (var arena = new ConfinedSwiftMemorySession()) {
53+
try (var arena = SwiftArena.ofConfined()) {
5454
MySwiftClass c = MySwiftClass.init(20, 10, arena);
5555
assertEquals(200, c.xMultiplied(10));
5656
}
5757
}
5858

5959
@Test
6060
void throwingFunction() {
61-
try (var arena = new ConfinedSwiftMemorySession()) {
61+
try (var arena = SwiftArena.ofConfined()) {
6262
MySwiftClass c = MySwiftClass.init(20, 10, arena);
6363
Exception exception = assertThrows(Exception.class, () -> c.throwingFunction());
6464

@@ -68,15 +68,15 @@ void throwingFunction() {
6868

6969
@Test
7070
void constant() {
71-
try (var arena = new ConfinedSwiftMemorySession()) {
71+
try (var arena = SwiftArena.ofConfined()) {
7272
MySwiftClass c = MySwiftClass.init(20, 10, arena);
7373
assertEquals(100, c.getConstant());
7474
}
7575
}
7676

7777
@Test
7878
void mutable() {
79-
try (var arena = new ConfinedSwiftMemorySession()) {
79+
try (var arena = SwiftArena.ofConfined()) {
8080
MySwiftClass c = MySwiftClass.init(20, 10, arena);
8181
assertEquals(0, c.getMutable());
8282
c.setMutable(42);
@@ -86,15 +86,15 @@ void mutable() {
8686

8787
@Test
8888
void product() {
89-
try (var arena = new ConfinedSwiftMemorySession()) {
89+
try (var arena = SwiftArena.ofConfined()) {
9090
MySwiftClass c = MySwiftClass.init(20, 10, arena);
9191
assertEquals(200, c.getProduct());
9292
}
9393
}
9494

9595
@Test
9696
void throwingVariable() {
97-
try (var arena = new ConfinedSwiftMemorySession()) {
97+
try (var arena = SwiftArena.ofConfined()) {
9898
MySwiftClass c = MySwiftClass.init(20, 10, arena);
9999

100100
Exception exception = assertThrows(Exception.class, () -> c.getThrowingVariable());
@@ -105,7 +105,7 @@ void throwingVariable() {
105105

106106
@Test
107107
void mutableDividedByTwo() {
108-
try (var arena = new ConfinedSwiftMemorySession()) {
108+
try (var arena = SwiftArena.ofConfined()) {
109109
MySwiftClass c = MySwiftClass.init(20, 10, arena);
110110
assertEquals(0, c.getMutableDividedByTwo());
111111
c.setMutable(20);
@@ -117,15 +117,15 @@ void mutableDividedByTwo() {
117117

118118
@Test
119119
void isWarm() {
120-
try (var arena = new ConfinedSwiftMemorySession()) {
120+
try (var arena = SwiftArena.ofConfined()) {
121121
MySwiftClass c = MySwiftClass.init(20, 10, arena);
122122
assertFalse(c.isWarm());
123123
}
124124
}
125125

126126
@Test
127127
void sumWithX() {
128-
try (var arena = new ConfinedSwiftMemorySession()) {
128+
try (var arena = SwiftArena.ofConfined()) {
129129
MySwiftClass c1 = MySwiftClass.init(20, 10, arena);
130130
MySwiftClass c2 = MySwiftClass.init(50, 10, arena);
131131
assertEquals(70, c1.sumX(c2));
@@ -134,7 +134,7 @@ void sumWithX() {
134134

135135
@Test
136136
void copy() {
137-
try (var arena = new ConfinedSwiftMemorySession()) {
137+
try (var arena = SwiftArena.ofConfined()) {
138138
MySwiftClass c1 = MySwiftClass.init(20, 10, arena);
139139
MySwiftClass c2 = c1.copy(arena);
140140

@@ -146,7 +146,7 @@ void copy() {
146146

147147
@Test
148148
void addXWithJavaLong() {
149-
try (var arena = new ConfinedSwiftMemorySession()) {
149+
try (var arena = SwiftArena.ofConfined()) {
150150
MySwiftClass c1 = MySwiftClass.init(20, 10, arena);
151151
Long javaLong = 50L;
152152
assertEquals(70, c1.addXWithJavaLong(javaLong));

Samples/JExtractJNISampleApp/src/test/java/com/example/swift/MySwiftStructTest.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,14 @@
1616

1717
import org.junit.jupiter.api.Test;
1818
import org.swift.swiftkit.core.ConfinedSwiftMemorySession;
19+
import org.swift.swiftkit.core.SwiftArena;
1920

2021
import static org.junit.jupiter.api.Assertions.*;
2122

2223
public class MySwiftStructTest {
2324
@Test
2425
void init() {
25-
try (var arena = new ConfinedSwiftMemorySession()) {
26+
try (var arena = SwiftArena.ofConfined()) {
2627
MySwiftStruct s = MySwiftStruct.init(1337, 42, arena);
2728
assertEquals(1337, s.getCapacity());
2829
assertEquals(42, s.getLen());
@@ -31,7 +32,7 @@ void init() {
3132

3233
@Test
3334
void getAndSetLen() {
34-
try (var arena = new ConfinedSwiftMemorySession()) {
35+
try (var arena = SwiftArena.ofConfined()) {
3536
MySwiftStruct s = MySwiftStruct.init(1337, 42, arena);
3637
s.setLen(100);
3738
assertEquals(100, s.getLen());
@@ -40,7 +41,7 @@ void getAndSetLen() {
4041

4142
@Test
4243
void increaseCap() {
43-
try (var arena = new ConfinedSwiftMemorySession()) {
44+
try (var arena = SwiftArena.ofConfined()) {
4445
MySwiftStruct s = MySwiftStruct.init(1337, 42, arena);
4546
long newCap = s.increaseCap(10);
4647
assertEquals(1347, newCap);

Samples/JExtractJNISampleApp/src/test/java/com/example/swift/OptionalsTest.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
package com.example.swift;
1616

1717
import org.junit.jupiter.api.Test;
18-
import org.swift.swiftkit.core.ConfinedSwiftMemorySession;
18+
import org.swift.swiftkit.core.SwiftArena;
1919

2020
import java.util.Optional;
2121
import java.util.OptionalDouble;
@@ -82,7 +82,7 @@ void optionalString() {
8282

8383
@Test
8484
void optionalClass() {
85-
try (var arena = new ConfinedSwiftMemorySession()) {
85+
try (var arena = SwiftArena.ofConfined()) {
8686
MySwiftClass c = MySwiftClass.init(arena);
8787
assertEquals(Optional.empty(), MySwiftLibrary.optionalClass(Optional.empty(), arena));
8888
Optional<MySwiftClass> optionalClass = MySwiftLibrary.optionalClass(Optional.of(c), arena);
@@ -99,7 +99,7 @@ void optionalJavaKitLong() {
9999

100100
@Test
101101
void multipleOptionals() {
102-
try (var arena = new ConfinedSwiftMemorySession()) {
102+
try (var arena = SwiftArena.ofConfined()) {
103103
MySwiftClass c = MySwiftClass.init(arena);
104104
OptionalLong result = MySwiftLibrary.multipleOptionals(
105105
Optional.of((byte) 1),
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
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.core;
16+
17+
18+
import org.swift.swiftkit.core.ref.Cleaner;
19+
20+
import java.util.Objects;
21+
import java.util.concurrent.ThreadFactory;
22+
23+
/**
24+
* A memory session which manages registered objects via the Garbage Collector.
25+
*
26+
* <p> When registered Java wrapper classes around native Swift instances {@link SwiftInstance},
27+
* are eligible for collection, this will trigger the cleanup of the native resources as well.
28+
*
29+
* <p> This memory session is LESS reliable than using a {@link ConfinedSwiftMemorySession} because
30+
* the timing of when the native resources are cleaned up is somewhat undefined, and rely on the
31+
* system GC. Meaning, that if an object nas been promoted to an old generation, there may be a
32+
* long time between the resource no longer being referenced "in Java" and its native memory being released,
33+
* and also the deinit of the Swift type being run.
34+
*
35+
* <p> This can be problematic for Swift applications which rely on quick release of resources, and may expect
36+
* the deinits to run in expected and "quick" succession.
37+
*
38+
* <p> Whenever possible, prefer using an explicitly managed {@link SwiftArena}, such as {@link SwiftArena#ofConfined()}.
39+
*/
40+
final class AutoSwiftMemorySession implements SwiftArena {
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(SwiftInstance instance) {
49+
Objects.requireNonNull(instance, "value");
50+
51+
// We make sure we don't capture `instance` in the
52+
// cleanup action, so we can ignore the warning below.
53+
var cleanupAction = instance.$createCleanup();
54+
cleaner.register(instance, cleanupAction);
55+
}
56+
}

SwiftKitCore/src/main/java/org/swift/swiftkit/core/ConfinedSwiftMemorySession.java

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,17 +28,13 @@ public class ConfinedSwiftMemorySession implements ClosableSwiftArena {
2828

2929
final ConfinedResourceList resources;
3030

31-
public ConfinedSwiftMemorySession() {
32-
this(Thread.currentThread());
33-
}
34-
3531
public ConfinedSwiftMemorySession(Thread owner) {
3632
this.owner = owner;
3733
this.state = new AtomicInteger(ACTIVE);
3834
this.resources = new ConfinedResourceList();
3935
}
4036

41-
public void checkValid() throws RuntimeException {
37+
void checkValid() throws RuntimeException {
4238
if (this.owner != null && this.owner != Thread.currentThread()) {
4339
throw new WrongThreadException(String.format("ConfinedSwift arena is confined to %s but was closed from %s!", this.owner, Thread.currentThread()));
4440
} else if (this.state.get() < ACTIVE) {

SwiftKitCore/src/main/java/org/swift/swiftkit/core/JNISwiftInstance.java

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -56,13 +56,8 @@ protected JNISwiftInstance(long selfPointer, SwiftArena arena) {
5656

5757
@Override
5858
public SwiftInstanceCleanup $createCleanup() {
59-
final AtomicBoolean statusDestroyedFlag = $statusDestroyedFlag();
60-
Runnable markAsDestroyed = new Runnable() {
61-
@Override
62-
public void run() {
63-
statusDestroyedFlag.set(true);
64-
}
65-
};
59+
var statusDestroyedFlag = $statusDestroyedFlag();
60+
Runnable markAsDestroyed = () -> statusDestroyedFlag.set(true);
6661

6762
return new JNISwiftInstanceCleanup(this.$createDestroyFunction(), markAsDestroyed);
6863
}

SwiftKitCore/src/main/java/org/swift/swiftkit/core/SwiftArena.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414

1515
package org.swift.swiftkit.core;
1616

17+
import java.util.concurrent.ThreadFactory;
18+
1719
/**
1820
* A Swift arena manages Swift allocated memory for classes, structs, enums etc.
1921
* 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 {
2729
* Its memory should be considered managed by this arena, and be destroyed when the arena is closed.
2830
*/
2931
void register(SwiftInstance instance);
32+
33+
static ClosableSwiftArena ofConfined() {
34+
return new ConfinedSwiftMemorySession(Thread.currentThread());
35+
}
36+
37+
static SwiftArena ofAuto() {
38+
ThreadFactory cleanerThreadFactory = r -> new Thread(r, "AutoSwiftArenaCleanerThread");
39+
return new AutoSwiftMemorySession(cleanerThreadFactory);
40+
}
3041
}
3142

3243
/**
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package org.swift.swiftkit.core.ref;
2+
3+
import java.lang.ref.ReferenceQueue;
4+
import java.util.LinkedList;
5+
import java.util.Objects;
6+
import java.util.concurrent.ThreadFactory;
7+
8+
public class Cleaner implements Runnable {
9+
final ReferenceQueue<Object> referenceQueue;
10+
final LinkedList<PhantomCleanable> list;
11+
12+
private Cleaner() {
13+
this.referenceQueue = new ReferenceQueue<>();
14+
this.list = new LinkedList<>();
15+
}
16+
17+
public static Cleaner create(ThreadFactory threadFactory) {
18+
Cleaner cleaner = new Cleaner();
19+
cleaner.start(threadFactory);
20+
return cleaner;
21+
}
22+
23+
void start(ThreadFactory threadFactory) {
24+
// This makes sure the linked list is not empty when the thread starts,
25+
// and the thread will run at least until the cleaner itself can be GCed.
26+
new PhantomCleanable(this, this, () -> {});
27+
28+
Thread thread = threadFactory.newThread(this);
29+
thread.setDaemon(true);
30+
thread.start();
31+
}
32+
33+
public void register(Object resourceHolder, Runnable cleaningAction) {
34+
Objects.requireNonNull(resourceHolder, "resourceHolder");
35+
Objects.requireNonNull(cleaningAction, "cleaningAction");
36+
new PhantomCleanable(resourceHolder, this, cleaningAction);
37+
}
38+
39+
@Override
40+
public void run() {
41+
while (!list.isEmpty()) {
42+
try {
43+
PhantomCleanable removed = (PhantomCleanable) referenceQueue.remove(60 * 1000L);
44+
removed.cleanup();
45+
} catch (Throwable e) {
46+
// ignore exceptions from the cleanup action
47+
// (including interruption of cleanup thread)
48+
}
49+
}
50+
}
51+
}

0 commit comments

Comments
 (0)