From e805badc12c302481153488eb0af37b9c1bb894c Mon Sep 17 00:00:00 2001 From: Konrad Malawski Date: Mon, 16 Mar 2026 12:35:05 +0900 Subject: [PATCH 1/5] Local/global reference management functions --- .../SwiftJavaJNICore/JavaEnvironment.swift | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/Sources/SwiftJavaJNICore/JavaEnvironment.swift b/Sources/SwiftJavaJNICore/JavaEnvironment.swift index 1612799..937a1a3 100644 --- a/Sources/SwiftJavaJNICore/JavaEnvironment.swift +++ b/Sources/SwiftJavaJNICore/JavaEnvironment.swift @@ -19,4 +19,72 @@ public typealias JNINativeInterface_ = JNINativeInterface extension UnsafeMutablePointer { public var interface: JNINativeInterface_ { self.pointee!.pointee } + + // ==== ------------------------------------------------------------------- + // MARK: JNI Local Frame Helpers + + /// 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). + /// + /// If `PushLocalFrame` fails (returns negative), `body` is still executed + /// but without a frame — this avoids popping the wrong frame on error. + /// + /// - 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: Rethrows any error thrown by `body`. + @inline(__always) + public func withJNILocalFrame(capacity: Int32, _ body: () throws -> R) rethrows -> R { + let pushed = self.interface.PushLocalFrame(self, capacity) + if pushed != JNI_OK { + // PushLocalFrame failed (OutOfMemoryError pending). Execute body without + // a frame rather than popping the wrong frame in the defer. + return try body() + } + 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: Rethrows any error thrown by `body`. + @inline(__always) + public func withJNILocalFramePromoting(capacity: Int32, _ body: () throws -> jobject?) rethrows -> jobject? { + let pushed = self.interface.PushLocalFrame(self, capacity) + if pushed != JNI_OK { + return try body() + } + 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). + @inline(__always) + public func deleteLocalRef(_ ref: jobject?) { + self.interface.DeleteLocalRef(self, ref) + } } From b5e79b24e48e8794f8f3803f2552b9c5acdba5be Mon Sep 17 00:00:00 2001 From: Konrad Malawski Date: Mon, 16 Mar 2026 15:31:24 +0900 Subject: [PATCH 2/5] Local/global reference management functions --- Sources/SwiftJavaJNICore/JavaEnvironment.swift | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/Sources/SwiftJavaJNICore/JavaEnvironment.swift b/Sources/SwiftJavaJNICore/JavaEnvironment.swift index d063c45..b53c876 100644 --- a/Sources/SwiftJavaJNICore/JavaEnvironment.swift +++ b/Sources/SwiftJavaJNICore/JavaEnvironment.swift @@ -13,10 +13,13 @@ //===----------------------------------------------------------------------===// extension UnsafeMutablePointer { - public var interface: JNINativeInterface_ { self.pointee!.pointee } + public var interface: JNINativeInterface { self.pointee!.pointee } +} + +// ==== ------------------------------------------------------------------- +// MARK: JNI Local Frame Helpers - // ==== ------------------------------------------------------------------- - // MARK: JNI Local Frame Helpers +extension UnsafeMutablePointer { /// Execute `body` inside a JNI local reference frame. /// @@ -33,8 +36,8 @@ extension UnsafeMutablePointer { /// - Returns: The value returned by `body`. /// - Throws: Rethrows any error thrown by `body`. @inline(__always) - public func withJNILocalFrame(capacity: Int32, _ body: () throws -> R) rethrows -> R { - let pushed = self.interface.PushLocalFrame(self, capacity) + public func withJNILocalFrame(capacity: Int = 16, _ body: () throws -> R) rethrows -> R { + let pushed = self.interface.PushLocalFrame(self, Int32(capacity)) if pushed != JNI_OK { // PushLocalFrame failed (OutOfMemoryError pending). Execute body without // a frame rather than popping the wrong frame in the defer. @@ -59,8 +62,8 @@ extension UnsafeMutablePointer { /// - Returns: A new local reference in the outer frame to the same object. /// - Throws: Rethrows any error thrown by `body`. @inline(__always) - public func withJNILocalFramePromoting(capacity: Int32, _ body: () throws -> jobject?) rethrows -> jobject? { - let pushed = self.interface.PushLocalFrame(self, capacity) + public func withJNILocalFramePromotingResult(capacity: Int = 16, _ body: () throws -> jobject?) rethrows -> jobject? { + let pushed = self.interface.PushLocalFrame(self, Int32(capacity)) if pushed != JNI_OK { return try body() } From 801564c06ebf3b90ee8739a0575a32f4fc630336 Mon Sep 17 00:00:00 2001 From: Konrad Malawski Date: Mon, 16 Mar 2026 15:47:43 +0900 Subject: [PATCH 3/5] Add more docs from JNI spec and some simple tests --- .../JavaEnvironment+Refs.swift | 105 ++++++++++++++ .../SwiftJavaJNICore/JavaEnvironment.swift | 71 ---------- .../JavaEnvironmentTests.swift | 132 ++++++++++++++++++ 3 files changed, 237 insertions(+), 71 deletions(-) create mode 100644 Sources/SwiftJavaJNICore/JavaEnvironment+Refs.swift create mode 100644 Tests/SwiftJavaJNICoreTests/JavaEnvironmentTests.swift diff --git a/Sources/SwiftJavaJNICore/JavaEnvironment+Refs.swift b/Sources/SwiftJavaJNICore/JavaEnvironment+Refs.swift new file mode 100644 index 0000000..4383e67 --- /dev/null +++ b/Sources/SwiftJavaJNICore/JavaEnvironment+Refs.swift @@ -0,0 +1,105 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +// ==== ------------------------------------------------------------------- +// 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 + +extension UnsafeMutablePointer { + + /// 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). + /// + /// If `PushLocalFrame` fails (returns negative), `body` is still executed + /// but without a frame — this avoids popping the wrong frame on error. + /// + /// - 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: 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(capacity: Int = 16, _ body: () throws -> R) rethrows -> R { + let pushed = self.interface.PushLocalFrame(self, Int32(capacity)) + if pushed != JNI_OK { + // PushLocalFrame failed (OutOfMemoryError pending). Execute body without + // a frame rather than popping the wrong frame in the defer. + return try body() + } + 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: 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?) rethrows -> jobject? { + let pushed = self.interface.PushLocalFrame(self, Int32(capacity)) + if pushed != JNI_OK { + return try body() + } + 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) + } +} diff --git a/Sources/SwiftJavaJNICore/JavaEnvironment.swift b/Sources/SwiftJavaJNICore/JavaEnvironment.swift index b53c876..84998bc 100644 --- a/Sources/SwiftJavaJNICore/JavaEnvironment.swift +++ b/Sources/SwiftJavaJNICore/JavaEnvironment.swift @@ -15,74 +15,3 @@ extension UnsafeMutablePointer { public var interface: JNINativeInterface { self.pointee!.pointee } } - -// ==== ------------------------------------------------------------------- -// MARK: JNI Local Frame Helpers - -extension UnsafeMutablePointer { - - /// 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). - /// - /// If `PushLocalFrame` fails (returns negative), `body` is still executed - /// but without a frame — this avoids popping the wrong frame on error. - /// - /// - 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: Rethrows any error thrown by `body`. - @inline(__always) - public func withJNILocalFrame(capacity: Int = 16, _ body: () throws -> R) rethrows -> R { - let pushed = self.interface.PushLocalFrame(self, Int32(capacity)) - if pushed != JNI_OK { - // PushLocalFrame failed (OutOfMemoryError pending). Execute body without - // a frame rather than popping the wrong frame in the defer. - return try body() - } - 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: Rethrows any error thrown by `body`. - @inline(__always) - public func withJNILocalFramePromotingResult(capacity: Int = 16, _ body: () throws -> jobject?) rethrows -> jobject? { - let pushed = self.interface.PushLocalFrame(self, Int32(capacity)) - if pushed != JNI_OK { - return try body() - } - 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). - @inline(__always) - public func deleteLocalRef(_ ref: jobject?) { - self.interface.DeleteLocalRef(self, ref) - } -} diff --git a/Tests/SwiftJavaJNICoreTests/JavaEnvironmentTests.swift b/Tests/SwiftJavaJNICoreTests/JavaEnvironmentTests.swift new file mode 100644 index 0000000..405b2dd --- /dev/null +++ b/Tests/SwiftJavaJNICoreTests/JavaEnvironmentTests.swift @@ -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 = env.withLocalFrame(capacity: 4) { + 42 + } + #expect(result == 42) + } + + @Test(.enabled(if: isSupportedPlatform)) + func withLocalFrame_defaultCapacity() throws { + let env = try JavaVirtualMachine.shared().environment() + + let result = 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() + + 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 = 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 = 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) + } +} From 6788f1c90ed4bc74b5b46f4694400fbfcce18c2b Mon Sep 17 00:00:00 2001 From: Konrad Malawski Date: Tue, 17 Mar 2026 15:35:18 +0900 Subject: [PATCH 4/5] adjust the withLocalFrame funcs to clear OOM and throw swift error --- Sources/SwiftJavaJNICore/JNIError.swift | 25 ++++++++ .../JavaEnvironment+Refs.swift | 59 +++++++++++++++---- .../JavaEnvironmentTests.swift | 10 ++-- 3 files changed, 78 insertions(+), 16 deletions(-) create mode 100644 Sources/SwiftJavaJNICore/JNIError.swift diff --git a/Sources/SwiftJavaJNICore/JNIError.swift b/Sources/SwiftJavaJNICore/JNIError.swift new file mode 100644 index 0000000..9105fcc --- /dev/null +++ b/Sources/SwiftJavaJNICore/JNIError.swift @@ -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) +} diff --git a/Sources/SwiftJavaJNICore/JavaEnvironment+Refs.swift b/Sources/SwiftJavaJNICore/JavaEnvironment+Refs.swift index 4383e67..f3399d3 100644 --- a/Sources/SwiftJavaJNICore/JavaEnvironment+Refs.swift +++ b/Sources/SwiftJavaJNICore/JavaEnvironment+Refs.swift @@ -12,6 +12,16 @@ // //===----------------------------------------------------------------------===// +#if canImport(Glibc) +import Glibc +#elseif canImport(Musl) +import Musl +#elseif canImport(Darwin) +import Darwin +#elseif os(Windows) +import ucrt +#endif + // ==== ------------------------------------------------------------------- // MARK: Local Frame Helpers @@ -25,33 +35,59 @@ // // 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 { + /// 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). /// - /// If `PushLocalFrame` fails (returns negative), `body` is still executed - /// but without a frame — this avoids popping the wrong frame on error. - /// /// - 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: Rethrows any error thrown 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(capacity: Int = 16, _ body: () throws -> R) rethrows -> R { + public func withLocalFrame(capacity: Int = 16, _ body: () throws -> R) throws -> R { let pushed = self.interface.PushLocalFrame(self, Int32(capacity)) if pushed != JNI_OK { - // PushLocalFrame failed (OutOfMemoryError pending). Execute body without - // a frame rather than popping the wrong frame in the defer. - return try body() + try self.throwPushLocalFrameOOM(capacity: capacity) } defer { _ = self.interface.PopLocalFrame(self, nil) } return try body() @@ -70,16 +106,17 @@ extension UnsafeMutablePointer { /// - 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: Rethrows any error thrown 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 withLocalFramePromotingResult(capacity: Int = 16, _ body: () throws -> jobject?) rethrows -> jobject? { + public func withLocalFramePromotingResult(capacity: Int = 16, _ body: () throws -> jobject?) throws -> jobject? { let pushed = self.interface.PushLocalFrame(self, Int32(capacity)) if pushed != JNI_OK { - return try body() + try self.throwPushLocalFrameOOM(capacity: capacity) } do { let result = try body() diff --git a/Tests/SwiftJavaJNICoreTests/JavaEnvironmentTests.swift b/Tests/SwiftJavaJNICoreTests/JavaEnvironmentTests.swift index 405b2dd..2c62119 100644 --- a/Tests/SwiftJavaJNICoreTests/JavaEnvironmentTests.swift +++ b/Tests/SwiftJavaJNICoreTests/JavaEnvironmentTests.swift @@ -38,7 +38,7 @@ struct JavaEnvironmentTests { func withLocalFrame_returnsBodyValue() throws { let env = try JavaVirtualMachine.shared().environment() - let result = env.withLocalFrame(capacity: 4) { + let result = try env.withLocalFrame(capacity: 4) { 42 } #expect(result == 42) @@ -48,7 +48,7 @@ struct JavaEnvironmentTests { func withLocalFrame_defaultCapacity() throws { let env = try JavaVirtualMachine.shared().environment() - let result = env.withLocalFrame { + let result = try env.withLocalFrame { "hello" } #expect(result == "hello") @@ -71,7 +71,7 @@ struct JavaEnvironmentTests { func withLocalFrame_localRefsWorkInsideFrame() throws { let env = try JavaVirtualMachine.shared().environment() - env.withLocalFrame(capacity: 8) { + 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") @@ -82,7 +82,7 @@ struct JavaEnvironmentTests { func withLocalFramePromotingResult_promotesObject() throws { let env = try JavaVirtualMachine.shared().environment() - let promoted = env.withLocalFramePromotingResult(capacity: 8) { () -> jobject? in + 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 @@ -103,7 +103,7 @@ struct JavaEnvironmentTests { func withLocalFramePromotingResult_nilResult() throws { let env = try JavaVirtualMachine.shared().environment() - let result = env.withLocalFramePromotingResult { + let result = try env.withLocalFramePromotingResult { nil } #expect(result == nil) From f7508a3cb5335c416f508cd92175752e0d3a0f7e Mon Sep 17 00:00:00 2001 From: Konrad Malawski Date: Tue, 17 Mar 2026 15:41:24 +0900 Subject: [PATCH 5/5] missing Bionic import --- Sources/SwiftJavaJNICore/JavaEnvironment+Refs.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Sources/SwiftJavaJNICore/JavaEnvironment+Refs.swift b/Sources/SwiftJavaJNICore/JavaEnvironment+Refs.swift index f3399d3..bd9494a 100644 --- a/Sources/SwiftJavaJNICore/JavaEnvironment+Refs.swift +++ b/Sources/SwiftJavaJNICore/JavaEnvironment+Refs.swift @@ -16,6 +16,8 @@ import Glibc #elseif canImport(Musl) import Musl +#elseif canImport(Bionic) +import Bionic #elseif canImport(Darwin) import Darwin #elseif os(Windows)