Skip to content

Commit 9218a76

Browse files
authored
Local/global reference management functions (#11)
* Local/global reference management functions * Local/global reference management functions * Add more docs from JNI spec and some simple tests * adjust the withLocalFrame funcs to clear OOM and throw swift error * missing Bionic import
1 parent 15f577f commit 9218a76

3 files changed

Lines changed: 301 additions & 0 deletions

File tree

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2024 Apple Inc. and the Swift.org project authors
6+
// Licensed under Apache License v2.0
7+
//
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
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
/// Errors originating from JNI environment operations.
16+
public enum JNIError: Error {
17+
/// The JVM was unable to allocate memory for a local reference frame.
18+
///
19+
/// This occurs when `PushLocalFrame` fails, typically because the JVM
20+
/// is running low on memory. The pending Java `OutOfMemoryError` is
21+
/// cleared before this error is thrown.
22+
///
23+
/// - Parameter framePushCapacity: The requested frame capacity that failed.
24+
case outOfMemory(framePushCapacity: Int)
25+
}
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2024 Apple Inc. and the Swift.org project authors
6+
// Licensed under Apache License v2.0
7+
//
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
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
#if canImport(Glibc)
16+
import Glibc
17+
#elseif canImport(Musl)
18+
import Musl
19+
#elseif canImport(Bionic)
20+
import Bionic
21+
#elseif canImport(Darwin)
22+
import Darwin
23+
#elseif os(Windows)
24+
import ucrt
25+
#endif
26+
27+
// ==== -------------------------------------------------------------------
28+
// MARK: Local Frame Helpers
29+
30+
// Local references are valid for the duration of a native method call. They are
31+
// freed automatically after the native method returns. Each local reference
32+
// costs some amount of Java Virtual Machine resource. Programmers need to make
33+
// sure that native methods do not excessively allocate local references.
34+
// Although local references are automatically freed after the native method
35+
// returns to Java, excessive allocation of local references may cause the VM to
36+
// run out of memory during the execution of a native method.
37+
//
38+
// See: https://docs.oracle.com/en/java/javase/21/docs/specs/jni/functions.html#local-references
39+
40+
/// Whether to print JNI `OutOfMemoryError` stack traces to stderr.
41+
///
42+
/// Checked once on first OOM and cached. Set the environment variable
43+
/// `SWIFT_JAVA_JNI_EXCEPTION_DESCRIBE_OOM` to `true` or `1` to enable.
44+
private let describeOOMException: Bool = {
45+
guard let value = getenv("SWIFT_JAVA_JNI_EXCEPTION_DESCRIBE_OOM") else {
46+
return false
47+
}
48+
let str = String(cString: value).lowercased()
49+
return str == "1" || str == "true" || str == "yes"
50+
}()
51+
52+
extension UnsafeMutablePointer<JNIEnv?> {
53+
54+
/// Handle a `PushLocalFrame` failure by optionally describing the pending
55+
/// exception to stderr, clearing it, and throwing a Swift error.
56+
///
57+
/// Must be called while the `OutOfMemoryError` is still pending (i.e.
58+
/// before `ExceptionClear`). `ExceptionDescribe` is safe to call with a
59+
/// pending exception — it prints the stack trace to stderr and does **not**
60+
/// clear the exception.
61+
@inline(__always)
62+
internal func throwPushLocalFrameOOM(capacity: Int) throws -> Never {
63+
if describeOOMException {
64+
// Print the pending OutOfMemoryError stack trace to stderr.
65+
// ExceptionDescribe does not clear the exception.
66+
self.interface.ExceptionDescribe(self)
67+
}
68+
self.interface.ExceptionClear(self)
69+
throw JNIError.outOfMemory(framePushCapacity: capacity)
70+
}
71+
72+
/// Execute `body` inside a JNI local reference frame.
73+
///
74+
/// All local references created inside `body` are freed when it returns.
75+
/// This prevents local reference table overflow when making many JNI calls
76+
/// (e.g., in loops or from non-JVM threads like Swift's cooperative pool).
77+
///
78+
/// - Parameter capacity: Hint for how many local refs will be created.
79+
/// The JVM may allocate more if needed. Must be > 0.
80+
/// - Parameter body: The closure to execute inside the local frame.
81+
/// - Returns: The value returned by `body`.
82+
/// - Throws: ``JNIError/outOfMemory`` if `PushLocalFrame` fails, or
83+
/// rethrows any error thrown by `body`.
84+
///
85+
/// ## See Also
86+
/// - [JNI PushLocalFrame](https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/functions.html#PushLocalFrame)
87+
/// - [JNI PopLocalFrame](https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/functions.html#PopLocalFrame)
88+
@inline(__always)
89+
public func withLocalFrame<R>(capacity: Int = 16, _ body: () throws -> R) throws -> R {
90+
let pushed = self.interface.PushLocalFrame(self, Int32(capacity))
91+
if pushed != JNI_OK {
92+
try self.throwPushLocalFrameOOM(capacity: capacity)
93+
}
94+
defer { _ = self.interface.PopLocalFrame(self, nil) }
95+
return try body()
96+
}
97+
98+
/// Execute `body` inside a JNI local reference frame, promoting one result
99+
/// object to the outer frame.
100+
///
101+
/// All local references created inside `body` are freed, **except** for the
102+
/// returned `jobject` which is promoted to the enclosing frame via
103+
/// `PopLocalFrame(env, result)`.
104+
///
105+
/// Use this when constructing a new Java object inside a frame that needs
106+
/// to survive after the frame is popped.
107+
///
108+
/// - Parameter capacity: Hint for how many local refs will be created.
109+
/// - Parameter body: Closure that returns the `jobject` to promote.
110+
/// - Returns: A new local reference in the outer frame to the same object.
111+
/// - Throws: ``JNIError/outOfMemory`` if `PushLocalFrame` fails, or
112+
/// rethrows any error thrown by `body`.
113+
///
114+
/// ## See Also
115+
/// - [JNI PushLocalFrame](https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/functions.html#PushLocalFrame)
116+
/// - [JNI PopLocalFrame](https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/functions.html#PopLocalFrame)
117+
@inline(__always)
118+
public func withLocalFramePromotingResult(capacity: Int = 16, _ body: () throws -> jobject?) throws -> jobject? {
119+
let pushed = self.interface.PushLocalFrame(self, Int32(capacity))
120+
if pushed != JNI_OK {
121+
try self.throwPushLocalFrameOOM(capacity: capacity)
122+
}
123+
do {
124+
let result = try body()
125+
return self.interface.PopLocalFrame(self, result)
126+
} catch {
127+
// Pop the frame (freeing all inner refs) before rethrowing.
128+
_ = self.interface.PopLocalFrame(self, nil)
129+
throw error
130+
}
131+
}
132+
133+
/// Delete a local reference.
134+
///
135+
/// Shorthand for `interface.DeleteLocalRef(self, ref)`. Safe to call with
136+
/// `nil` (no-op).
137+
///
138+
/// ## See Also
139+
/// - [JNI DeleteLocalRef](https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/functions.html#DeleteLocalRef)
140+
@inline(__always)
141+
public func deleteLocalRef(_ ref: jobject?) {
142+
self.interface.DeleteLocalRef(self, ref)
143+
}
144+
}
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2026 Apple Inc. and the Swift.org project authors
6+
// Licensed under Apache License v2.0
7+
//
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
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import Testing
16+
17+
@testable import SwiftJavaJNICore
18+
19+
#if canImport(FoundationEssentials)
20+
import class FoundationEssentials.ProcessInfo
21+
#else
22+
import class Foundation.ProcessInfo
23+
#endif
24+
25+
@Suite
26+
struct JavaEnvironmentTests {
27+
28+
static var isSupportedPlatform: Bool {
29+
#if os(Android)
30+
let testSentinel = "0"
31+
#else
32+
let testSentinel = "1"
33+
#endif
34+
return (ProcessInfo.processInfo.environment["SWIFT_JAVA_JNI_TEST_JVM"] ?? testSentinel) != "0"
35+
}
36+
37+
@Test(.enabled(if: isSupportedPlatform))
38+
func withLocalFrame_returnsBodyValue() throws {
39+
let env = try JavaVirtualMachine.shared().environment()
40+
41+
let result = try env.withLocalFrame(capacity: 4) {
42+
42
43+
}
44+
#expect(result == 42)
45+
}
46+
47+
@Test(.enabled(if: isSupportedPlatform))
48+
func withLocalFrame_defaultCapacity() throws {
49+
let env = try JavaVirtualMachine.shared().environment()
50+
51+
let result = try env.withLocalFrame {
52+
"hello"
53+
}
54+
#expect(result == "hello")
55+
}
56+
57+
@Test(.enabled(if: isSupportedPlatform))
58+
func withLocalFrame_rethrowsErrors() throws {
59+
let env = try JavaVirtualMachine.shared().environment()
60+
61+
struct TestError: Error {}
62+
63+
#expect(throws: TestError.self) {
64+
try env.withLocalFrame {
65+
throw TestError()
66+
}
67+
}
68+
}
69+
70+
@Test(.enabled(if: isSupportedPlatform))
71+
func withLocalFrame_localRefsWorkInsideFrame() throws {
72+
let env = try JavaVirtualMachine.shared().environment()
73+
74+
try env.withLocalFrame(capacity: 8) {
75+
// Create a local ref inside the frame — it should be valid here
76+
let cls = env.interface.FindClass(env, "java/lang/String")
77+
#expect(cls != nil, "Should be able to find java.lang.String inside frame")
78+
}
79+
}
80+
81+
@Test(.enabled(if: isSupportedPlatform))
82+
func withLocalFramePromotingResult_promotesObject() throws {
83+
let env = try JavaVirtualMachine.shared().environment()
84+
85+
let promoted = try env.withLocalFramePromotingResult(capacity: 8) { () -> jobject? in
86+
// Create a Java String inside the frame
87+
let str = env.interface.NewStringUTF(env, "test")
88+
return str
89+
}
90+
91+
// The promoted reference should still be valid in the outer frame
92+
#expect(promoted != nil, "Promoted reference should not be nil")
93+
94+
// Verify it's a valid object by getting its class
95+
let cls = env.interface.GetObjectClass(env, promoted)
96+
#expect(cls != nil, "Promoted object should have a valid class")
97+
98+
env.deleteLocalRef(promoted)
99+
env.deleteLocalRef(cls)
100+
}
101+
102+
@Test(.enabled(if: isSupportedPlatform))
103+
func withLocalFramePromotingResult_nilResult() throws {
104+
let env = try JavaVirtualMachine.shared().environment()
105+
106+
let result = try env.withLocalFramePromotingResult {
107+
nil
108+
}
109+
#expect(result == nil)
110+
}
111+
112+
@Test(.enabled(if: isSupportedPlatform))
113+
func withLocalFramePromotingResult_rethrowsErrors() throws {
114+
let env = try JavaVirtualMachine.shared().environment()
115+
116+
struct TestError: Error {}
117+
118+
#expect(throws: TestError.self) {
119+
try env.withLocalFramePromotingResult {
120+
throw TestError()
121+
}
122+
}
123+
}
124+
125+
@Test(.enabled(if: isSupportedPlatform))
126+
func deleteLocalRef_nilIsSafe() throws {
127+
let env = try JavaVirtualMachine.shared().environment()
128+
129+
// Should not crash
130+
env.deleteLocalRef(nil)
131+
}
132+
}

0 commit comments

Comments
 (0)