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
29 changes: 19 additions & 10 deletions Sources/SwiftJavaJNICore/BridgedValues/JavaValue+Array.swift
Original file line number Diff line number Diff line change
Expand Up @@ -77,29 +77,39 @@ 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.
// 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
)
return jniArray
}

return jniArray
}

public static func jniMethodCall(in environment: JNIEnvironment) -> JNIMethodCall<JNIType> {
Expand Down Expand Up @@ -128,9 +138,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)
}
}

Expand Down
22 changes: 20 additions & 2 deletions Sources/SwiftJavaJNICore/Mangling.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
}
Expand Down
76 changes: 76 additions & 0 deletions Tests/SwiftJavaJNICoreTests/JavaEnvironmentTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -129,4 +129,80 @@ 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)
}

@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]])
}
}
66 changes: 66 additions & 0 deletions Tests/SwiftJavaJNICoreTests/ManglingTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,70 @@ 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)
}
}
Loading