Skip to content

Commit 98b7184

Browse files
committed
Lazily attach threads to the JVM when querying the JNI environment
Instead of having the JavaVirtualMachine instance keep track of the JNI environment for the thread in which it was initialized, provide a JNI environment query function that attaches the current thread to the JVM if needed. This lets us query the JNI environment for multiple threads, should we want to. We no longer need to force all of the JavaKit runtime tests to the main actor.
1 parent b69f4d7 commit 98b7184

File tree

3 files changed

+63
-13
lines changed

3 files changed

+63
-13
lines changed

Sources/Java2Swift/JavaToSwift.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ struct JavaToSwift: ParsableCommand {
7171
let jvm = try JavaVirtualMachine(vmOptions: vmOptions)
7272
javaVirtualMachine = jvm
7373

74-
try run(environment: jvm.environment)
74+
try run(environment: jvm.environment())
7575
}
7676

7777
mutating func run(environment: JNIEnvironment) throws {

Sources/JavaKitVM/JavaVirtualMachine.swift

Lines changed: 49 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,12 @@ import JavaKit
1717
typealias JavaVMPointer = UnsafeMutablePointer<JavaVM?>
1818

1919
public final class JavaVirtualMachine: @unchecked Sendable {
20+
/// The JNI version that we depend on.
21+
static let jniVersion = JNI_VERSION_1_6
22+
2023
/// The Java virtual machine instance.
2124
private let jvm: JavaVMPointer
2225

23-
/// The JNI environment for the JVM.
24-
public let environment: JNIEnvironment
25-
2626
/// Initialize a new Java virtual machine instance.
2727
///
2828
/// - Parameters:
@@ -41,7 +41,7 @@ public final class JavaVirtualMachine: @unchecked Sendable {
4141
var jvm: JavaVMPointer? = nil
4242
var environment: UnsafeMutableRawPointer? = nil
4343
var vmArgs = JavaVMInitArgs()
44-
vmArgs.version = JNI_VERSION_1_6
44+
vmArgs.version = JavaVirtualMachine.jniVersion
4545
vmArgs.ignoreUnrecognized = jboolean(ignoreUnrecognized ? JNI_TRUE : JNI_FALSE)
4646

4747
// Construct the complete list of VM options.
@@ -80,7 +80,6 @@ public final class JavaVirtualMachine: @unchecked Sendable {
8080
}
8181

8282
self.jvm = jvm!
83-
self.environment = environment!.assumingMemoryBound(to: JNIEnv?.self)
8483
}
8584

8685
deinit {
@@ -89,10 +88,55 @@ public final class JavaVirtualMachine: @unchecked Sendable {
8988
fatalError("Failed to destroy the JVM.")
9089
}
9190
}
91+
92+
/// Produce the JNI environment for the active thread, attaching this
93+
/// thread to the JVM if it isn't already.
94+
///
95+
/// - Parameter
96+
/// - asDaemon: Whether this thread should be treated as a daemon
97+
/// thread in the Java Virtual Machine.
98+
public func environment(asDaemon: Bool = false) throws -> JNIEnvironment {
99+
// Check whether this thread is already attached. If so, return the
100+
// corresponding environment.
101+
var environment: UnsafeMutableRawPointer? = nil
102+
let getEnvResult = jvm.pointee!.pointee.GetEnv(
103+
jvm,
104+
&environment,
105+
JavaVirtualMachine.jniVersion
106+
)
107+
if getEnvResult == JNI_OK, let environment {
108+
return environment.assumingMemoryBound(to: JNIEnv?.self)
109+
}
110+
111+
// Attach the current thread to the JVM.
112+
let attachResult: jint
113+
if asDaemon {
114+
attachResult = jvm.pointee!.pointee.AttachCurrentThreadAsDaemon(jvm, &environment, nil)
115+
} else {
116+
attachResult = jvm.pointee!.pointee.AttachCurrentThread(jvm, &environment, nil)
117+
}
118+
119+
if attachResult == JNI_OK, let environment {
120+
return environment.assumingMemoryBound(to: JNIEnv?.self)
121+
}
122+
123+
throw VMError.failedToAttachThread
124+
}
125+
126+
/// Detach the current thread from the Java Virtual Machine. All Java
127+
/// threads waiting for this thread to die are notified.
128+
public func detachCurrentThread() throws {
129+
let result = jvm.pointee!.pointee.DetachCurrentThread(jvm)
130+
if result != JNI_OK {
131+
throw VMError.failedToDetachThread
132+
}
133+
}
92134
}
93135

94136
extension JavaVirtualMachine {
95137
enum VMError: Error {
96138
case failedToCreateVM
139+
case failedToAttachThread
140+
case failedToDetachThread
97141
}
98142
}

Tests/JavaKitTests/BasicRuntimeTests.swift

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,16 @@ import XCTest // NOTE: Workaround for https://github.com/swiftlang/swift-java/is
2020
@MainActor
2121
let jvm = try! JavaVirtualMachine(vmOptions: [])
2222

23-
@MainActor
2423
class BasicRuntimeTests: XCTestCase {
2524
func testJavaObjectManagement() async throws {
2625
if isLinux {
2726
throw XCTSkip("Attempts to refcount a null pointer on Linux")
2827
}
2928

29+
let environment = try await jvm.environment()
3030
let sneakyJavaThis: jobject
3131
do {
32-
let object = JavaObject(environment: jvm.environment)
32+
let object = JavaObject(environment: environment)
3333
XCTAssert(object.toString().starts(with: "java.lang.Object"))
3434

3535
// Make sure this object was promoted to a global reference.
@@ -41,10 +41,10 @@ class BasicRuntimeTests: XCTestCase {
4141

4242
// The reference should now be invalid, because we've deleted the
4343
// global reference.
44-
XCTAssertEqual(jvm.environment.pointee?.pointee.GetObjectRefType(jvm.environment, sneakyJavaThis), JNIInvalidRefType)
44+
XCTAssertEqual(environment.pointee?.pointee.GetObjectRefType(environment, sneakyJavaThis), JNIInvalidRefType)
4545

4646
// 'super' and 'as' don't require allocating a new holder.
47-
let url = try URL("http://swift.org", environment: jvm.environment)
47+
let url = try URL("http://swift.org", environment: environment)
4848
let superURL = url.super
4949
XCTAssert(url.javaHolder === superURL.javaHolder)
5050
let urlAgain = superURL.as(URL.self)!
@@ -56,8 +56,10 @@ class BasicRuntimeTests: XCTestCase {
5656
throw XCTSkip("Attempts to refcount a null pointer on Linux")
5757
}
5858

59+
let environment = try await jvm.environment()
60+
5961
do {
60-
_ = try URL("bad url", environment: jvm.environment)
62+
_ = try URL("bad url", environment: environment)
6163
} catch {
6264
XCTAssert(String(describing: error) == "no protocol: bad url")
6365
}
@@ -68,13 +70,17 @@ class BasicRuntimeTests: XCTestCase {
6870
throw XCTSkip("Attempts to refcount a null pointer on Linux")
6971
}
7072

71-
let urlConnectionClass = try JavaClass<URLConnection>(in: jvm.environment)
73+
let environment = try await jvm.environment()
74+
75+
let urlConnectionClass = try JavaClass<URLConnection>(in: environment)
7276
XCTAssert(urlConnectionClass.getDefaultAllowUserInteraction() == false)
7377
}
7478

7579
func testClassInstanceLookup() async throws {
80+
let environment = try await jvm.environment()
81+
7682
do {
77-
_ = try JavaClass<Nonexistent>(in: jvm.environment)
83+
_ = try JavaClass<Nonexistent>(in: environment)
7884
} catch {
7985
XCTAssertEqual(String(describing: error), "org/swift/javakit/Nonexistent")
8086
}

0 commit comments

Comments
 (0)