Skip to content

Commit fab398b

Browse files
authored
Merge pull request #69 from DougGregor/shared-jvm
Support concurrent use of a shared `JavaVirtualMachine`
2 parents b69f4d7 + 3ddd2b7 commit fab398b

File tree

4 files changed

+214
-32
lines changed

4 files changed

+214
-32
lines changed

Sources/Java2Swift/JavaToSwift.swift

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,6 @@ import JavaKitVM
2222
import SwiftSyntax
2323
import SwiftSyntaxBuilder
2424

25-
/// Global instance of the Java virtual machine that we keep alive forever.
26-
var javaVirtualMachine: JavaVirtualMachine? = nil
27-
2825
/// Command-line utility to drive the export of Java classes into Swift types.
2926
@main
3027
struct JavaToSwift: ParsableCommand {
@@ -68,10 +65,8 @@ struct JavaToSwift: ParsableCommand {
6865
vmOptions.append(contentsOf: classpath)
6966
}
7067

71-
let jvm = try JavaVirtualMachine(vmOptions: vmOptions)
72-
javaVirtualMachine = jvm
73-
74-
try run(environment: jvm.environment)
68+
let jvm = try JavaVirtualMachine.shared(vmOptions: vmOptions)
69+
try run(environment: jvm.environment())
7570
}
7671

7772
mutating func run(environment: JNIEnvironment) throws {

Sources/JavaKitVM/JavaVirtualMachine.swift

Lines changed: 187 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,20 @@ 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
26+
/// Whether to destroy the JVM on deinit.
27+
private let destroyOnDeinit: Bool
28+
29+
/// Adopt an existing JVM pointer.
30+
private init(adoptingJVM jvm: JavaVMPointer) {
31+
self.jvm = jvm
32+
self.destroyOnDeinit = false
33+
}
2534

2635
/// Initialize a new Java virtual machine instance.
2736
///
@@ -33,15 +42,15 @@ public final class JavaVirtualMachine: @unchecked Sendable {
3342
/// be prefixed by the class-path argument described above.
3443
/// - ignoreUnrecognized: Whether the JVM should ignore any VM options it
3544
/// does not recognize.
36-
public init(
45+
private init(
3746
classPath: [String] = [],
3847
vmOptions: [String] = [],
3948
ignoreUnrecognized: Bool = true
4049
) throws {
4150
var jvm: JavaVMPointer? = nil
4251
var environment: UnsafeMutableRawPointer? = nil
4352
var vmArgs = JavaVMInitArgs()
44-
vmArgs.version = JNI_VERSION_1_6
53+
vmArgs.version = JavaVirtualMachine.jniVersion
4554
vmArgs.ignoreUnrecognized = jboolean(ignoreUnrecognized ? JNI_TRUE : JNI_FALSE)
4655

4756
// Construct the complete list of VM options.
@@ -74,25 +83,191 @@ public final class JavaVirtualMachine: @unchecked Sendable {
7483
vmArgs.options = optionsBuffer.baseAddress
7584
vmArgs.nOptions = jint(optionsBuffer.count)
7685

77-
// Create the JVM.
78-
if JNI_CreateJavaVM(&jvm, &environment, &vmArgs) != JNI_OK {
79-
throw VMError.failedToCreateVM
86+
// Create the JVM instance.
87+
if let createError = VMError(fromJNIError: JNI_CreateJavaVM(&jvm, &environment, &vmArgs)) {
88+
throw createError
8089
}
8190

8291
self.jvm = jvm!
83-
self.environment = environment!.assumingMemoryBound(to: JNIEnv?.self)
92+
self.destroyOnDeinit = true
8493
}
8594

8695
deinit {
87-
// Destroy the JVM.
88-
if jvm.pointee!.pointee.DestroyJavaVM(jvm) != JNI_OK {
89-
fatalError("Failed to destroy the JVM.")
96+
if destroyOnDeinit {
97+
// Destroy the JVM.
98+
if let resultError = VMError(fromJNIError: jvm.pointee!.pointee.DestroyJavaVM(jvm)) {
99+
fatalError("Failed to destroy the JVM: \(resultError)")
100+
}
90101
}
91102
}
92103
}
93104

105+
// MARK: Java thread management.
94106
extension JavaVirtualMachine {
107+
/// Produce the JNI environment for the active thread, attaching this
108+
/// thread to the JVM if it isn't already.
109+
///
110+
/// - Parameter
111+
/// - asDaemon: Whether this thread should be treated as a daemon
112+
/// thread in the Java Virtual Machine.
113+
public func environment(asDaemon: Bool = false) throws -> JNIEnvironment {
114+
// Check whether this thread is already attached. If so, return the
115+
// corresponding environment.
116+
var environment: UnsafeMutableRawPointer? = nil
117+
let getEnvResult = jvm.pointee!.pointee.GetEnv(
118+
jvm,
119+
&environment,
120+
JavaVirtualMachine.jniVersion
121+
)
122+
if getEnvResult == JNI_OK, let environment {
123+
return environment.assumingMemoryBound(to: JNIEnv?.self)
124+
}
125+
126+
// Attach the current thread to the JVM.
127+
let attachResult: jint
128+
if asDaemon {
129+
attachResult = jvm.pointee!.pointee.AttachCurrentThreadAsDaemon(jvm, &environment, nil)
130+
} else {
131+
attachResult = jvm.pointee!.pointee.AttachCurrentThread(jvm, &environment, nil)
132+
}
133+
134+
// If we failed to attach, report that.
135+
if let attachError = VMError(fromJNIError: attachResult) {
136+
throw attachError
137+
}
138+
139+
return environment!.assumingMemoryBound(to: JNIEnv?.self)
140+
}
141+
142+
/// Detach the current thread from the Java Virtual Machine. All Java
143+
/// threads waiting for this thread to die are notified.
144+
public func detachCurrentThread() throws {
145+
if let resultError = VMError(fromJNIError: jvm.pointee!.pointee.DetachCurrentThread(jvm)) {
146+
throw resultError
147+
}
148+
}
149+
}
150+
151+
// MARK: Shared Java Virtual Machine management.
152+
extension JavaVirtualMachine {
153+
/// The globally shared JavaVirtualMachine instance, behind a lock.
154+
///
155+
/// TODO: If the use of the lock itself ends up being slow, we could
156+
/// use an atomic here instead because our access pattern is fairly
157+
/// simple.
158+
private static let sharedJVM: LockedState<JavaVirtualMachine?> = .init(initialState: nil)
159+
160+
/// Access the shared Java Virtual Machine instance.
161+
///
162+
/// If there is no shared Java Virtual Machine, create one with the given
163+
/// arguments. Note that this function makes no attempt to try to augment
164+
/// an existing virtual machine instance with the options given, so it is
165+
/// up to clients to ensure that consistent arguments are provided to all
166+
/// calls.
167+
///
168+
/// - Parameters:
169+
/// - classPath: The directories, JAR files, and ZIP files in which the JVM
170+
/// should look to find classes. This maps to the VM option
171+
/// `-Djava.class.path=`.
172+
/// - vmOptions: Options that should be passed along to the JVM, which will
173+
/// be prefixed by the class-path argument described above.
174+
/// - ignoreUnrecognized: Whether the JVM should ignore any VM options it
175+
/// does not recognize.
176+
public static func shared(
177+
classPath: [String] = [],
178+
vmOptions: [String] = [],
179+
ignoreUnrecognized: Bool = true
180+
) throws -> JavaVirtualMachine {
181+
try sharedJVM.withLock { (sharedJVMPointer: inout JavaVirtualMachine?) in
182+
// If we already have a JavaVirtualMachine instance, return it.
183+
if let existingInstance = sharedJVMPointer {
184+
return existingInstance
185+
}
186+
187+
while true {
188+
var wasExistingVM: Bool = false
189+
while true {
190+
// Query the JVM itself to determine whether there is a JVM
191+
// instance that we don't yet know about.
192+
var jvm: UnsafeMutablePointer<JavaVM?>? = nil
193+
var numJVMs: jsize = 0
194+
if JNI_GetCreatedJavaVMs(&jvm, 1, &numJVMs) == JNI_OK, numJVMs >= 1 {
195+
// Adopt this JVM into a new instance of the JavaVirtualMachine
196+
// wrapper.
197+
let javaVirtualMachine = JavaVirtualMachine(adoptingJVM: jvm!)
198+
sharedJVMPointer = javaVirtualMachine
199+
return javaVirtualMachine
200+
}
201+
202+
precondition(
203+
!wasExistingVM,
204+
"JVM reports that an instance of the JVM was already created, but we didn't see it."
205+
)
206+
207+
// Create a new instance of the JVM.
208+
let javaVirtualMachine: JavaVirtualMachine
209+
do {
210+
javaVirtualMachine = try JavaVirtualMachine(
211+
classPath: classPath,
212+
vmOptions: vmOptions,
213+
ignoreUnrecognized: ignoreUnrecognized
214+
)
215+
} catch VMError.existingVM {
216+
// We raced with code outside of this JavaVirtualMachine instance
217+
// that created a VM while we were trying to do the same. Go
218+
// through the loop again to pick up the underlying JVM pointer.
219+
wasExistingVM = true
220+
continue
221+
}
222+
223+
sharedJVMPointer = javaVirtualMachine
224+
return javaVirtualMachine
225+
}
226+
}
227+
}
228+
}
229+
230+
/// "Forget" the shared JavaVirtualMachine instance.
231+
///
232+
/// This will allow the shared JavaVirtualMachine instance to be deallocated.
233+
public static func forgetShared() {
234+
sharedJVM.withLock { sharedJVMPointer in
235+
sharedJVMPointer = nil
236+
}
237+
}
238+
}
239+
240+
extension JavaVirtualMachine {
241+
/// Describes the kinds of errors that can occur when interacting with JNI.
95242
enum VMError: Error {
96-
case failedToCreateVM
243+
/// There is already a Java Virtual Machine.
244+
case existingVM
245+
246+
/// JNI version mismatch error.
247+
case jniVersion
248+
249+
/// Thread is detached from the VM.
250+
case threadDetached
251+
252+
/// Out of memory.
253+
case outOfMemory
254+
255+
/// Invalid arguments.
256+
case invalidArguments
257+
258+
/// Unknown JNI error.
259+
case unknown(jint)
260+
261+
init?(fromJNIError error: jint) {
262+
switch error {
263+
case JNI_OK: return nil
264+
case JNI_EDETACHED: self = .threadDetached
265+
case JNI_EVERSION: self = .jniVersion
266+
case JNI_ENOMEM: self = .outOfMemory
267+
case JNI_EEXIST: self = .existingVM
268+
case JNI_EINVAL: self = .invalidArguments
269+
default: self = .unknown(error)
270+
}
271+
}
97272
}
98273
}

Sources/_Subprocess/LockedState.swift renamed to Sources/JavaKitVM/LockedState.swift

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@
22
//
33
// This source file is part of the Swift.org open source project
44
//
5-
// Copyright (c) 2022 Apple Inc. and the Swift project authors
6-
// Licensed under Apache License v2.0 with Runtime Library Exception
5+
// Copyright (c) 2024 Apple Inc. and the Swift.org project authors
6+
// Licensed under Apache License v2.0
77
//
8-
// See https://swift.org/LICENSE.txt for license information
9-
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
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
1012
//
1113
//===----------------------------------------------------------------------===//
1214

Tests/JavaKitTests/BasicRuntimeTests.swift

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,19 +17,23 @@ import JavaKitNetwork
1717
import JavaKitVM
1818
import XCTest // NOTE: Workaround for https://github.com/swiftlang/swift-java/issues/43
1919

20-
@MainActor
21-
let jvm = try! JavaVirtualMachine(vmOptions: [])
20+
/// Handy reference to the JVM abstraction.
21+
var jvm: JavaVirtualMachine {
22+
get throws {
23+
try .shared()
24+
}
25+
}
2226

23-
@MainActor
2427
class BasicRuntimeTests: XCTestCase {
2528
func testJavaObjectManagement() async throws {
2629
if isLinux {
2730
throw XCTSkip("Attempts to refcount a null pointer on Linux")
2831
}
2932

33+
let environment = try jvm.environment()
3034
let sneakyJavaThis: jobject
3135
do {
32-
let object = JavaObject(environment: jvm.environment)
36+
let object = JavaObject(environment: environment)
3337
XCTAssert(object.toString().starts(with: "java.lang.Object"))
3438

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

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

4650
// 'super' and 'as' don't require allocating a new holder.
47-
let url = try URL("http://swift.org", environment: jvm.environment)
51+
let url = try URL("http://swift.org", environment: environment)
4852
let superURL = url.super
4953
XCTAssert(url.javaHolder === superURL.javaHolder)
5054
let urlAgain = superURL.as(URL.self)!
@@ -56,8 +60,10 @@ class BasicRuntimeTests: XCTestCase {
5660
throw XCTSkip("Attempts to refcount a null pointer on Linux")
5761
}
5862

63+
let environment = try jvm.environment()
64+
5965
do {
60-
_ = try URL("bad url", environment: jvm.environment)
66+
_ = try URL("bad url", environment: environment)
6167
} catch {
6268
XCTAssert(String(describing: error) == "no protocol: bad url")
6369
}
@@ -68,13 +74,17 @@ class BasicRuntimeTests: XCTestCase {
6874
throw XCTSkip("Attempts to refcount a null pointer on Linux")
6975
}
7076

71-
let urlConnectionClass = try JavaClass<URLConnection>(in: jvm.environment)
77+
let environment = try jvm.environment()
78+
79+
let urlConnectionClass = try JavaClass<URLConnection>(in: environment)
7280
XCTAssert(urlConnectionClass.getDefaultAllowUserInteraction() == false)
7381
}
7482

7583
func testClassInstanceLookup() async throws {
84+
let environment = try jvm.environment()
85+
7686
do {
77-
_ = try JavaClass<Nonexistent>(in: jvm.environment)
87+
_ = try JavaClass<Nonexistent>(in: environment)
7888
} catch {
7989
XCTAssertEqual(String(describing: error), "org/swift/javakit/Nonexistent")
8090
}

0 commit comments

Comments
 (0)