Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions Sources/SwiftJavaJNICore/JNIError.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2024 Apple Inc. and the Swift.org project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of Swift.org project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

/// Errors originating from JNI environment operations.
public enum JNIError: Error {
/// The JVM was unable to allocate memory for a local reference frame.
///
/// This occurs when `PushLocalFrame` fails, typically because the JVM
/// is running low on memory. The pending Java `OutOfMemoryError` is
/// cleared before this error is thrown.
///
/// - Parameter framePushCapacity: The requested frame capacity that failed.
case outOfMemory(framePushCapacity: Int)
}
144 changes: 144 additions & 0 deletions Sources/SwiftJavaJNICore/JavaEnvironment+Refs.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2024 Apple Inc. and the Swift.org project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of Swift.org project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

#if canImport(Glibc)
import Glibc
#elseif canImport(Musl)
import Musl
#elseif canImport(Bionic)
import Bionic
#elseif canImport(Darwin)
import Darwin
#elseif os(Windows)
import ucrt
#endif

// ==== -------------------------------------------------------------------
// MARK: Local Frame Helpers

// Local references are valid for the duration of a native method call. They are
// freed automatically after the native method returns. Each local reference
// costs some amount of Java Virtual Machine resource. Programmers need to make
// sure that native methods do not excessively allocate local references.
// Although local references are automatically freed after the native method
// returns to Java, excessive allocation of local references may cause the VM to
// run out of memory during the execution of a native method.
//
// See: https://docs.oracle.com/en/java/javase/21/docs/specs/jni/functions.html#local-references

/// Whether to print JNI `OutOfMemoryError` stack traces to stderr.
///
/// Checked once on first OOM and cached. Set the environment variable
/// `SWIFT_JAVA_JNI_EXCEPTION_DESCRIBE_OOM` to `true` or `1` to enable.
private let describeOOMException: Bool = {
guard let value = getenv("SWIFT_JAVA_JNI_EXCEPTION_DESCRIBE_OOM") else {
return false
}
let str = String(cString: value).lowercased()
return str == "1" || str == "true" || str == "yes"
}()

extension UnsafeMutablePointer<JNIEnv?> {

/// Handle a `PushLocalFrame` failure by optionally describing the pending
/// exception to stderr, clearing it, and throwing a Swift error.
///
/// Must be called while the `OutOfMemoryError` is still pending (i.e.
/// before `ExceptionClear`). `ExceptionDescribe` is safe to call with a
/// pending exception — it prints the stack trace to stderr and does **not**
/// clear the exception.
@inline(__always)
internal func throwPushLocalFrameOOM(capacity: Int) throws -> Never {
if describeOOMException {
// Print the pending OutOfMemoryError stack trace to stderr.
// ExceptionDescribe does not clear the exception.
self.interface.ExceptionDescribe(self)
}
self.interface.ExceptionClear(self)
throw JNIError.outOfMemory(framePushCapacity: capacity)
}

/// Execute `body` inside a JNI local reference frame.
///
/// All local references created inside `body` are freed when it returns.
/// This prevents local reference table overflow when making many JNI calls
/// (e.g., in loops or from non-JVM threads like Swift's cooperative pool).
///
/// - Parameter capacity: Hint for how many local refs will be created.
/// The JVM may allocate more if needed. Must be > 0.
/// - Parameter body: The closure to execute inside the local frame.
/// - Returns: The value returned by `body`.
/// - Throws: ``JNIError/outOfMemory`` if `PushLocalFrame` fails, or
/// rethrows any error thrown by `body`.
///
/// ## See Also
/// - [JNI PushLocalFrame](https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/functions.html#PushLocalFrame)
/// - [JNI PopLocalFrame](https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/functions.html#PopLocalFrame)
@inline(__always)
public func withLocalFrame<R>(capacity: Int = 16, _ body: () throws -> R) throws -> R {
let pushed = self.interface.PushLocalFrame(self, Int32(capacity))
if pushed != JNI_OK {
try self.throwPushLocalFrameOOM(capacity: capacity)
}
defer { _ = self.interface.PopLocalFrame(self, nil) }
return try body()
}

/// Execute `body` inside a JNI local reference frame, promoting one result
/// object to the outer frame.
///
/// All local references created inside `body` are freed, **except** for the
/// returned `jobject` which is promoted to the enclosing frame via
/// `PopLocalFrame(env, result)`.
///
/// Use this when constructing a new Java object inside a frame that needs
/// to survive after the frame is popped.
///
/// - Parameter capacity: Hint for how many local refs will be created.
/// - Parameter body: Closure that returns the `jobject` to promote.
/// - Returns: A new local reference in the outer frame to the same object.
/// - Throws: ``JNIError/outOfMemory`` if `PushLocalFrame` fails, or
/// rethrows any error thrown by `body`.
///
/// ## See Also
/// - [JNI PushLocalFrame](https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/functions.html#PushLocalFrame)
/// - [JNI PopLocalFrame](https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/functions.html#PopLocalFrame)
@inline(__always)
public func withLocalFramePromotingResult(capacity: Int = 16, _ body: () throws -> jobject?) throws -> jobject? {
let pushed = self.interface.PushLocalFrame(self, Int32(capacity))
if pushed != JNI_OK {
try self.throwPushLocalFrameOOM(capacity: capacity)
}
do {
let result = try body()
return self.interface.PopLocalFrame(self, result)
} catch {
// Pop the frame (freeing all inner refs) before rethrowing.
_ = self.interface.PopLocalFrame(self, nil)
throw error
}
}

/// Delete a local reference.
///
/// Shorthand for `interface.DeleteLocalRef(self, ref)`. Safe to call with
/// `nil` (no-op).
///
/// ## See Also
/// - [JNI DeleteLocalRef](https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/functions.html#DeleteLocalRef)
@inline(__always)
public func deleteLocalRef(_ ref: jobject?) {
self.interface.DeleteLocalRef(self, ref)
}
}
132 changes: 132 additions & 0 deletions Tests/SwiftJavaJNICoreTests/JavaEnvironmentTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2026 Apple Inc. and the Swift.org project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of Swift.org project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

import Testing

@testable import SwiftJavaJNICore

#if canImport(FoundationEssentials)
import class FoundationEssentials.ProcessInfo
#else
import class Foundation.ProcessInfo
#endif

@Suite
struct JavaEnvironmentTests {

static var isSupportedPlatform: Bool {
#if os(Android)
let testSentinel = "0"
#else
let testSentinel = "1"
#endif
return (ProcessInfo.processInfo.environment["SWIFT_JAVA_JNI_TEST_JVM"] ?? testSentinel) != "0"
}

@Test(.enabled(if: isSupportedPlatform))
func withLocalFrame_returnsBodyValue() throws {
let env = try JavaVirtualMachine.shared().environment()

let result = try env.withLocalFrame(capacity: 4) {
42
}
#expect(result == 42)
}

@Test(.enabled(if: isSupportedPlatform))
func withLocalFrame_defaultCapacity() throws {
let env = try JavaVirtualMachine.shared().environment()

let result = try env.withLocalFrame {
"hello"
}
#expect(result == "hello")
}

@Test(.enabled(if: isSupportedPlatform))
func withLocalFrame_rethrowsErrors() throws {
let env = try JavaVirtualMachine.shared().environment()

struct TestError: Error {}

#expect(throws: TestError.self) {
try env.withLocalFrame {
throw TestError()
}
}
}

@Test(.enabled(if: isSupportedPlatform))
func withLocalFrame_localRefsWorkInsideFrame() throws {
let env = try JavaVirtualMachine.shared().environment()

try env.withLocalFrame(capacity: 8) {
// Create a local ref inside the frame — it should be valid here
let cls = env.interface.FindClass(env, "java/lang/String")
#expect(cls != nil, "Should be able to find java.lang.String inside frame")
}
}

@Test(.enabled(if: isSupportedPlatform))
func withLocalFramePromotingResult_promotesObject() throws {
let env = try JavaVirtualMachine.shared().environment()

let promoted = try env.withLocalFramePromotingResult(capacity: 8) { () -> jobject? in
// Create a Java String inside the frame
let str = env.interface.NewStringUTF(env, "test")
return str
}

// The promoted reference should still be valid in the outer frame
#expect(promoted != nil, "Promoted reference should not be nil")

// Verify it's a valid object by getting its class
let cls = env.interface.GetObjectClass(env, promoted)
#expect(cls != nil, "Promoted object should have a valid class")

env.deleteLocalRef(promoted)
env.deleteLocalRef(cls)
}

@Test(.enabled(if: isSupportedPlatform))
func withLocalFramePromotingResult_nilResult() throws {
let env = try JavaVirtualMachine.shared().environment()

let result = try env.withLocalFramePromotingResult {
nil
}
#expect(result == nil)
}

@Test(.enabled(if: isSupportedPlatform))
func withLocalFramePromotingResult_rethrowsErrors() throws {
let env = try JavaVirtualMachine.shared().environment()

struct TestError: Error {}

#expect(throws: TestError.self) {
try env.withLocalFramePromotingResult {
throw TestError()
}
}
}

@Test(.enabled(if: isSupportedPlatform))
func deleteLocalRef_nilIsSafe() throws {
let env = try JavaVirtualMachine.shared().environment()

// Should not crash
env.deleteLocalRef(nil)
}
}
Loading