From 7e3ed6ddb84b340736085e84fd088111075d897a Mon Sep 17 00:00:00 2001 From: Konrad Malawski Date: Tue, 7 Apr 2026 16:13:10 +0900 Subject: [PATCH 1/3] Support for nested arrays in Array JavaValue The types and lookups for jniNewArray did not properly handle nested arrays; this corrects that and by handling the special naming expectations this method has: [[[[[[Ljava/lang/String; for nested arrays, however skipping the L if just looking for plain class name --- .../BridgedValues/JavaValue+Array.swift | 20 +++--- Sources/SwiftJavaJNICore/JavaType.swift | 1 + Sources/SwiftJavaJNICore/Mangling.swift | 22 ++++++- .../JavaEnvironmentTests.swift | 52 +++++++++++++++ .../SwiftJavaJNICoreTests/ManglingTests.swift | 64 +++++++++++++++++++ 5 files changed, 148 insertions(+), 11 deletions(-) diff --git a/Sources/SwiftJavaJNICore/BridgedValues/JavaValue+Array.swift b/Sources/SwiftJavaJNICore/BridgedValues/JavaValue+Array.swift index 8a0cb31..68fa6ad 100644 --- a/Sources/SwiftJavaJNICore/BridgedValues/JavaValue+Array.swift +++ b/Sources/SwiftJavaJNICore/BridgedValues/JavaValue+Array.swift @@ -77,16 +77,20 @@ extension Array: JavaValue where Element: JavaValue { @inlinable public func getJNIValue(in environment: JNIEnvironment) -> JNIType { let count = self.count - var jniArray = Element.jniNewArray(in: environment)(environment, Int32(count))! if Element.self == UInt8.self || Element.self == Int8.self { - // Fast path, Since the memory layout of `jbyte`` and those is the same, we rebind the memory - // rather than convert every element independently. This allows us to avoid another Swift array creation. + // Fast path: the memory layout of jbyte and UInt8/Int8 is identical, + // so we rebind the memory rather than convert every element independently + var jniArray = Element.jniNewArray(in: environment)(environment, Int32(count))! self.withUnsafeBytes { buffer in buffer.getJNIValue(into: &jniArray, in: environment) } + return jniArray } else { - // Slow path, convert every element to the apropriate JNIType: + // Slow path, convert every element to the appropriate JNIType. + // Use Self.jniNewArray (not Element.jniNewArray) so that nested arrays + // get the correct outer array type, e.g. [[String]] creates String[][] + let jniArray = Self.jniNewArray(in: environment)(environment, Int32(count))! let jniElementBuffer: [Element.JNIType] = self.map { // meh, temporary array $0.getJNIValue(in: environment) } @@ -97,9 +101,8 @@ extension Array: JavaValue where Element: JavaValue { jsize(self.count), jniElementBuffer ) + return jniArray } - - return jniArray } public static func jniMethodCall(in environment: JNIEnvironment) -> JNIMethodCall { @@ -128,9 +131,8 @@ extension Array: JavaValue where Element: JavaValue { public static func jniNewArray(in environment: JNIEnvironment) -> JNINewArray { { environment, size in - // FIXME: We should have a bridged JavaArray that we can use here. - let arrayClass = environment.interface.FindClass(environment, "java/lang/Array") - return environment.interface.NewObjectArray(environment, size, arrayClass, nil) + let elementClass = environment.interface.FindClass(environment, Element.javaType.jniFindClassName) + return environment.interface.NewObjectArray(environment, size, elementClass, nil) } } diff --git a/Sources/SwiftJavaJNICore/JavaType.swift b/Sources/SwiftJavaJNICore/JavaType.swift index 67f2702..c04397b 100644 --- a/Sources/SwiftJavaJNICore/JavaType.swift +++ b/Sources/SwiftJavaJNICore/JavaType.swift @@ -80,3 +80,4 @@ extension JavaType { } } } + diff --git a/Sources/SwiftJavaJNICore/Mangling.swift b/Sources/SwiftJavaJNICore/Mangling.swift index 0981d63..e07ccbb 100644 --- a/Sources/SwiftJavaJNICore/Mangling.swift +++ b/Sources/SwiftJavaJNICore/Mangling.swift @@ -35,8 +35,26 @@ extension JavaType { case .short: "S" case .void: "V" case .array(let elementType): "[" + elementType.mangledName - case .class(let package, let name, _): - "L\(package!).\(name.replacingPeriodsWithDollars());".replacingPeriodsWithSlashes() + case .class(.some(let package), let name, _): + "L\(package).\(name.replacingPeriodsWithDollars());".replacingPeriodsWithSlashes() + case .class(.none, let name, _): + "L\(name.replacingPeriodsWithDollars());".replacingPeriodsWithSlashes() + } + } + + /// The class name format expected by JNI's ``FindClass`` function. + /// + /// For classes this is the slash-separated fully qualified name (e.g. `"java/lang/String"`), + /// which differs from ``mangledName`` that wraps it in `L...;`. + /// For arrays, the format matches ``mangledName`` (e.g. `"[Ljava/lang/String;"`). + public var jniFindClassName: String { + switch self { + case .class: + let mangled = mangledName + assert(mangled.hasPrefix("L") && mangled.hasSuffix(";")) + return String(mangled.dropFirst().dropLast()) + default: + return mangledName } } } diff --git a/Tests/SwiftJavaJNICoreTests/JavaEnvironmentTests.swift b/Tests/SwiftJavaJNICoreTests/JavaEnvironmentTests.swift index 2c62119..d850895 100644 --- a/Tests/SwiftJavaJNICoreTests/JavaEnvironmentTests.swift +++ b/Tests/SwiftJavaJNICoreTests/JavaEnvironmentTests.swift @@ -129,4 +129,56 @@ struct JavaEnvironmentTests { // Should not crash env.deleteLocalRef(nil) } + + @Test(.enabled(if: isSupportedPlatform)) + func jniNewArray_nestedStringArray() throws { + let env = try JavaVirtualMachine.shared().environment() + + // Use jniNewArray to create a String[][] — this is the function fixed by HEAD + let makeOuter = [[String]].jniNewArray(in: env) + let outer = makeOuter(env, 2) + #expect(outer != nil) + + // Create inner String[] arrays via getJNIValue and store them + let inner0 = ["hello", "world"].getJNIValue(in: env) + let inner1 = ["foo"].getJNIValue(in: env) + + env.interface.SetObjectArrayElement(env, outer, 0, inner0) + env.interface.SetObjectArrayElement(env, outer, 1, inner1) + + // Read back and verify structure + let readInner0 = env.interface.GetObjectArrayElement(env, outer, 0) + #expect(readInner0 != nil) + #expect(env.interface.GetArrayLength(env, readInner0) == 2) + + let readInner1 = env.interface.GetObjectArrayElement(env, outer, 1) + #expect(readInner1 != nil) + #expect(env.interface.GetArrayLength(env, readInner1) == 1) + } + + @Test(.enabled(if: isSupportedPlatform)) + func jniNewArray_tripleNestedStringArray() throws { + let env = try JavaVirtualMachine.shared().environment() + + // String[][][] + let makeOuter = [[[String]]].jniNewArray(in: env) + let outer = makeOuter(env, 1) + #expect(outer != nil) + } + + @Test(.enabled(if: isSupportedPlatform)) + func getJNIValue_nestedStringArray() throws { + let env = try JavaVirtualMachine.shared().environment() + + let jniValue = [["hello", "world"]].getJNIValue(in: env) + #expect(jniValue != nil) + + let outerLen = env.interface.GetArrayLength(env, jniValue) + #expect(outerLen == 1) + + // Verify inner elements are accessible + let inner = env.interface.GetObjectArrayElement(env, jniValue, 0) + #expect(inner != nil) + #expect(env.interface.GetArrayLength(env, inner) == 2) + } } diff --git a/Tests/SwiftJavaJNICoreTests/ManglingTests.swift b/Tests/SwiftJavaJNICoreTests/ManglingTests.swift index 672555c..50c5bc4 100644 --- a/Tests/SwiftJavaJNICoreTests/ManglingTests.swift +++ b/Tests/SwiftJavaJNICoreTests/ManglingTests.swift @@ -34,4 +34,68 @@ struct ManglingTests { #expect(demangledSignature == expectedSignature) #expect(expectedSignature.mangledName == "(ILjava/lang/String;[I)J") } + + // Nested arrays + + @Test( + arguments: [ + // Primitive arrays + (.array(.int), "[I"), + (.array(.long), "[J"), + (.array(.byte), "[B"), + (.array(.boolean), "[Z"), + (.array(.double), "[D"), + + // Object arrays + (.array(.class(package: "java.lang", name: "String")), "[Ljava/lang/String;"), + + // Nested arrays (the fix in jniNewArray relies on these mangled names) + (.array(.array(.int)), "[[I"), + (.array(.array(.long)), "[[J"), + (.array(.array(.class(package: "java.lang", name: "String"))), "[[Ljava/lang/String;"), + (.array(.array(.array(.int))), "[[[I"), + ] as [(JavaType, String)] + ) + func arrayMangling(javaType: JavaType, expectedMangledName: String) throws { + #expect(javaType.mangledName == expectedMangledName) + let roundTripped = try JavaType(mangledName: expectedMangledName) + #expect(roundTripped == javaType) + } + + @Test + func nestedArrayElementMangledName() throws { + let nestedIntArray = JavaType.array(.array(.int)) + let elementType = switch nestedIntArray { + case .array(let element): element + default: fatalError("expected array type") + } + #expect(elementType.mangledName == "[I") + + let nestedStringArray = JavaType.array(.array(.class(package: "java.lang", name: "String"))) + let stringElementType = switch nestedStringArray { + case .array(let element): element + default: fatalError("expected array type") + } + #expect(stringElementType.mangledName == "[Ljava/lang/String;") + } + + @Test( + arguments: [ + // Class types: FindClass needs "java/lang/String", not "Ljava/lang/String;" + (.class(package: "java.lang", name: "String"), "java/lang/String"), + (.class(package: "java.util", name: "List"), "java/util/List"), + + // Array types: FindClass accepts the full type descriptor + (.array(.class(package: "java.lang", name: "String")), "[Ljava/lang/String;"), + (.array(.int), "[I"), + (.array(.array(.class(package: "java.lang", name: "String"))), "[[Ljava/lang/String;"), + + // Primitives (not typically used with FindClass, but should return mangledName) + (.int, "I"), + (.boolean, "Z"), + ] as [(JavaType, String)] + ) + func jniFindClassName(javaType: JavaType, expected: String) { + #expect(javaType.jniFindClassName == expected) + } } From 3a1d8456b0328edd1206ffc1b7a4ba00fbb3564a Mon Sep 17 00:00:00 2001 From: Konrad Malawski Date: Tue, 7 Apr 2026 16:37:57 +0900 Subject: [PATCH 2/3] formatting --- Sources/SwiftJavaJNICore/JavaType.swift | 1 - Tests/SwiftJavaJNICoreTests/ManglingTests.swift | 10 ++++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/Sources/SwiftJavaJNICore/JavaType.swift b/Sources/SwiftJavaJNICore/JavaType.swift index c04397b..67f2702 100644 --- a/Sources/SwiftJavaJNICore/JavaType.swift +++ b/Sources/SwiftJavaJNICore/JavaType.swift @@ -80,4 +80,3 @@ extension JavaType { } } } - diff --git a/Tests/SwiftJavaJNICoreTests/ManglingTests.swift b/Tests/SwiftJavaJNICoreTests/ManglingTests.swift index 50c5bc4..220d9a3 100644 --- a/Tests/SwiftJavaJNICoreTests/ManglingTests.swift +++ b/Tests/SwiftJavaJNICoreTests/ManglingTests.swift @@ -65,17 +65,19 @@ struct ManglingTests { @Test func nestedArrayElementMangledName() throws { let nestedIntArray = JavaType.array(.array(.int)) - let elementType = switch nestedIntArray { + let elementType = + switch nestedIntArray { case .array(let element): element default: fatalError("expected array type") - } + } #expect(elementType.mangledName == "[I") let nestedStringArray = JavaType.array(.array(.class(package: "java.lang", name: "String"))) - let stringElementType = switch nestedStringArray { + let stringElementType = + switch nestedStringArray { case .array(let element): element default: fatalError("expected array type") - } + } #expect(stringElementType.mangledName == "[Ljava/lang/String;") } From a3b3a87a6f73c9b3b9bfb2ea194b73912695d5e7 Mon Sep 17 00:00:00 2001 From: Konrad Malawski Date: Tue, 7 Apr 2026 16:41:51 +0900 Subject: [PATCH 3/3] also handle nested arrays where root is primitive type --- .../BridgedValues/JavaValue+Array.swift | 15 ++++++++---- .../JavaEnvironmentTests.swift | 24 +++++++++++++++++++ 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/Sources/SwiftJavaJNICore/BridgedValues/JavaValue+Array.swift b/Sources/SwiftJavaJNICore/BridgedValues/JavaValue+Array.swift index 68fa6ad..61ab0cf 100644 --- a/Sources/SwiftJavaJNICore/BridgedValues/JavaValue+Array.swift +++ b/Sources/SwiftJavaJNICore/BridgedValues/JavaValue+Array.swift @@ -88,15 +88,22 @@ extension Array: JavaValue where Element: JavaValue { return jniArray } else { // Slow path, convert every element to the appropriate JNIType. - // Use Self.jniNewArray (not Element.jniNewArray) so that nested arrays - // get the correct outer array type, e.g. [[String]] creates String[][] - let jniArray = Self.jniNewArray(in: environment)(environment, Int32(count))! + // For object/array elements, use Self.jniNewArray so that nested arrays + // get the correct outer array type, e.g. [[String]] creates String[][]. + // For primitive elements (Int32, Float, etc.), use Element.jniNewArray + // which dispatches to the correct JNI function (NewIntArray, etc.) + let jniArray: jobject? + if Element.javaType.isPrimitive { + jniArray = Element.jniNewArray(in: environment)(environment, Int32(count)) + } else { + jniArray = Self.jniNewArray(in: environment)(environment, Int32(count)) + } let jniElementBuffer: [Element.JNIType] = self.map { // meh, temporary array $0.getJNIValue(in: environment) } Element.jniSetArrayRegion(in: environment)( environment, - jniArray, + jniArray!, 0, jsize(self.count), jniElementBuffer diff --git a/Tests/SwiftJavaJNICoreTests/JavaEnvironmentTests.swift b/Tests/SwiftJavaJNICoreTests/JavaEnvironmentTests.swift index d850895..69466c2 100644 --- a/Tests/SwiftJavaJNICoreTests/JavaEnvironmentTests.swift +++ b/Tests/SwiftJavaJNICoreTests/JavaEnvironmentTests.swift @@ -181,4 +181,28 @@ struct JavaEnvironmentTests { #expect(inner != nil) #expect(env.interface.GetArrayLength(env, inner) == 2) } + + @Test(.enabled(if: isSupportedPlatform)) + func getJNIValue_nestedInt32Array() throws { + let env = try JavaVirtualMachine.shared().environment() + + let jniValue: jobject? = [[Int32(1), Int32(2), Int32(3)], [Int32(4)]].getJNIValue(in: env) + #expect(jniValue != nil) + + let outerLen = env.interface.GetArrayLength(env, jniValue) + #expect(outerLen == 2) + + // Verify inner arrays are accessible and have correct lengths + let inner0 = env.interface.GetObjectArrayElement(env, jniValue, 0) + #expect(inner0 != nil) + #expect(env.interface.GetArrayLength(env, inner0) == 3) + + let inner1 = env.interface.GetObjectArrayElement(env, jniValue, 1) + #expect(inner1 != nil) + #expect(env.interface.GetArrayLength(env, inner1) == 1) + + // Round-trip: read back inner values + let roundTripped = [[Int32]](fromJNI: jniValue, in: env) + #expect(roundTripped == [[1, 2, 3], [4]]) + } }