Skip to content

Commit adb1e00

Browse files
committed
Introduce support for a shared JavaVirtualMachine instance
Java implementations have a longstanding limitation that there may only be one Java Virtual Machine instance per process. Take advantage of this to provide a shared JavaVirtualMachine instance, initialized on first use, that can be accessed from any thread. This eliminates the need to force JavaVirtualMachine creation to be on the main actor, since we can do it from any thread and get the same result.
1 parent 98b7184 commit adb1e00

File tree

4 files changed

+109
-15
lines changed

4 files changed

+109
-15
lines changed

Sources/Java2Swift/JavaToSwift.swift

Lines changed: 1 addition & 6 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,9 +65,7 @@ struct JavaToSwift: ParsableCommand {
6865
vmOptions.append(contentsOf: classpath)
6966
}
7067

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

Sources/JavaKitVM/JavaVirtualMachine.swift

Lines changed: 98 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ public final class JavaVirtualMachine: @unchecked Sendable {
2323
/// The Java virtual machine instance.
2424
private let jvm: JavaVMPointer
2525

26+
/// Adopt an existing JVM pointer.
27+
private init(adoptingJVM jvm: JavaVMPointer) {
28+
self.jvm = jvm
29+
}
30+
2631
/// Initialize a new Java virtual machine instance.
2732
///
2833
/// - Parameters:
@@ -33,7 +38,7 @@ public final class JavaVirtualMachine: @unchecked Sendable {
3338
/// be prefixed by the class-path argument described above.
3439
/// - ignoreUnrecognized: Whether the JVM should ignore any VM options it
3540
/// does not recognize.
36-
public init(
41+
private init(
3742
classPath: [String] = [],
3843
vmOptions: [String] = [],
3944
ignoreUnrecognized: Bool = true
@@ -74,8 +79,13 @@ public final class JavaVirtualMachine: @unchecked Sendable {
7479
vmArgs.options = optionsBuffer.baseAddress
7580
vmArgs.nOptions = jint(optionsBuffer.count)
7681

77-
// Create the JVM.
78-
if JNI_CreateJavaVM(&jvm, &environment, &vmArgs) != JNI_OK {
82+
// Create the JVM instance.
83+
let createResult = JNI_CreateJavaVM(&jvm, &environment, &vmArgs)
84+
if createResult != JNI_OK {
85+
if createResult == JNI_EEXIST {
86+
throw VMError.existingVM
87+
}
88+
7989
throw VMError.failedToCreateVM
8090
}
8191

@@ -88,7 +98,10 @@ public final class JavaVirtualMachine: @unchecked Sendable {
8898
fatalError("Failed to destroy the JVM.")
8999
}
90100
}
101+
}
91102

103+
// MARK: Java thread management.
104+
extension JavaVirtualMachine {
92105
/// Produce the JNI environment for the active thread, attaching this
93106
/// thread to the JVM if it isn't already.
94107
///
@@ -133,10 +146,92 @@ public final class JavaVirtualMachine: @unchecked Sendable {
133146
}
134147
}
135148

149+
// MARK: Shared Java Virtual Machine management.
150+
extension JavaVirtualMachine {
151+
/// The globally shared JavaVirtualMachine instance, behind a lock.
152+
///
153+
/// TODO: If the use of the lock itself ends up being slow, we could
154+
/// use an atomic here instead because our access pattern is fairly
155+
/// simple.
156+
private static let sharedJVM: LockedState<JavaVirtualMachine?> = .init(initialState: nil)
157+
158+
/// Access the shared Java Virtual Machine instance.
159+
///
160+
/// If there is no shared Java Virtual Machine, create one with the given
161+
/// arguments. Note that this function makes no attempt to try to augment
162+
/// an existing virtual machine instance with the options given, so it is
163+
/// up to clients to ensure that consistent arguments are provided to all
164+
/// calls.
165+
///
166+
/// - Parameters:
167+
/// - classPath: The directories, JAR files, and ZIP files in which the JVM
168+
/// should look to find classes. This maps to the VM option
169+
/// `-Djava.class.path=`.
170+
/// - vmOptions: Options that should be passed along to the JVM, which will
171+
/// be prefixed by the class-path argument described above.
172+
/// - ignoreUnrecognized: Whether the JVM should ignore any VM options it
173+
/// does not recognize.
174+
public static func shared(
175+
classPath: [String] = [],
176+
vmOptions: [String] = [],
177+
ignoreUnrecognized: Bool = true
178+
) throws -> JavaVirtualMachine {
179+
try sharedJVM.withLock { (sharedJVMPointer: inout JavaVirtualMachine?) in
180+
// If we already have a JavaVirtualMachine instance, return it.
181+
if let existingInstance = sharedJVMPointer {
182+
return existingInstance
183+
}
184+
185+
while true {
186+
var wasExistingVM: Bool = false
187+
while true {
188+
// Query the JVM itself to determine whether there is a JVM
189+
// instance that we don't yet know about.
190+
var jvm: UnsafeMutablePointer<JavaVM?>? = nil
191+
var numJVMs: jsize = 0
192+
if JNI_GetCreatedJavaVMs(&jvm, 1, &numJVMs) == JNI_OK, numJVMs >= 1 {
193+
// Adopt this JVM into a new instance of the JavaVirtualMachine
194+
// wrapper.
195+
let javaVirtualMachine = JavaVirtualMachine(adoptingJVM: jvm!)
196+
sharedJVMPointer = javaVirtualMachine
197+
return javaVirtualMachine
198+
}
199+
200+
precondition(
201+
!wasExistingVM,
202+
"JVM reports that an instance of the JVM was already created, but we didn't see it."
203+
)
204+
205+
// Create a new instance of the JVM.
206+
let javaVirtualMachine: JavaVirtualMachine
207+
do {
208+
javaVirtualMachine = try JavaVirtualMachine(
209+
classPath: classPath,
210+
vmOptions: vmOptions,
211+
ignoreUnrecognized: ignoreUnrecognized
212+
)
213+
} catch VMError.existingVM {
214+
// We raced with code outside of this JavaVirtualMachine instance
215+
// that created a VM while we were trying to do the same. Go
216+
// through the loop again to pick up the underlying JVM pointer.
217+
wasExistingVM = true
218+
continue
219+
}
220+
221+
sharedJVMPointer = javaVirtualMachine
222+
return javaVirtualMachine
223+
}
224+
}
225+
}
226+
}
227+
}
228+
136229
extension JavaVirtualMachine {
137230
enum VMError: Error {
138231
case failedToCreateVM
139232
case failedToAttachThread
140233
case failedToDetachThread
234+
case failedToQueryVM
235+
case existingVM
141236
}
142237
}

Tests/JavaKitTests/BasicRuntimeTests.swift

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,20 @@ 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

2327
class BasicRuntimeTests: XCTestCase {
2428
func testJavaObjectManagement() async throws {
2529
if isLinux {
2630
throw XCTSkip("Attempts to refcount a null pointer on Linux")
2731
}
2832

29-
let environment = try await jvm.environment()
33+
let environment = try jvm.environment()
3034
let sneakyJavaThis: jobject
3135
do {
3236
let object = JavaObject(environment: environment)
@@ -56,7 +60,7 @@ class BasicRuntimeTests: XCTestCase {
5660
throw XCTSkip("Attempts to refcount a null pointer on Linux")
5761
}
5862

59-
let environment = try await jvm.environment()
63+
let environment = try jvm.environment()
6064

6165
do {
6266
_ = try URL("bad url", environment: environment)
@@ -70,14 +74,14 @@ class BasicRuntimeTests: XCTestCase {
7074
throw XCTSkip("Attempts to refcount a null pointer on Linux")
7175
}
7276

73-
let environment = try await jvm.environment()
77+
let environment = try jvm.environment()
7478

7579
let urlConnectionClass = try JavaClass<URLConnection>(in: environment)
7680
XCTAssert(urlConnectionClass.getDefaultAllowUserInteraction() == false)
7781
}
7882

7983
func testClassInstanceLookup() async throws {
80-
let environment = try await jvm.environment()
84+
let environment = try jvm.environment()
8185

8286
do {
8387
_ = try JavaClass<Nonexistent>(in: environment)

0 commit comments

Comments
 (0)