@@ -17,11 +17,20 @@ import JavaKit
17
17
typealias JavaVMPointer = UnsafeMutablePointer < JavaVM ? >
18
18
19
19
public final class JavaVirtualMachine : @unchecked Sendable {
20
+ /// The JNI version that we depend on.
21
+ static let jniVersion = JNI_VERSION_1_6
22
+
20
23
/// The Java virtual machine instance.
21
24
private let jvm : JavaVMPointer
22
25
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
+ }
25
34
26
35
/// Initialize a new Java virtual machine instance.
27
36
///
@@ -33,15 +42,15 @@ public final class JavaVirtualMachine: @unchecked Sendable {
33
42
/// be prefixed by the class-path argument described above.
34
43
/// - ignoreUnrecognized: Whether the JVM should ignore any VM options it
35
44
/// does not recognize.
36
- public init (
45
+ private init (
37
46
classPath: [ String ] = [ ] ,
38
47
vmOptions: [ String ] = [ ] ,
39
48
ignoreUnrecognized: Bool = true
40
49
) throws {
41
50
var jvm : JavaVMPointer ? = nil
42
51
var environment : UnsafeMutableRawPointer ? = nil
43
52
var vmArgs = JavaVMInitArgs ( )
44
- vmArgs. version = JNI_VERSION_1_6
53
+ vmArgs. version = JavaVirtualMachine . jniVersion
45
54
vmArgs. ignoreUnrecognized = jboolean ( ignoreUnrecognized ? JNI_TRUE : JNI_FALSE)
46
55
47
56
// Construct the complete list of VM options.
@@ -74,25 +83,191 @@ public final class JavaVirtualMachine: @unchecked Sendable {
74
83
vmArgs. options = optionsBuffer. baseAddress
75
84
vmArgs. nOptions = jint ( optionsBuffer. count)
76
85
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
80
89
}
81
90
82
91
self . jvm = jvm!
83
- self . environment = environment! . assumingMemoryBound ( to : JNIEnv ? . self )
92
+ self . destroyOnDeinit = true
84
93
}
85
94
86
95
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
+ }
90
101
}
91
102
}
92
103
}
93
104
105
+ // MARK: Java thread management.
94
106
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.
95
242
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
+ }
97
272
}
98
273
}
0 commit comments