@@ -17,11 +17,20 @@ import JavaKit
1717typealias JavaVMPointer = UnsafeMutablePointer < JavaVM ? >
1818
1919public 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.
94106extension 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}
0 commit comments