From d8b6591335f30c0cbc57bfbef46b602ba73d7dad Mon Sep 17 00:00:00 2001 From: Konrad Malawski Date: Mon, 9 Feb 2026 14:03:59 +0900 Subject: [PATCH 01/14] Initial attempt at implementing Data support for JNI --- .../Sources/MySwiftLibrary/Data.swift | 35 +++++++ .../swift/swiftkit/ffm/FFMDataBenchmark.java | 96 +++++++++++++++++++ .../Sources/MySwiftLibrary/Data.swift | 36 +++++++ .../MySwiftLibrary/MySwiftLibrary.swift | 4 + .../com/example/swift/JNIDataBenchmark.java | 80 ++++++++++++++++ .../test/java/com/example/swift/DataTest.java | 66 +++++++++++++ ...MSwift2JavaGenerator+JavaTranslation.swift | 6 +- .../JNI/JNIJavaTypeTranslator.swift | 23 ++++- ...t2JavaGenerator+JavaBindingsPrinting.swift | 22 +++++ ...ISwift2JavaGenerator+JavaTranslation.swift | 10 +- ...wift2JavaGenerator+NativeTranslation.swift | 4 +- .../SwiftTypes/SwiftKnownModules.swift | 5 +- .../Documentation.docc/SupportedFeatures.md | 28 +++++- .../JExtractSwiftTests/DataImportTests.swift | 75 ++++++++++++++- 14 files changed, 474 insertions(+), 16 deletions(-) create mode 100644 Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/Data.swift create mode 100644 Samples/SwiftJavaExtractFFMSampleApp/src/jmh/java/org/swift/swiftkit/ffm/FFMDataBenchmark.java create mode 100644 Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Data.swift create mode 100644 Samples/SwiftJavaExtractJNISampleApp/src/jmh/java/com/example/swift/JNIDataBenchmark.java create mode 100644 Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/DataTest.java diff --git a/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/Data.swift b/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/Data.swift new file mode 100644 index 000000000..871d7d007 --- /dev/null +++ b/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/Data.swift @@ -0,0 +1,35 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 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(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +public func echoData(_ data: Data) -> Data { + return data +} + +public func makeData() -> Data { + return Data([0x01, 0x02, 0x03, 0x04]) +} + +public func getDataCount(_ data: Data) -> Int { + return data.count +} + +public func compareData(_ data1: Data, _ data2: Data) -> Bool { + return data1 == data2 +} diff --git a/Samples/SwiftJavaExtractFFMSampleApp/src/jmh/java/org/swift/swiftkit/ffm/FFMDataBenchmark.java b/Samples/SwiftJavaExtractFFMSampleApp/src/jmh/java/org/swift/swiftkit/ffm/FFMDataBenchmark.java new file mode 100644 index 000000000..5665aafda --- /dev/null +++ b/Samples/SwiftJavaExtractFFMSampleApp/src/jmh/java/org/swift/swiftkit/ffm/FFMDataBenchmark.java @@ -0,0 +1,96 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 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 +// +//===----------------------------------------------------------------------===// + +package org.swift.swiftkit.ffm; + +import com.example.swift.Data; +import com.example.swift.MySwiftLibrary; +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.infra.Blackhole; + +import java.util.concurrent.TimeUnit; + +@BenchmarkMode(Mode.AverageTime) +@Warmup(iterations = 5, time = 200, timeUnit = TimeUnit.MILLISECONDS) +@Measurement(iterations = 10, time = 500, timeUnit = TimeUnit.MILLISECONDS) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@State(Scope.Benchmark) +@Fork(value = 3, jvmArgsAppend = { "--enable-native-access=ALL-UNNAMED" }) +public class FFMDataBenchmark { + + @Param({"4", "100", "1000"}) + public int dataSize; + + ClosableAllocatingSwiftArena arena; + Data data; + + @Setup(Level.Trial) + public void beforeAll() { + arena = AllocatingSwiftArena.ofConfined(); + // FFM mode has Data.init(byte[], arena) directly + data = Data.init(makeBytes(dataSize), arena); + } + + @TearDown(Level.Trial) + public void afterAll() { + arena.close(); + } + + private static byte[] makeBytes(int size) { + byte[] bytes = new byte[size]; + for (int i = 0; i < size; i++) { + bytes[i] = (byte) (i % 256); + } + return bytes; + } + + /** + * Baseline: simple int echo to measure FFM call overhead. + */ + @Benchmark + public long ffm_baseline_globalEchoInt() { + return MySwiftLibrary.globalEchoInt(13); + } + + /** + * Benchmark passing Data to Swift and getting count back. + * Measures: Java → Swift data passing overhead. + */ + @Benchmark + public long ffm_passDataToSwift() { + return MySwiftLibrary.getDataCount(data); + } + + /** + * Benchmark receiving Data from Swift (makeData returns 4 bytes). + * Measures: Swift → Java data return overhead. + */ + @Benchmark + public Data ffm_receiveDataFromSwift(Blackhole bh) { + Data result = MySwiftLibrary.makeData(arena); + bh.consume(result.getCount()); + return result; + } + + /** + * Benchmark echo: send Data to Swift and receive it back. + * Measures: Full round-trip overhead. + */ + @Benchmark + public Data ffm_echoData(Blackhole bh) { + Data echoed = MySwiftLibrary.echoData(data, arena); + bh.consume(echoed.getCount()); + return echoed; + } +} diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Data.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Data.swift new file mode 100644 index 000000000..d04b30775 --- /dev/null +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Data.swift @@ -0,0 +1,36 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 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(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif +import SwiftJava + +public func echoData(_ data: Data) -> Data { + return data +} + +public func makeData() -> Data { + return Data([0x01, 0x02, 0x03, 0x04]) +} + +public func getDataCount(_ data: Data) -> Int { + return data.count +} + +public func compareData(_ data1: Data, _ data2: Data) -> Bool { + return data1 == data2 +} diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftLibrary.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftLibrary.swift index b362879fc..975748eaa 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftLibrary.swift +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftLibrary.swift @@ -35,6 +35,10 @@ public func globalTakeInt(i: Int64) { p("i:\(i)") } +public func globalEchoInt(i: Int64) -> Int64{ + i +} + public func globalMakeInt() -> Int64 { return 42 } diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/jmh/java/com/example/swift/JNIDataBenchmark.java b/Samples/SwiftJavaExtractJNISampleApp/src/jmh/java/com/example/swift/JNIDataBenchmark.java new file mode 100644 index 000000000..86a3f2312 --- /dev/null +++ b/Samples/SwiftJavaExtractJNISampleApp/src/jmh/java/com/example/swift/JNIDataBenchmark.java @@ -0,0 +1,80 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 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 +// +//===----------------------------------------------------------------------===// + +package com.example.swift; + +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.infra.Blackhole; +import org.swift.swiftkit.core.ClosableSwiftArena; +import org.swift.swiftkit.core.SwiftArena; + +import java.util.concurrent.TimeUnit; + +@BenchmarkMode(Mode.AverageTime) +@Warmup(iterations = 5, time = 200, timeUnit = TimeUnit.MILLISECONDS) +@Measurement(iterations = 10, time = 500, timeUnit = TimeUnit.MILLISECONDS) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@State(Scope.Benchmark) +@Fork(value = 3, jvmArgsAppend = { "--enable-native-access=ALL-UNNAMED" }) +public class JNIDataBenchmark { + + @Param({"4", "100", "1000"}) + public int dataSize; + + ClosableSwiftArena arena; + Data data; + + @Setup(Level.Trial) + public void beforeAll() { + arena = SwiftArena.ofConfined(); + data = Data.fromByteArray(makeBytes(dataSize), arena); + } + + @TearDown(Level.Trial) + public void afterAll() { + arena.close(); + } + + private static byte[] makeBytes(int size) { + byte[] bytes = new byte[size]; + for (int i = 0; i < size; i++) { + bytes[i] = (byte) (i % 256); + } + return bytes; + } + + @Benchmark + public long jni_baseline_globalEchoInt() { + return MySwiftLibrary.globalEchoInt(13); + } + + @Benchmark + public long jni_passDataToSwift() { + return MySwiftLibrary.getDataCount(data); + } + + @Benchmark + public Data jni_receiveDataFromSwift(Blackhole bh) { + Data result = MySwiftLibrary.makeData(arena); + bh.consume(result.getCount()); + return result; + } + + @Benchmark + public Data jni_echoData(Blackhole bh) { + Data echoed = MySwiftLibrary.echoData(data, arena); + bh.consume(echoed.getCount()); + return echoed; + } +} diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/DataTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/DataTest.java new file mode 100644 index 000000000..7add4b9a8 --- /dev/null +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/DataTest.java @@ -0,0 +1,66 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 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 +// +//===----------------------------------------------------------------------===// + +package com.example.swift; + +import org.junit.jupiter.api.Test; +import org.swift.swiftkit.core.SwiftArena; + +import static org.junit.jupiter.api.Assertions.*; + +public class DataTest { + @Test + void data_echo() { + try (var arena = SwiftArena.ofConfined()) { + byte[] bytes = new byte[] { 1, 2, 3, 4 }; + var data = Data.fromByteArray(bytes, arena); + + var echoed = MySwiftLibrary.echoData(data, arena); + assertEquals(4, echoed.getCount()); + } + } + + @Test + void data_make() { + try (var arena = SwiftArena.ofConfined()) { + var data = MySwiftLibrary.makeData(arena); + assertEquals(4, data.getCount()); + } + } + + @Test + void data_getCount() { + try (var arena = SwiftArena.ofConfined()) { + byte[] bytes = new byte[] { 1, 2, 3, 4, 5 }; + var data = Data.fromByteArray(bytes, arena); + assertEquals(5, MySwiftLibrary.getDataCount(data)); + } + } + + @Test + void data_compare() { + try (var arena = SwiftArena.ofConfined()) { + byte[] bytes1 = new byte[] { 1, 2, 3 }; + byte[] bytes2 = new byte[] { 1, 2, 3 }; + byte[] bytes3 = new byte[] { 1, 2, 4 }; + + var data1 = Data.fromByteArray(bytes1, arena); + var data2 = Data.fromByteArray(bytes2, arena); + var data3 = Data.fromByteArray(bytes3, arena); + + assertTrue(MySwiftLibrary.compareData(data1, data2)); + assertFalse(MySwiftLibrary.compareData(data1, data3)); + } + } +} diff --git a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift index da1914d45..e8a28d064 100644 --- a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift +++ b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift @@ -538,7 +538,9 @@ extension FFMSwift2JavaGenerator { case .nominal(let nominal): if let knownType = nominal.nominalTypeDecl.knownTypeKind { switch knownType { - case .foundationData, .foundationDataProtocol, .essentialsData, .essentialsDataProtocol: + case .foundationData, .foundationDataProtocol: + break + case .essentialsData, .essentialsDataProtocol: break default: throw JavaTranslationError.unhandledType(.optional(swiftType)) @@ -637,7 +639,7 @@ extension FFMSwift2JavaGenerator { ) case .foundationData, .essentialsData: - break + break // Implemented as wrapper case .unsafePointer, .unsafeMutablePointer: // FIXME: Implement diff --git a/Sources/JExtractSwiftLib/JNI/JNIJavaTypeTranslator.swift b/Sources/JExtractSwiftLib/JNI/JNIJavaTypeTranslator.swift index bde9afbd1..568332c0f 100644 --- a/Sources/JExtractSwiftLib/JNI/JNIJavaTypeTranslator.swift +++ b/Sources/JExtractSwiftLib/JNI/JNIJavaTypeTranslator.swift @@ -45,7 +45,12 @@ enum JNIJavaTypeTranslator { .unsafePointer, .unsafeMutablePointer, .unsafeRawBufferPointer, .unsafeMutableRawBufferPointer, .unsafeBufferPointer, .unsafeMutableBufferPointer, - .optional, .foundationData, .foundationDataProtocol, .essentialsData, .essentialsDataProtocol, .array, .foundationDate, .essentialsDate, .foundationUUID, .essentialsUUID: + .optional, + .foundationData, .foundationDataProtocol, + .essentialsData, .essentialsDataProtocol, + .array, + .foundationDate, .essentialsDate, + .foundationUUID, .essentialsUUID: return nil } } @@ -61,8 +66,12 @@ enum JNIJavaTypeTranslator { .unsafePointer, .unsafeMutablePointer, .unsafeRawBufferPointer, .unsafeMutableRawBufferPointer, .unsafeBufferPointer, .unsafeMutableBufferPointer, - .optional, .foundationData, .foundationDataProtocol, .essentialsData, .essentialsDataProtocol, - .array, .foundationDate, .essentialsDate, .foundationUUID, .essentialsUUID: + .optional, + .foundationData, .foundationDataProtocol, + .essentialsData, .essentialsDataProtocol, + .array, + .foundationDate, .essentialsDate, + .foundationUUID, .essentialsUUID: nil } } @@ -78,8 +87,12 @@ enum JNIJavaTypeTranslator { .unsafePointer, .unsafeMutablePointer, .unsafeRawBufferPointer, .unsafeMutableRawBufferPointer, .unsafeBufferPointer, .unsafeMutableBufferPointer, - .optional, .foundationData, .foundationDataProtocol, .essentialsData, .essentialsDataProtocol, - .array, .foundationDate, .essentialsDate, .foundationUUID, .essentialsUUID: + .optional, + .foundationData, .foundationDataProtocol, + .essentialsData, .essentialsDataProtocol, + .array, + .foundationDate, .essentialsDate, + .foundationUUID, .essentialsUUID: nil } } diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift index 14b15217e..de31ffb95 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift @@ -281,6 +281,9 @@ extension JNISwift2JavaGenerator { case .foundationDate, .essentialsDate: printFoundationDateHelpers(&printer, decl) + case .foundationData, .essentialsData: + printFoundationDataHelpers(&printer, decl) + default: break } @@ -756,4 +759,23 @@ extension JNISwift2JavaGenerator { """ ) } + + private func printFoundationDataHelpers(_ printer: inout CodePrinter, _ decl: ImportedNominalType) { + printer.print( + """ + /** + * Creates a new Data instance from a byte array. + * + * @param bytes The byte array to copy into the Data + * @param swiftArena$ The arena for memory management + * @return A new Data instance containing a copy of the bytes + */ + public static Data fromByteArray(byte[] bytes, SwiftArena swiftArena$) { + Objects.requireNonNull(bytes, "bytes cannot be null"); + return Data.init(bytes, swiftArena$); + } + + """ + ) + } } diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift index 1c791df05..984b53fca 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift @@ -406,8 +406,10 @@ extension JNISwift2JavaGenerator { ) case .foundationDate, .essentialsDate: - // Handled as wrapped struct - break + break // Handled as wrapped struct + + case .foundationData, .essentialsData: + break // Handled as wrapped struct case .foundationUUID, .essentialsUUID: return TranslatedParameter( @@ -701,6 +703,10 @@ extension JNISwift2JavaGenerator { // Handled as wrapped struct break + case .foundationData, .essentialsData: + // Handled as wrapped struct + break + case .foundationUUID, .essentialsUUID: return TranslatedResult( javaType: .javaUtilUUID, diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift index 5be38bad8..0b00094fc 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift @@ -98,7 +98,7 @@ extension JNISwift2JavaGenerator { } return try translateArrayParameter(elementType: elementType, parameterName: parameterName) - case .foundationDate, .essentialsDate: + case .foundationDate, .essentialsDate, .foundationData, .essentialsData: // Handled as wrapped struct break @@ -538,7 +538,7 @@ extension JNISwift2JavaGenerator { } return try translateArrayResult(elementType: elementType, resultName: resultName) - case .foundationDate, .essentialsDate: + case .foundationDate, .essentialsDate, .foundationData, .essentialsData: // Handled as wrapped struct break diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownModules.swift b/Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownModules.swift index 8cc4bb449..48fbe4138 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownModules.swift +++ b/Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownModules.swift @@ -107,13 +107,14 @@ private let swiftSourceFile: SourceFileSyntax = """ private let foundationEssentialsSourceFile: SourceFileSyntax = """ public protocol DataProtocol {} - + public struct Data: DataProtocol { public init(bytes: UnsafeRawPointer, count: Int) + public init(_ bytes: [UInt8]) public var count: Int { get } public func withUnsafeBytes(_ body: (UnsafeRawBufferPointer) -> Void) } - + public struct Date { /// The interval between the date object and 00:00:00 UTC on 1 January 1970. public var timeIntervalSince1970: Double { get } diff --git a/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md b/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md index d36be9a48..19ee87bb5 100644 --- a/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md +++ b/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md @@ -68,7 +68,7 @@ SwiftJava's `swift-java jextract` tool automates generating Java bindings from S | Existential parameters `f(x: any SomeProtocol)` (excepts `Any`) | ❌ | ✅ | | Existential parameters `f(x: any (A & B)) ` | ❌ | ✅ | | Existential return types `f() -> any Collection` | ❌ | ❌ | -| Foundation Data and DataProtocol: `f(x: any DataProtocol) -> Data` | ✅ | ❌ | +| Foundation Data and DataProtocol: `f(x: any DataProtocol) -> Data` | ✅ | ✅ | | Foundation Date: `f(date: Date) -> Date` | ❌ | ✅ | | Foundation UUID: `f(uuid: UUID) -> UUID` | ❌ | ✅ | | Opaque parameters: `func take(worker: some Builder) -> some Builder` | ❌ | ✅ | @@ -135,6 +135,30 @@ on the Java side. | `Float` | `float` | | `Double` | `double` | +### Passing Foundation.Data + +`Data` is a common currency type in Swift for passing a bag of bytes. Some APIs use Data instead of `[UInt8]` or other types +like Swift-NIO's `ByteBuffer`, because it is so commonly used swift-java offers specialized support for it in order to avoid copying bytes unless necessary. + +### Data in jextract FFM mode + +When using jextract in FFM mode, the generated `Data` wrapper offers an efficient way to initialize the Swift `Data` type +from a `MemorySegment` as well as the `withUnsafeBytes` function which offers direct access to Data's underlying bytes +by exposing the unsafe base pointer as a `MemorySegment`: + +```swift +Data data = MySwiftLibrary.getSomeData(arena); +data.withUnsafeBytes((bytes) -> { + var str = bytes.getString(0); + System.out.println("string = " + str); +}); +``` + +This API avoids copying the data into the Java heap in order to perform operations on it, as we are able to manipulate +it directly thanks to the exposed `MemorySegment`. + +### Data in jextract JNI mode + ### Enums > Note: Enums are currently only supported in JNI mode. @@ -281,7 +305,7 @@ interface Named extends JNISwiftInstance { } ``` -#### Parameters +#### Protocol types in parameters Any opaque, existential or generic parameters are imported as Java generics. This means that the following function: ```swift diff --git a/Tests/JExtractSwiftTests/DataImportTests.swift b/Tests/JExtractSwiftTests/DataImportTests.swift index bad4174db..39dd84654 100644 --- a/Tests/JExtractSwiftTests/DataImportTests.swift +++ b/Tests/JExtractSwiftTests/DataImportTests.swift @@ -449,5 +449,78 @@ final class DataImportTests { ] ) } - + + // MARK: - JNI Mode Tests + + @Test("Import Data: JNI accept Data") + func data_jni_accept() throws { + let text = """ + import Foundation + public func acceptData(data: Data) + """ + + try assertOutput( + input: text, .jni, .java, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + public static void acceptData(Data data) { + SwiftModule.$acceptData(data.$memoryAddress()); + } + """ + ]) + + try assertOutput( + input: text, .jni, .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + @_cdecl("Java_com_example_swift_SwiftModule__00024acceptData__J") + public func Java_com_example_swift_SwiftModule__00024acceptData__J(environment: UnsafeMutablePointer!, thisClass: jclass, data: jlong) { + """ + ]) + } + + @Test("Import Data: JNI return Data") + func data_jni_return() throws { + let text = """ + import Foundation + public func returnData() -> Data + """ + + try assertOutput( + input: text, .jni, .java, + expectedChunks: [ + """ + public static Data returnData(SwiftArena swiftArena$) { + """ + ]) + + try assertOutput( + input: text, .jni, .swift, + expectedChunks: [ + """ + @_cdecl("Java_com_example_swift_SwiftModule__00024returnData__") + public func Java_com_example_swift_SwiftModule__00024returnData__(environment: UnsafeMutablePointer!, thisClass: jclass) -> jlong { + """ + ]) + } + + @Test("Import Data: JNI Data class") + func data_jni_class() throws { + let text = """ + import Foundation + public func f() -> Data + """ + + try assertOutput( + input: text, .jni, .java, + detectChunkByInitialLines: 1, + expectedChunks: [ + "public final class Data implements JNISwiftInstance, DataProtocol {", + "public long getCount() {", + "public static Data fromByteArray(byte[] bytes, SwiftArena swiftArena$) {" + ]) + } + } From b28137672bb41f778adf0407e130151ecdd46036 Mon Sep 17 00:00:00 2001 From: Konrad Malawski Date: Mon, 9 Feb 2026 14:22:48 +0900 Subject: [PATCH 02/14] jni: added basic toByteArray that does a naive double copy --- .../test/java/com/example/swift/DataTest.java | 39 ++++++++++++++++++ ...t2JavaGenerator+JavaBindingsPrinting.swift | 20 +++++++++- ...ift2JavaGenerator+SwiftThunkPrinting.swift | 40 +++++++++++++++++++ .../SwiftRuntimeFunctions.swift | 16 ++++---- .../JExtractSwiftTests/DataImportTests.swift | 4 +- 5 files changed, 109 insertions(+), 10 deletions(-) diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/DataTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/DataTest.java index 7add4b9a8..017fb7b34 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/DataTest.java +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/DataTest.java @@ -63,4 +63,43 @@ void data_compare() { assertFalse(MySwiftLibrary.compareData(data1, data3)); } } + + @Test + void data_toByteArray() { + try (var arena = SwiftArena.ofConfined()) { + byte[] original = new byte[] { 10, 20, 30, 40 }; + var data = Data.fromByteArray(original, arena); + + byte[] result = data.toByteArray(); + + assertEquals(original.length, result.length); + assertArrayEquals(original, result); + } + } + + @Test + void data_toByteArray_empty() { + try (var arena = SwiftArena.ofConfined()) { + byte[] original = new byte[] {}; + var data = Data.fromByteArray(original, arena); + + byte[] result = data.toByteArray(); + + assertArrayEquals(original, result); + assertEquals(0, result.length); + } + } + + @Test + void data_roundTrip() { + try (var arena = SwiftArena.ofConfined()) { + byte[] original = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8 }; + var data = Data.fromByteArray(original, arena); + var echoed = MySwiftLibrary.echoData(data, arena); + + byte[] result = echoed.toByteArray(); + + assertArrayEquals(original, result); + } + } } diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift index de31ffb95..131acf134 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift @@ -764,7 +764,7 @@ extension JNISwift2JavaGenerator { printer.print( """ /** - * Creates a new Data instance from a byte array. + * Creates a new Swift @{link Data} instance from a byte array. * * @param bytes The byte array to copy into the Data * @param swiftArena$ The arena for memory management @@ -774,7 +774,25 @@ extension JNISwift2JavaGenerator { Objects.requireNonNull(bytes, "bytes cannot be null"); return Data.init(bytes, swiftArena$); } + """ + ) + + printer.print( + """ + /** + * Copies the contents of this Data to a new byte array. + * + *

Note: This operation copies the bytes from Swift memory + * to the Java heap. For large Data objects, consider the performance + * implications.

+ * + * @return A byte array containing a copy of this Data's bytes + */ + public byte[] toByteArray() { + return $toByteArray(this.$memoryAddress()); + } + private static native byte[] $toByteArray(long selfPointer); """ ) } diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift index dd82f82d9..27f658467 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift @@ -253,6 +253,7 @@ extension JNISwift2JavaGenerator { } printToStringMethods(&printer, type) + printSpecificTypeThunks(&printer, type) printTypeMetadataAddressThunk(&printer, type) printer.println() printDestroyFunctionThunk(&printer, type) @@ -791,6 +792,45 @@ extension JNISwift2JavaGenerator { } } + /// Prints thunks for specific known types like Foundation.Date, Foundation.Data + private func printSpecificTypeThunks(_ printer: inout CodePrinter, _ type: ImportedNominalType) { + guard let knownType = type.swiftNominal.knownTypeKind else { return } + + switch knownType { + case .foundationData, .essentialsData: + printFoundationDataThunks(&printer, type) + printer.println() + + default: + break + } + } + + /// Prints Swift thunks for Foundation.Data helper methods + private func printFoundationDataThunks(_ printer: inout CodePrinter, _ type: ImportedNominalType) { + let selfPointerParam = JavaParameter(name: "selfPointer", type: .long) + let parentName = type.qualifiedName + + printCDecl( + &printer, + javaMethodName: "$toByteArray", + parentName: type.swiftNominal.qualifiedName, + parameters: [ + selfPointerParam + ], + resultType: .array(.byte) + ) { printer in + let selfVar = self.printSelfJLongToUnsafeMutablePointer(&printer, swiftParentName: parentName, selfPointerParam) + + printer.print( + """ + // TODO: This is a double copy, we need to initialize the array and then copy into a JVM array in getJNIValue + return [UInt8](\(selfVar).pointee).getJNIValue(in: environment) + """ + ) + } + } + /// Print the necessary conversion logic to go from a `jlong` to a `UnsafeMutablePointer` /// /// - Returns: name of the created "self" variable diff --git a/Sources/SwiftRuntimeFunctions/SwiftRuntimeFunctions.swift b/Sources/SwiftRuntimeFunctions/SwiftRuntimeFunctions.swift index fb9781630..577617077 100644 --- a/Sources/SwiftRuntimeFunctions/SwiftRuntimeFunctions.swift +++ b/Sources/SwiftRuntimeFunctions/SwiftRuntimeFunctions.swift @@ -33,12 +33,12 @@ public func _swiftjava_swift_retainCount(object: UnsafeMutableRawPointer) -> Int public func _swiftjava_swift_isUniquelyReferenced(object: UnsafeMutableRawPointer) -> Bool - @_alwaysEmitIntoClient @_transparent +@_alwaysEmitIntoClient @_transparent func _swiftjava_withHeapObject( - of object: AnyObject, - _ body: (UnsafeMutableRawPointer) -> R - ) -> R { - defer { _fixLifetime(object) } - let unmanaged = Unmanaged.passUnretained(object) - return body(unmanaged.toOpaque()) - } + of object: AnyObject, + _ body: (UnsafeMutableRawPointer) -> R +) -> R { + defer { _fixLifetime(object) } + let unmanaged = Unmanaged.passUnretained(object) + return body(unmanaged.toOpaque()) +} diff --git a/Tests/JExtractSwiftTests/DataImportTests.swift b/Tests/JExtractSwiftTests/DataImportTests.swift index 39dd84654..fb8d16daa 100644 --- a/Tests/JExtractSwiftTests/DataImportTests.swift +++ b/Tests/JExtractSwiftTests/DataImportTests.swift @@ -519,7 +519,9 @@ final class DataImportTests { expectedChunks: [ "public final class Data implements JNISwiftInstance, DataProtocol {", "public long getCount() {", - "public static Data fromByteArray(byte[] bytes, SwiftArena swiftArena$) {" + "public static Data fromByteArray(byte[] bytes, SwiftArena swiftArena$) {", + "public byte[] toByteArray() {", + "private static native byte[] $toByteArray(long selfPointer);" ]) } From fd0d7324aadfb30fb69a2f013a4355d1462e1222 Mon Sep 17 00:00:00 2001 From: Konrad Malawski Date: Mon, 9 Feb 2026 14:50:56 +0900 Subject: [PATCH 03/14] add another version of the toByteArray method, less copying? --- .../com/example/swift/JNIDataBenchmark.java | 12 ++++++++ .../test/java/com/example/swift/DataTest.java | 12 ++++++++ ...t2JavaGenerator+JavaBindingsPrinting.swift | 15 ++++++++++ ...ift2JavaGenerator+SwiftThunkPrinting.swift | 29 +++++++++++++++++++ .../BridgedValues/JavaValue+Array.swift | 1 + .../JExtractSwiftTests/DataImportTests.swift | 4 ++- 6 files changed, 72 insertions(+), 1 deletion(-) diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/jmh/java/com/example/swift/JNIDataBenchmark.java b/Samples/SwiftJavaExtractJNISampleApp/src/jmh/java/com/example/swift/JNIDataBenchmark.java index 86a3f2312..15b62474f 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/src/jmh/java/com/example/swift/JNIDataBenchmark.java +++ b/Samples/SwiftJavaExtractJNISampleApp/src/jmh/java/com/example/swift/JNIDataBenchmark.java @@ -21,6 +21,8 @@ import java.util.concurrent.TimeUnit; +import javax.xml.crypto.Data; + @BenchmarkMode(Mode.AverageTime) @Warmup(iterations = 5, time = 200, timeUnit = TimeUnit.MILLISECONDS) @Measurement(iterations = 10, time = 500, timeUnit = TimeUnit.MILLISECONDS) @@ -64,6 +66,16 @@ public long jni_passDataToSwift() { return MySwiftLibrary.getDataCount(data); } + @Benchmark + public byte[] jni_data_toByteArray() { + return data.toByteArray(); + } + + @Benchmark + public byte[] jni_data_toByteArrayLessCopy() { + return data.toByteArrayLessCopy(); + } + @Benchmark public Data jni_receiveDataFromSwift(Blackhole bh) { Data result = MySwiftLibrary.makeData(arena); diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/DataTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/DataTest.java index 017fb7b34..26319e698 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/DataTest.java +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/DataTest.java @@ -99,6 +99,18 @@ void data_roundTrip() { byte[] result = echoed.toByteArray(); + assertArrayEquals(original, result); + } + } + @Test + void data_toByteArrayLessCopy_roundTrip() { + try (var arena = SwiftArena.ofConfined()) { + byte[] original = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8 }; + var data = Data.fromByteArray(original, arena); + var echoed = MySwiftLibrary.echoData(data, arena); + + byte[] result = echoed.toByteArrayLessCopy(); + assertArrayEquals(original, result); } } diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift index 131acf134..f4e5990ba 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift @@ -793,6 +793,21 @@ extension JNISwift2JavaGenerator { } private static native byte[] $toByteArray(long selfPointer); + + /** + * Copies the contents of this Data to a new byte array with fewer intermediate copies. + * + *

Note: This operation copies the bytes from Swift memory + * to the Java heap. For large Data objects, consider the performance + * implications.

+ * + * @return A byte array containing a copy of this Data's bytes + */ + public byte[] toByteArrayLessCopy() { + return $toByteArrayLessCopy(this.$memoryAddress()); + } + + private static native byte[] $toByteArrayLessCopy(long selfPointer); """ ) } diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift index 27f658467..1b79a177a 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift @@ -829,6 +829,35 @@ extension JNISwift2JavaGenerator { """ ) } + + printCDecl( + &printer, + javaMethodName: "$toByteArrayLessCopy", + parentName: type.swiftNominal.qualifiedName, + parameters: [ + selfPointerParam + ], + resultType: .array(.byte) + ) { printer in + let selfVar = self.printSelfJLongToUnsafeMutablePointer(&printer, swiftParentName: parentName, selfPointerParam) + + printer.print( + """ + let jniArray = UInt8.jniNewArray(in: environment)(environment, Int32(\(selfVar).pointee.count))! + let jniElementBuffer: [UInt8.JNIType] = \(selfVar).pointee.map { + $0.getJNIValue(in: environment) + } + UInt8.jniSetArrayRegion(in: environment)( + environment, + jniArray, + 0, + jsize(\(selfVar).pointee.count), + jniElementBuffer + ) + return jniArray + """ + ) + } } /// Print the necessary conversion logic to go from a `jlong` to a `UnsafeMutablePointer` diff --git a/Sources/SwiftJava/BridgedValues/JavaValue+Array.swift b/Sources/SwiftJava/BridgedValues/JavaValue+Array.swift index 3db307891..3ff835106 100644 --- a/Sources/SwiftJava/BridgedValues/JavaValue+Array.swift +++ b/Sources/SwiftJava/BridgedValues/JavaValue+Array.swift @@ -48,6 +48,7 @@ extension Array: JavaValue where Element: JavaValue { self = jniArray.map { Element(fromJNI: $0, in: environment) } } + @inlinable public func getJNIValue(in environment: JNIEnvironment) -> JNIType { // FIXME: If we have a 1:1 match between the Java layout and the // Swift layout, as we do for integer/float types, we can do some diff --git a/Tests/JExtractSwiftTests/DataImportTests.swift b/Tests/JExtractSwiftTests/DataImportTests.swift index fb8d16daa..dc7c54732 100644 --- a/Tests/JExtractSwiftTests/DataImportTests.swift +++ b/Tests/JExtractSwiftTests/DataImportTests.swift @@ -521,7 +521,9 @@ final class DataImportTests { "public long getCount() {", "public static Data fromByteArray(byte[] bytes, SwiftArena swiftArena$) {", "public byte[] toByteArray() {", - "private static native byte[] $toByteArray(long selfPointer);" + "private static native byte[] $toByteArray(long selfPointer);", + "public byte[] toByteArrayLessCopy() {", + "private static native byte[] $toByteArrayLessCopy(long selfPointer);" ]) } From a7d110b4452eea72d0052acbe545bc7af9446bea Mon Sep 17 00:00:00 2001 From: Konrad Malawski Date: Mon, 9 Feb 2026 15:58:34 +0900 Subject: [PATCH 04/14] ignore BuildLogic/.kotlin/ --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 02563c8e2..01dc9f697 100644 --- a/.gitignore +++ b/.gitignore @@ -49,3 +49,5 @@ Package.resolved */**/*.swiftdeps~ */**/.docc-build/ + +BuildLogic/.kotlin/ From 3cc505884f33203c1a599cdfc0d6675dc1482a0c Mon Sep 17 00:00:00 2001 From: Konrad Malawski Date: Mon, 9 Feb 2026 15:58:43 +0900 Subject: [PATCH 05/14] Improve Data -> byte[] implementation in JNI mode --- .../com/example/swift/JNIDataBenchmark.java | 23 ++++++++-- .../test/java/com/example/swift/DataTest.java | 39 ++++++++++++++++ ...t2JavaGenerator+JavaBindingsPrinting.swift | 14 ++++++ ...ift2JavaGenerator+SwiftThunkPrinting.swift | 21 +++++++++ .../BridgedValues/JavaValue+Array.swift | 44 ++++++++++++++----- .../UnsafeRawBufferPointer+getJNIValue.swift | 39 ++++++++++++++++ .../JExtractSwiftTests/DataImportTests.swift | 4 +- 7 files changed, 168 insertions(+), 16 deletions(-) create mode 100644 Sources/SwiftJava/BridgedValues/UnsafeRawBufferPointer+getJNIValue.swift diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/jmh/java/com/example/swift/JNIDataBenchmark.java b/Samples/SwiftJavaExtractJNISampleApp/src/jmh/java/com/example/swift/JNIDataBenchmark.java index 15b62474f..427ccd50e 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/src/jmh/java/com/example/swift/JNIDataBenchmark.java +++ b/Samples/SwiftJavaExtractJNISampleApp/src/jmh/java/com/example/swift/JNIDataBenchmark.java @@ -21,8 +21,6 @@ import java.util.concurrent.TimeUnit; -import javax.xml.crypto.Data; - @BenchmarkMode(Mode.AverageTime) @Warmup(iterations = 5, time = 200, timeUnit = TimeUnit.MILLISECONDS) @Measurement(iterations = 10, time = 500, timeUnit = TimeUnit.MILLISECONDS) @@ -60,22 +58,41 @@ private static byte[] makeBytes(int size) { public long jni_baseline_globalEchoInt() { return MySwiftLibrary.globalEchoInt(13); } - + @Benchmark public long jni_passDataToSwift() { return MySwiftLibrary.getDataCount(data); } + /** + * toByteArray: Creates [UInt8] from Data, then copies to JVM array. + * This involves two copies: Data -> Swift Array -> JVM Array. + */ @Benchmark public byte[] jni_data_toByteArray() { return data.toByteArray(); } + /** + * toByteArrayLessCopy: Creates JVM array, maps Data bytes to buffer, + * then uses SetByteArrayRegion. + * Still creates an intermediate Swift array via map(). + */ @Benchmark public byte[] jni_data_toByteArrayLessCopy() { return data.toByteArrayLessCopy(); } + /** + * toByteArrayDirect: Uses withUnsafeBytes to pass Data's memory + * directly to SetByteArrayRegion. + * No intermediate Swift array allocation - just one copy to JVM. + */ + @Benchmark + public byte[] jni_data_toByteArrayDirect() { + return data.toByteArrayDirect(); + } + @Benchmark public Data jni_receiveDataFromSwift(Blackhole bh) { Data result = MySwiftLibrary.makeData(arena); diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/DataTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/DataTest.java index 26319e698..3e1e53a0a 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/DataTest.java +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/DataTest.java @@ -77,6 +77,32 @@ void data_toByteArray() { } } + @Test + void data_toByteArrayLessCopy() { + try (var arena = SwiftArena.ofConfined()) { + byte[] original = new byte[] { 10, 20, 30, 40 }; + var data = Data.fromByteArray(original, arena); + + byte[] result = data.toByteArrayLessCopy(); + + assertEquals(original.length, result.length); + assertArrayEquals(original, result); + } + } + + @Test + void data_toByteArrayDirect() { + try (var arena = SwiftArena.ofConfined()) { + byte[] original = new byte[] { 10, 20, 30, 40 }; + var data = Data.fromByteArray(original, arena); + + byte[] result = data.toByteArrayDirect(); + + assertEquals(original.length, result.length); + assertArrayEquals(original, result); + } + } + @Test void data_toByteArray_empty() { try (var arena = SwiftArena.ofConfined()) { @@ -114,4 +140,17 @@ void data_toByteArrayLessCopy_roundTrip() { assertArrayEquals(original, result); } } + + @Test + void data_toByteArrayDirect_roundTrip() { + try (var arena = SwiftArena.ofConfined()) { + byte[] original = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8 }; + var data = Data.fromByteArray(original, arena); + var echoed = MySwiftLibrary.echoData(data, arena); + + byte[] result = echoed.toByteArrayDirect(); + + assertArrayEquals(original, result); + } + } } diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift index f4e5990ba..1086415f4 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift @@ -808,6 +808,20 @@ extension JNISwift2JavaGenerator { } private static native byte[] $toByteArrayLessCopy(long selfPointer); + + /** + * Copies the contents of this Data to a new byte array using direct memory access. + * + *

This is the most efficient implementation as it uses {@code withUnsafeBytes} + * to pass Data's memory directly to JNI without any intermediate Swift allocations.

+ * + * @return A byte array containing a copy of this Data's bytes + */ + public byte[] toByteArrayDirect() { + return $toByteArrayDirect(this.$memoryAddress()); + } + + private static native byte[] $toByteArrayDirect(long selfPointer); """ ) } diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift index 1b79a177a..c5e44b292 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift @@ -858,6 +858,27 @@ extension JNISwift2JavaGenerator { """ ) } + + // Zero-copy version: uses withUnsafeBytes to pass Data's memory directly to JNI + printCDecl( + &printer, + javaMethodName: "$toByteArrayDirect", + parentName: type.swiftNominal.qualifiedName, + parameters: [ + selfPointerParam + ], + resultType: .array(.byte) + ) { printer in + let selfVar = self.printSelfJLongToUnsafeMutablePointer(&printer, swiftParentName: parentName, selfPointerParam) + + printer.print( + """ + return \(selfVar).pointee.withUnsafeBytes { buffer in + return buffer.getJNIValue(in: environment) + } + """ + ) + } } /// Print the necessary conversion logic to go from a `jlong` to a `UnsafeMutablePointer` diff --git a/Sources/SwiftJava/BridgedValues/JavaValue+Array.swift b/Sources/SwiftJava/BridgedValues/JavaValue+Array.swift index 3ff835106..3a54e9b7e 100644 --- a/Sources/SwiftJava/BridgedValues/JavaValue+Array.swift +++ b/Sources/SwiftJava/BridgedValues/JavaValue+Array.swift @@ -50,20 +50,40 @@ extension Array: JavaValue where Element: JavaValue { @inlinable public func getJNIValue(in environment: JNIEnvironment) -> JNIType { - // FIXME: If we have a 1:1 match between the Java layout and the - // Swift layout, as we do for integer/float types, we can do some - // awful alias tries to avoid creating the second array here. + let count = self.count let jniArray = Element.jniNewArray(in: environment)(environment, Int32(count))! - let jniElementBuffer: [Element.JNIType] = map { - $0.getJNIValue(in: environment) + + 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. + self.withUnsafeBytes { buffer in + guard let baseAddress = buffer.baseAddress else { + fatalError("Buffer had no base address?! \(self)") + } + + baseAddress.withMemoryRebound(to: jbyte.self, capacity: count) { ptr in + UInt8.jniSetArrayRegion(in: environment)( + environment, + jniArray, + 0, + jsize(count), + ptr + ) + } + } + } else { + // Slow path, convert every element to the apropriate JNIType: + let jniElementBuffer: [Element.JNIType] = self.map { // meh, temporary array + $0.getJNIValue(in: environment) + } + Element.jniSetArrayRegion(in: environment)( + environment, + jniArray, + 0, + jsize(self.count), + jniElementBuffer + ) } - Element.jniSetArrayRegion(in: environment)( - environment, - jniArray, - 0, - jsize(count), - jniElementBuffer - ) return jniArray } diff --git a/Sources/SwiftJava/BridgedValues/UnsafeRawBufferPointer+getJNIValue.swift b/Sources/SwiftJava/BridgedValues/UnsafeRawBufferPointer+getJNIValue.swift new file mode 100644 index 000000000..a59c1b6ff --- /dev/null +++ b/Sources/SwiftJava/BridgedValues/UnsafeRawBufferPointer+getJNIValue.swift @@ -0,0 +1,39 @@ +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +extension UnsafeRawBufferPointer { + + /// Helper method to extract bytes from an unsafe byte buffer into a newly allocated Java `byte[]`. + @_alwaysEmitIntoClient + public func getJNIValue(in environment: JNIEnvironment) -> jbyteArray { + let count = self.count + var jniArray: jbyteArray = UInt8.jniNewArray(in: environment)(environment, Int32(count))! + getJNIValue(into: &jniArray, in: environment) + return jniArray + } + + public func getJNIValue(into jniArray: inout jbyteArray, in environment: JNIEnvironment) { + assert(Element.self == UInt8.self, "We're going to rebind memory with the assumption storage are bytes") + + // 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. + self.withUnsafeBytes { buffer in + guard let baseAddress = buffer.baseAddress else { + fatalError("Buffer had no base address?! \(self)") + } + + baseAddress.withMemoryRebound(to: jbyte.self, capacity: count) { ptr in + UInt8.jniSetArrayRegion(in: environment)( + environment, + jniArray, + 0, + jsize(count), + ptr + ) + } + } + } +} \ No newline at end of file diff --git a/Tests/JExtractSwiftTests/DataImportTests.swift b/Tests/JExtractSwiftTests/DataImportTests.swift index dc7c54732..4e0bb484d 100644 --- a/Tests/JExtractSwiftTests/DataImportTests.swift +++ b/Tests/JExtractSwiftTests/DataImportTests.swift @@ -523,7 +523,9 @@ final class DataImportTests { "public byte[] toByteArray() {", "private static native byte[] $toByteArray(long selfPointer);", "public byte[] toByteArrayLessCopy() {", - "private static native byte[] $toByteArrayLessCopy(long selfPointer);" + "private static native byte[] $toByteArrayLessCopy(long selfPointer);", + "public byte[] toByteArrayDirect() {", + "private static native byte[] $toByteArrayDirect(long selfPointer);" ]) } From 84292bf4e5d07c02559389d796271fb085b31a32 Mon Sep 17 00:00:00 2001 From: Konrad Malawski Date: Mon, 9 Feb 2026 19:34:41 +0900 Subject: [PATCH 06/14] remove prints from sample swift code, impacts benchmarks --- .../Sources/MySwiftLibrary/MySwiftClass.swift | 11 ----------- .../Sources/MySwiftLibrary/MySwiftLibrary.swift | 3 --- .../Sources/MySwiftLibrary/MySwiftClass.swift | 14 -------------- .../Sources/MySwiftLibrary/MySwiftLibrary.swift | 7 ------- .../Sources/MySwiftLibrary/MySwiftStruct.swift | 4 ---- .../Sources/MySwiftLibrary/MySwiftClass.swift | 3 --- Sources/ExampleSwiftLibrary/MySwiftLibrary.swift | 4 ---- 7 files changed, 46 deletions(-) diff --git a/Samples/SwiftAndJavaJarSampleLib/Sources/MySwiftLibrary/MySwiftClass.swift b/Samples/SwiftAndJavaJarSampleLib/Sources/MySwiftLibrary/MySwiftClass.swift index c842715cd..615873ea6 100644 --- a/Samples/SwiftAndJavaJarSampleLib/Sources/MySwiftLibrary/MySwiftClass.swift +++ b/Samples/SwiftAndJavaJarSampleLib/Sources/MySwiftLibrary/MySwiftClass.swift @@ -31,35 +31,24 @@ public class MySwiftClass { public init(len: Int, cap: Int) { self.len = len self.cap = cap - - p("\(MySwiftClass.self).len = \(self.len)") - p("\(MySwiftClass.self).cap = \(self.cap)") - let addr = unsafeBitCast(self, to: UInt64.self) - p("initializer done, self = 0x\(String(addr, radix: 16, uppercase: true))") } deinit { - let addr = unsafeBitCast(self, to: UInt64.self) - p("Deinit, self = 0x\(String(addr, radix: 16, uppercase: true))") } public var counter: Int32 = 0 public func voidMethod() { - p("") } public func takeIntMethod(i: Int) { - p("i:\(i)") } public func echoIntMethod(i: Int) -> Int { - p("i:\(i)") return i } public func makeIntMethod() -> Int { - p("make int -> 12") return 12 } diff --git a/Samples/SwiftAndJavaJarSampleLib/Sources/MySwiftLibrary/MySwiftLibrary.swift b/Samples/SwiftAndJavaJarSampleLib/Sources/MySwiftLibrary/MySwiftLibrary.swift index e900fdd0f..f60036c81 100644 --- a/Samples/SwiftAndJavaJarSampleLib/Sources/MySwiftLibrary/MySwiftLibrary.swift +++ b/Samples/SwiftAndJavaJarSampleLib/Sources/MySwiftLibrary/MySwiftLibrary.swift @@ -24,15 +24,12 @@ import Darwin.C #endif public func helloWorld() { - p("\(#function)") } public func globalTakeInt(i: Int) { - p("i:\(i)") } public func globalTakeIntInt(i: Int, j: Int) { - p("i:\(i), j:\(j)") } public func globalCallMeRunnable(run: () -> ()) { diff --git a/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftClass.swift b/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftClass.swift index e1139c2b3..bbac8038a 100644 --- a/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftClass.swift +++ b/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftClass.swift @@ -21,16 +21,9 @@ public class MySwiftClass { public init(len: Int, cap: Int) { self.len = len self.cap = cap - - p("\(MySwiftClass.self).len = \(self.len)") - p("\(MySwiftClass.self).cap = \(self.cap)") - let addr = unsafeBitCast(self, to: UInt64.self) - p("initializer done, self = 0x\(String(addr, radix: 16, uppercase: true))") } deinit { - let addr = unsafeBitCast(self, to: UInt64.self) - p("Deinit, self = 0x\(String(addr, radix: 16, uppercase: true))") } public var counter: Int32 = 0 @@ -40,20 +33,16 @@ public class MySwiftClass { } public func voidMethod() { - p("") } public func takeIntMethod(i: Int) { - p("i:\(i)") } public func echoIntMethod(i: Int) -> Int { - p("i:\(i)") return i } public func makeIntMethod() -> Int { - p("make int -> 12") return 12 } @@ -62,14 +51,11 @@ public class MySwiftClass { } public func takeUnsignedChar(arg: UInt16) { - p("\(UInt32.self) = \(arg)") } public func takeUnsignedInt(arg: UInt32) { - p("\(UInt32.self) = \(arg)") } public func takeUnsignedLong(arg: UInt64) { - p("\(UInt64.self) = \(arg)") } } diff --git a/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftLibrary.swift b/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftLibrary.swift index c830e9f6c..46ee268ac 100644 --- a/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftLibrary.swift +++ b/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftLibrary.swift @@ -26,11 +26,9 @@ import Darwin.C import Foundation public func helloWorld() { - p("\(#function)") } public func globalTakeInt(i: Int) { - p("i:\(i)") } public func globalMakeInt() -> Int { @@ -42,7 +40,6 @@ public func globalWriteString(string: String) -> Int { } public func globalTakeIntInt(i: Int, j: Int) { - p("i:\(i), j:\(j)") } public func globalCallMeRunnable(run: () -> ()) { @@ -92,16 +89,12 @@ public func globalReceiveSomeDataProtocol(data: some DataProtocol) -> Int { public func globalReceiveOptional(o1: Int?, o2: (some DataProtocol)?) -> Int { switch (o1, o2) { case (nil, nil): - p(", ") return 0 case (let v1?, nil): - p("\(v1), ") return 1 case (nil, let v2?): - p(", \(v2)") return 2 case (let v1?, let v2?): - p("\(v1), \(v2)") return 3 } } diff --git a/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftStruct.swift b/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftStruct.swift index c81c84b12..5329f8102 100644 --- a/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftStruct.swift +++ b/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftStruct.swift @@ -27,20 +27,16 @@ public struct MySwiftStruct { } public func voidMethod() { - p("") } public func takeIntMethod(i: Int) { - p("i:\(i)") } public func echoIntMethod(i: Int) -> Int { - p("i:\(i)") return i } public func makeIntMethod() -> Int { - p("make int -> 12") return 12 } diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftClass.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftClass.swift index 72f2fa357..46bed5229 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftClass.swift +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftClass.swift @@ -45,13 +45,11 @@ public class MySwiftClass { } public static func method() { - p("Hello from static method in a class!") } public init(x: Int64, y: Int64) { self.x = x self.y = y - p("\(self)") } public init() { @@ -68,7 +66,6 @@ public class MySwiftClass { } deinit { - p("deinit called!") } public func sum() -> Int64 { diff --git a/Sources/ExampleSwiftLibrary/MySwiftLibrary.swift b/Sources/ExampleSwiftLibrary/MySwiftLibrary.swift index 9ccbc164b..4a9ae1eb1 100644 --- a/Sources/ExampleSwiftLibrary/MySwiftLibrary.swift +++ b/Sources/ExampleSwiftLibrary/MySwiftLibrary.swift @@ -70,20 +70,16 @@ public class MySwiftClass { public var counter: Int32 = 0 public func voidMethod() { - p("") } public func takeIntMethod(i: Int) { - p("i:\(i)") } public func echoIntMethod(i: Int) -> Int { - p("i:\(i)") return i } public func makeIntMethod() -> Int { - p("make int -> 12") return 12 } From 4e3d1102950c682f028db0db0e738f8227e0b5f3 Mon Sep 17 00:00:00 2001 From: Konrad Malawski Date: Mon, 9 Feb 2026 19:58:04 +0900 Subject: [PATCH 07/14] cleanup toByteArray impls, we only keep the indirect and direct one --- .../swift/swiftkit/ffm/FFMDataBenchmark.java | 50 +++++++++++-------- .../com/example/swift/JNIDataBenchmark.java | 19 ------- .../test/java/com/example/swift/DataTest.java | 25 ---------- ...t2JavaGenerator+JavaBindingsPrinting.swift | 42 ++++++---------- ...ift2JavaGenerator+SwiftThunkPrinting.swift | 44 +++------------- .../BridgedValues/JavaValue+Array.swift | 17 ++----- .../JExtractSwiftTests/DataImportTests.swift | 9 ++-- 7 files changed, 61 insertions(+), 145 deletions(-) diff --git a/Samples/SwiftJavaExtractFFMSampleApp/src/jmh/java/org/swift/swiftkit/ffm/FFMDataBenchmark.java b/Samples/SwiftJavaExtractFFMSampleApp/src/jmh/java/org/swift/swiftkit/ffm/FFMDataBenchmark.java index 5665aafda..50dd88d92 100644 --- a/Samples/SwiftJavaExtractFFMSampleApp/src/jmh/java/org/swift/swiftkit/ffm/FFMDataBenchmark.java +++ b/Samples/SwiftJavaExtractFFMSampleApp/src/jmh/java/org/swift/swiftkit/ffm/FFMDataBenchmark.java @@ -19,16 +19,22 @@ import org.openjdk.jmh.annotations.*; import org.openjdk.jmh.infra.Blackhole; +import java.lang.foreign.ValueLayout; +import java.nio.ByteBuffer; import java.util.concurrent.TimeUnit; @BenchmarkMode(Mode.AverageTime) -@Warmup(iterations = 5, time = 200, timeUnit = TimeUnit.MILLISECONDS) -@Measurement(iterations = 10, time = 500, timeUnit = TimeUnit.MILLISECONDS) +@Warmup(iterations = 1, time = 200, timeUnit = TimeUnit.MILLISECONDS) +@Measurement(iterations = 5, time = 500, timeUnit = TimeUnit.MILLISECONDS) @OutputTimeUnit(TimeUnit.NANOSECONDS) @State(Scope.Benchmark) -@Fork(value = 3, jvmArgsAppend = { "--enable-native-access=ALL-UNNAMED" }) +@Fork(value = 1, jvmArgsAppend = { "--enable-native-access=ALL-UNNAMED" }) public class FFMDataBenchmark { + private static class Holder { + T value; + } + @Param({"4", "100", "1000"}) public int dataSize; @@ -38,7 +44,6 @@ public class FFMDataBenchmark { @Setup(Level.Trial) public void beforeAll() { arena = AllocatingSwiftArena.ofConfined(); - // FFM mode has Data.init(byte[], arena) directly data = Data.init(makeBytes(dataSize), arena); } @@ -55,27 +60,34 @@ private static byte[] makeBytes(int size) { return bytes; } - /** - * Baseline: simple int echo to measure FFM call overhead. - */ @Benchmark - public long ffm_baseline_globalEchoInt() { - return MySwiftLibrary.globalEchoInt(13); + public long ffm_baseline_globalMakeInt() { + return MySwiftLibrary.globalMakeInt(); } - /** - * Benchmark passing Data to Swift and getting count back. - * Measures: Java → Swift data passing overhead. - */ @Benchmark public long ffm_passDataToSwift() { return MySwiftLibrary.getDataCount(data); } - /** - * Benchmark receiving Data from Swift (makeData returns 4 bytes). - * Measures: Swift → Java data return overhead. - */ + @Benchmark + public ByteBuffer ffm_data_withUnsafeBytes_asByteBuffer() { + Holder buf = new Holder<>(); + data.withUnsafeBytes((bytes) -> { + buf.value = bytes.asByteBuffer(); + }); + return buf.value; + } + + @Benchmark + public byte[] ffm_data_withUnsafeBytes_toArray() { + Holder buf = new Holder<>(); + data.withUnsafeBytes((bytes) -> { + buf.value = bytes.toArray(ValueLayout.JAVA_BYTE); + }); + return buf.value; + } + @Benchmark public Data ffm_receiveDataFromSwift(Blackhole bh) { Data result = MySwiftLibrary.makeData(arena); @@ -83,10 +95,6 @@ public Data ffm_receiveDataFromSwift(Blackhole bh) { return result; } - /** - * Benchmark echo: send Data to Swift and receive it back. - * Measures: Full round-trip overhead. - */ @Benchmark public Data ffm_echoData(Blackhole bh) { Data echoed = MySwiftLibrary.echoData(data, arena); diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/jmh/java/com/example/swift/JNIDataBenchmark.java b/Samples/SwiftJavaExtractJNISampleApp/src/jmh/java/com/example/swift/JNIDataBenchmark.java index 427ccd50e..17630b7b1 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/src/jmh/java/com/example/swift/JNIDataBenchmark.java +++ b/Samples/SwiftJavaExtractJNISampleApp/src/jmh/java/com/example/swift/JNIDataBenchmark.java @@ -64,30 +64,11 @@ public long jni_passDataToSwift() { return MySwiftLibrary.getDataCount(data); } - /** - * toByteArray: Creates [UInt8] from Data, then copies to JVM array. - * This involves two copies: Data -> Swift Array -> JVM Array. - */ @Benchmark public byte[] jni_data_toByteArray() { return data.toByteArray(); } - /** - * toByteArrayLessCopy: Creates JVM array, maps Data bytes to buffer, - * then uses SetByteArrayRegion. - * Still creates an intermediate Swift array via map(). - */ - @Benchmark - public byte[] jni_data_toByteArrayLessCopy() { - return data.toByteArrayLessCopy(); - } - - /** - * toByteArrayDirect: Uses withUnsafeBytes to pass Data's memory - * directly to SetByteArrayRegion. - * No intermediate Swift array allocation - just one copy to JVM. - */ @Benchmark public byte[] jni_data_toByteArrayDirect() { return data.toByteArrayDirect(); diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/DataTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/DataTest.java index 3e1e53a0a..eeb217957 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/DataTest.java +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/DataTest.java @@ -77,19 +77,6 @@ void data_toByteArray() { } } - @Test - void data_toByteArrayLessCopy() { - try (var arena = SwiftArena.ofConfined()) { - byte[] original = new byte[] { 10, 20, 30, 40 }; - var data = Data.fromByteArray(original, arena); - - byte[] result = data.toByteArrayLessCopy(); - - assertEquals(original.length, result.length); - assertArrayEquals(original, result); - } - } - @Test void data_toByteArrayDirect() { try (var arena = SwiftArena.ofConfined()) { @@ -128,18 +115,6 @@ void data_roundTrip() { assertArrayEquals(original, result); } } - @Test - void data_toByteArrayLessCopy_roundTrip() { - try (var arena = SwiftArena.ofConfined()) { - byte[] original = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8 }; - var data = Data.fromByteArray(original, arena); - var echoed = MySwiftLibrary.echoData(data, arena); - - byte[] result = echoed.toByteArrayLessCopy(); - - assertArrayEquals(original, result); - } - } @Test void data_toByteArrayDirect_roundTrip() { diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift index 1086415f4..6b944f435 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift @@ -781,10 +781,13 @@ extension JNISwift2JavaGenerator { """ /** * Copies the contents of this Data to a new byte array. - * - *

Note: This operation copies the bytes from Swift memory - * to the Java heap. For large Data objects, consider the performance - * implications.

+ * + * This is a relatively efficient implementation, which avoids native array copies, + * however it will still perform a copy of the data onto the JVM heap, so use this + * only when necessary. + * + *

When utmost performance is necessary, you may want to investigate the FFM mode + * of jextract which is able to map memory more efficiently. * * @return A byte array containing a copy of this Data's bytes */ @@ -795,33 +798,20 @@ extension JNISwift2JavaGenerator { private static native byte[] $toByteArray(long selfPointer); /** - * Copies the contents of this Data to a new byte array with fewer intermediate copies. - * - *

Note: This operation copies the bytes from Swift memory - * to the Java heap. For large Data objects, consider the performance - * implications.

- * - * @return A byte array containing a copy of this Data's bytes - */ - public byte[] toByteArrayLessCopy() { - return $toByteArrayLessCopy(this.$memoryAddress()); - } - - private static native byte[] $toByteArrayLessCopy(long selfPointer); - - /** - * Copies the contents of this Data to a new byte array using direct memory access. - * - *

This is the most efficient implementation as it uses {@code withUnsafeBytes} - * to pass Data's memory directly to JNI without any intermediate Swift allocations.

- * + * Copies the contents of this Data to a new byte array. + * + * @deprecated Prefer using the `toByteArray` method as it is more performant. + * This implementation uses a naive conversion path from native bytes into jbytes + * and then copying them onto the jvm heap. + * * @return A byte array containing a copy of this Data's bytes */ + @Deprecated(forRemoval = true) public byte[] toByteArrayDirect() { - return $toByteArrayDirect(this.$memoryAddress()); + return $toByteArrayIndirectCopyDirect(this.$memoryAddress()); } - private static native byte[] $toByteArrayDirect(long selfPointer); + private static native byte[] $toByteArrayIndirectCopyDirect(long selfPointer); """ ) } diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift index c5e44b292..3ec6ceab9 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift @@ -811,9 +811,10 @@ extension JNISwift2JavaGenerator { let selfPointerParam = JavaParameter(name: "selfPointer", type: .long) let parentName = type.qualifiedName + // Rebind the memory instead of converting, and set the memory directly using 'jniSetArrayRegion' from the buffer printCDecl( &printer, - javaMethodName: "$toByteArray", + javaMethodName: "$toByteArrayIndirectCopyDirect", parentName: type.swiftNominal.qualifiedName, parameters: [ selfPointerParam @@ -824,45 +825,17 @@ extension JNISwift2JavaGenerator { printer.print( """ - // TODO: This is a double copy, we need to initialize the array and then copy into a JVM array in getJNIValue - return [UInt8](\(selfVar).pointee).getJNIValue(in: environment) - """ - ) - } - - printCDecl( - &printer, - javaMethodName: "$toByteArrayLessCopy", - parentName: type.swiftNominal.qualifiedName, - parameters: [ - selfPointerParam - ], - resultType: .array(.byte) - ) { printer in - let selfVar = self.printSelfJLongToUnsafeMutablePointer(&printer, swiftParentName: parentName, selfPointerParam) - - printer.print( - """ - let jniArray = UInt8.jniNewArray(in: environment)(environment, Int32(\(selfVar).pointee.count))! - let jniElementBuffer: [UInt8.JNIType] = \(selfVar).pointee.map { - $0.getJNIValue(in: environment) + return \(selfVar).pointee.withUnsafeBytes { buffer in + return buffer.getJNIValue(in: environment) } - UInt8.jniSetArrayRegion(in: environment)( - environment, - jniArray, - 0, - jsize(\(selfVar).pointee.count), - jniElementBuffer - ) - return jniArray """ ) } - // Zero-copy version: uses withUnsafeBytes to pass Data's memory directly to JNI + // Legacy API, also to compare with as a baseline, we could remove it printCDecl( &printer, - javaMethodName: "$toByteArrayDirect", + javaMethodName: "$toByteArrayIndirectCopy", parentName: type.swiftNominal.qualifiedName, parameters: [ selfPointerParam @@ -873,9 +846,8 @@ extension JNISwift2JavaGenerator { printer.print( """ - return \(selfVar).pointee.withUnsafeBytes { buffer in - return buffer.getJNIValue(in: environment) - } + // This is a double copy, we need to initialize the array and then copy into a JVM array in getJNIValue + return [UInt8](\(selfVar).pointee).getJNIValue(in: environment) """ ) } diff --git a/Sources/SwiftJava/BridgedValues/JavaValue+Array.swift b/Sources/SwiftJava/BridgedValues/JavaValue+Array.swift index 3a54e9b7e..4c59b140e 100644 --- a/Sources/SwiftJava/BridgedValues/JavaValue+Array.swift +++ b/Sources/SwiftJava/BridgedValues/JavaValue+Array.swift @@ -51,25 +51,13 @@ extension Array: JavaValue where Element: JavaValue { @inlinable public func getJNIValue(in environment: JNIEnvironment) -> JNIType { let count = self.count - let jniArray = Element.jniNewArray(in: environment)(environment, Int32(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. self.withUnsafeBytes { buffer in - guard let baseAddress = buffer.baseAddress else { - fatalError("Buffer had no base address?! \(self)") - } - - baseAddress.withMemoryRebound(to: jbyte.self, capacity: count) { ptr in - UInt8.jniSetArrayRegion(in: environment)( - environment, - jniArray, - 0, - jsize(count), - ptr - ) - } + buffer.getJNIValue(into: &jniArray, in: environment) } } else { // Slow path, convert every element to the apropriate JNIType: @@ -84,6 +72,7 @@ extension Array: JavaValue where Element: JavaValue { jniElementBuffer ) } + return jniArray } diff --git a/Tests/JExtractSwiftTests/DataImportTests.swift b/Tests/JExtractSwiftTests/DataImportTests.swift index 4e0bb484d..111076c20 100644 --- a/Tests/JExtractSwiftTests/DataImportTests.swift +++ b/Tests/JExtractSwiftTests/DataImportTests.swift @@ -519,13 +519,14 @@ final class DataImportTests { expectedChunks: [ "public final class Data implements JNISwiftInstance, DataProtocol {", "public long getCount() {", + "public static Data fromByteArray(byte[] bytes, SwiftArena swiftArena$) {", + "public byte[] toByteArray() {", "private static native byte[] $toByteArray(long selfPointer);", - "public byte[] toByteArrayLessCopy() {", - "private static native byte[] $toByteArrayLessCopy(long selfPointer);", - "public byte[] toByteArrayDirect() {", - "private static native byte[] $toByteArrayDirect(long selfPointer);" + + "public byte[] toByteArrayIndirectCopy() {", + "private static native byte[] $toByteArrayIndirectCopy(long selfPointer);" ]) } From 33f39328ba29584a556f2369c00fa9fbfa8fc8a0 Mon Sep 17 00:00:00 2001 From: Konrad Malawski Date: Mon, 9 Feb 2026 20:00:13 +0900 Subject: [PATCH 08/14] add missing license header --- .../UnsafeRawBufferPointer+getJNIValue.swift | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/Sources/SwiftJava/BridgedValues/UnsafeRawBufferPointer+getJNIValue.swift b/Sources/SwiftJava/BridgedValues/UnsafeRawBufferPointer+getJNIValue.swift index a59c1b6ff..f40ab9203 100644 --- a/Sources/SwiftJava/BridgedValues/UnsafeRawBufferPointer+getJNIValue.swift +++ b/Sources/SwiftJava/BridgedValues/UnsafeRawBufferPointer+getJNIValue.swift @@ -1,3 +1,17 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 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(FoundationEssentials) import FoundationEssentials #else From bf633f572e1666e54db636bcd38e5b4a8fe91ab3 Mon Sep 17 00:00:00 2001 From: Konrad Malawski Date: Mon, 9 Feb 2026 20:56:41 +0900 Subject: [PATCH 09/14] adjust add FFM toByteArray and ByteBuffer methods for Data, cleanup benchmarks --- .../swift/swiftkit/ffm/FFMDataBenchmark.java | 27 ++- .../com/example/swift/DataImportTest.java | 91 +++++++++ .../com/example/swift/JNIDataBenchmark.java | 6 +- .../test/java/com/example/swift/DataTest.java | 8 +- ...FMSwift2JavaGenerator+FoundationData.swift | 129 ++++++++++++ ...ift2JavaGenerator+SwiftThunkPrinting.swift | 40 ++++ .../FFM/FFMSwift2JavaGenerator.swift | 183 +++++++++++++++++- ...t2JavaGenerator+JavaBindingsPrinting.swift | 10 +- ...ift2JavaGenerator+SwiftThunkPrinting.swift | 2 +- 9 files changed, 480 insertions(+), 16 deletions(-) create mode 100644 Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+FoundationData.swift diff --git a/Samples/SwiftJavaExtractFFMSampleApp/src/jmh/java/org/swift/swiftkit/ffm/FFMDataBenchmark.java b/Samples/SwiftJavaExtractFFMSampleApp/src/jmh/java/org/swift/swiftkit/ffm/FFMDataBenchmark.java index 50dd88d92..c7b59c903 100644 --- a/Samples/SwiftJavaExtractFFMSampleApp/src/jmh/java/org/swift/swiftkit/ffm/FFMDataBenchmark.java +++ b/Samples/SwiftJavaExtractFFMSampleApp/src/jmh/java/org/swift/swiftkit/ffm/FFMDataBenchmark.java @@ -19,16 +19,17 @@ import org.openjdk.jmh.annotations.*; import org.openjdk.jmh.infra.Blackhole; +import java.lang.foreign.MemorySegment; import java.lang.foreign.ValueLayout; import java.nio.ByteBuffer; import java.util.concurrent.TimeUnit; @BenchmarkMode(Mode.AverageTime) -@Warmup(iterations = 1, time = 200, timeUnit = TimeUnit.MILLISECONDS) -@Measurement(iterations = 5, time = 500, timeUnit = TimeUnit.MILLISECONDS) +@Warmup(iterations = 5, time = 200, timeUnit = TimeUnit.MILLISECONDS) +@Measurement(iterations = 10, time = 500, timeUnit = TimeUnit.MILLISECONDS) @OutputTimeUnit(TimeUnit.NANOSECONDS) @State(Scope.Benchmark) -@Fork(value = 1, jvmArgsAppend = { "--enable-native-access=ALL-UNNAMED" }) +@Fork(value = 2, jvmArgsAppend = { "--enable-native-access=ALL-UNNAMED" }) public class FFMDataBenchmark { private static class Holder { @@ -88,6 +89,26 @@ public byte[] ffm_data_withUnsafeBytes_toArray() { return buf.value; } + @Benchmark + public byte[] ffm_data_toByteArray() { + return data.toByteArray(); + } + + @Benchmark + public byte[] ffm_data_toByteArray_withArena() { + return data.toByteArray(arena); + } + + @Benchmark + public MemorySegment ffm_data_toMemorySegment() { + return data.toMemorySegment(arena); + } + + @Benchmark + public ByteBuffer ffm_data_toByteBuffer() { + return data.toByteBuffer(arena); + } + @Benchmark public Data ffm_receiveDataFromSwift(Blackhole bh) { Data result = MySwiftLibrary.makeData(arena); diff --git a/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/DataImportTest.java b/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/DataImportTest.java index 52a63f815..bd7678f36 100644 --- a/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/DataImportTest.java +++ b/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/DataImportTest.java @@ -17,6 +17,8 @@ import org.junit.jupiter.api.Test; import org.swift.swiftkit.ffm.AllocatingSwiftArena; +import java.lang.foreign.ValueLayout; + import static org.junit.jupiter.api.Assertions.*; public class DataImportTest { @@ -46,4 +48,93 @@ void test_DataProtocol_receive() { assertEquals(6, result); } } + + @Test + void test_Data_toByteArray() { + try (var arena = AllocatingSwiftArena.ofConfined()) { + byte[] original = new byte[] { 10, 20, 30, 40 }; + var data = Data.fromByteArray(original, arena); + byte[] result = data.toByteArray(); + assertEquals(original.length, result.length); + assertArrayEquals(original, result); + } + } + + @Test + void test_Data_toByteArray_withArena() { + try (var arena = AllocatingSwiftArena.ofConfined()) { + byte[] original = new byte[] { 10, 20, 30, 40 }; + var data = Data.fromByteArray(original, arena); + byte[] result = data.toByteArray(arena); + assertEquals(original.length, result.length); + assertArrayEquals(original, result); + } + } + + @Test + void test_Data_toByteArray_emptyData() { + try (var arena = AllocatingSwiftArena.ofConfined()) { + byte[] original = new byte[0]; + var data = Data.fromByteArray(original, arena); + byte[] result = data.toByteArray(); + assertEquals(0, result.length); + } + } + + @Test + void test_Data_fromByteArray() { + try (var arena = AllocatingSwiftArena.ofConfined()) { + byte[] original = new byte[] { 1, 2, 3, 4, 5 }; + var data = Data.fromByteArray(original, arena); + assertEquals(5, data.getCount()); + } + } + + @Test + void test_Data_toMemorySegment() { + try (var arena = AllocatingSwiftArena.ofConfined()) { + byte[] original = new byte[] { 10, 20, 30, 40 }; + var data = Data.fromByteArray(original, arena); + var segment = data.toMemorySegment(arena); + assertEquals(original.length, segment.byteSize()); + // Verify contents + for (int i = 0; i < original.length; i++) { + assertEquals(original[i], segment.get(ValueLayout.JAVA_BYTE, i)); + } + } + } + + @Test + void test_Data_toByteBuffer() { + try (var arena = AllocatingSwiftArena.ofConfined()) { + byte[] original = new byte[] { 10, 20, 30, 40 }; + var data = Data.fromByteArray(original, arena); + var buffer = data.toByteBuffer(arena); + assertEquals(original.length, buffer.capacity()); + // Verify contents + for (int i = 0; i < original.length; i++) { + assertEquals(original[i], buffer.get(i)); + } + } + } + + @Test + void test_Data_toMemorySegment_emptyData() { + try (var arena = AllocatingSwiftArena.ofConfined()) { + byte[] original = new byte[0]; + var data = Data.fromByteArray(original, arena); + var segment = data.toMemorySegment(arena); + assertEquals(0, segment.byteSize()); + } + } + + @Test + void test_Data_toByteBuffer_emptyData() { + try (var arena = AllocatingSwiftArena.ofConfined()) { + byte[] original = new byte[0]; + var data = Data.fromByteArray(original, arena); + var buffer = data.toByteBuffer(arena); + assertEquals(0, buffer.capacity()); + } + } } diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/jmh/java/com/example/swift/JNIDataBenchmark.java b/Samples/SwiftJavaExtractJNISampleApp/src/jmh/java/com/example/swift/JNIDataBenchmark.java index 17630b7b1..7d815dbc5 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/src/jmh/java/com/example/swift/JNIDataBenchmark.java +++ b/Samples/SwiftJavaExtractJNISampleApp/src/jmh/java/com/example/swift/JNIDataBenchmark.java @@ -26,7 +26,7 @@ @Measurement(iterations = 10, time = 500, timeUnit = TimeUnit.MILLISECONDS) @OutputTimeUnit(TimeUnit.NANOSECONDS) @State(Scope.Benchmark) -@Fork(value = 3, jvmArgsAppend = { "--enable-native-access=ALL-UNNAMED" }) +@Fork(value = 2, jvmArgsAppend = { "--enable-native-access=ALL-UNNAMED" }) public class JNIDataBenchmark { @Param({"4", "100", "1000"}) @@ -70,8 +70,8 @@ public byte[] jni_data_toByteArray() { } @Benchmark - public byte[] jni_data_toByteArrayDirect() { - return data.toByteArrayDirect(); + public byte[] jni_data_toByteArrayIndirectCopy() { + return data.toByteArrayIndirectCopy(); } @Benchmark diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/DataTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/DataTest.java index eeb217957..6acfe70bf 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/DataTest.java +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/DataTest.java @@ -78,12 +78,12 @@ void data_toByteArray() { } @Test - void data_toByteArrayDirect() { + void data_toByteArray() { try (var arena = SwiftArena.ofConfined()) { byte[] original = new byte[] { 10, 20, 30, 40 }; var data = Data.fromByteArray(original, arena); - byte[] result = data.toByteArrayDirect(); + byte[] result = data.toByteArray(); assertEquals(original.length, result.length); assertArrayEquals(original, result); @@ -117,13 +117,13 @@ void data_roundTrip() { } @Test - void data_toByteArrayDirect_roundTrip() { + void data_toByteArray_roundTrip() { try (var arena = SwiftArena.ofConfined()) { byte[] original = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8 }; var data = Data.fromByteArray(original, arena); var echoed = MySwiftLibrary.echoData(data, arena); - byte[] result = echoed.toByteArrayDirect(); + byte[] result = echoed.toByteArray(); assertArrayEquals(original, result); } diff --git a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+FoundationData.swift b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+FoundationData.swift new file mode 100644 index 000000000..9116158c6 --- /dev/null +++ b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+FoundationData.swift @@ -0,0 +1,129 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +import JavaTypes +import SwiftSyntax +import SwiftSyntaxBuilder +import SwiftJavaConfigurationShared +import struct Foundation.URL + +extension FFMSwift2JavaGenerator { + + /// Print Java helper methods for Foundation.Data type + private func printFoundationDataHelpers(_ printer: inout CodePrinter, _ decl: ImportedNominalType) { + let typeName = decl.swiftNominal.name + let thunkNameCopyBytes = "swiftjava_\(swiftModuleName)_\(typeName)_copyBytes__" + + // Print the descriptor class for copyBytes native call + printer.printSeparator("\(typeName) helper methods") + + // This is primarily here for API parity with the JNI version and easier discovery + printer.print( + """ + /** + * Creates a new Swift {@link \(typeName)} instance from a byte array. + * + * @param bytes The byte array to copy into the \(typeName) + * @param arena The arena for memory management + * @return A new \(typeName) instance containing a copy of the bytes + */ + public static \(typeName) fromByteArray(byte[] bytes, AllocatingSwiftArena arena) { + Objects.requireNonNull(bytes, "bytes cannot be null"); + return \(typeName).init(bytes, arena); + } + """ + ) + + // TODO: fromByteBuffer also + + // FIXME: remove the duplication text here + printer.print( + """ + /** + * {@snippet lang=c : + * void \(thunkNameCopyBytes)(const void *self, void *destination, ptrdiff_t count) + * } + */ + private static class \(thunkNameCopyBytes) { + private static final FunctionDescriptor DESC = FunctionDescriptor.ofVoid( + /* self: */SwiftValueLayout.SWIFT_POINTER, + /* destination: */SwiftValueLayout.SWIFT_POINTER, + /* count: */SwiftValueLayout.SWIFT_INT + ); + private static final MemorySegment ADDR = + \(swiftModuleName).findOrThrow("\(thunkNameCopyBytes)"); + private static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); + public static void call(java.lang.foreign.MemorySegment self, java.lang.foreign.MemorySegment destination, long count) { + try { + if (CallTraces.TRACE_DOWNCALLS) { + CallTraces.traceDowncall(self, destination, count); + } + HANDLE.invokeExact(self, destination, count); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + } + """ + ) + + // Print toByteArray with arena parameter + printer.print( + """ + /** + * Copies the contents of this \(typeName) to a new byte array. + * + * This is an efficient implementation that copies the Swift \(typeName) bytes + * directly into a native memory segment, then to the Java heap. + * + * @param arena$ The arena to use for temporary native memory allocation + * @return A byte array containing a copy of this \(typeName)'s bytes + */ + public byte[] toByteArray(AllocatingSwiftArena arena$) { + $ensureAlive(); + long count = getCount(); + if (count == 0) return new byte[0]; + MemorySegment segment = arena$.allocate(count); + \(thunkNameCopyBytes).call(this.$memorySegment(), segment, count); + return segment.toArray(ValueLayout.JAVA_BYTE); + } + """ + ) + + // Print toByteArray convenience method (creates temporary arena) + printer.print( + """ + /** + * Copies the contents of this \(typeName) to a new byte array. + * + * This is a convenience method that creates a temporary arena for the copy. + * For repeated calls, prefer {@link #toByteArray(AllocatingSwiftArena)} to reuse an arena. + * + * @return A byte array containing a copy of this \(typeName)'s bytes + */ + public byte[] toByteArray() { + $ensureAlive(); + long count = getCount(); + if (count == 0) return new byte[0]; + try (var arena$ = Arena.ofConfined()) { + MemorySegment output = arena$.allocate(count); + \(thunkNameCopyBytes).call(this.$memorySegment(), output, count); + return output.toArray(ValueLayout.JAVA_BYTE); + } + } + """ + ) + } +} + diff --git a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+SwiftThunkPrinting.swift b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+SwiftThunkPrinting.swift index 21b3fa679..ee76e3519 100644 --- a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+SwiftThunkPrinting.swift +++ b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+SwiftThunkPrinting.swift @@ -195,6 +195,9 @@ struct SwiftThunkTranslator { decls.append(contentsOf: render(forFunc: decl)) } + // Add special thunks for known types (e.g. Data) + decls.append(contentsOf: renderSpecificTypeThunks(nominal)) + return decls } @@ -228,4 +231,41 @@ struct SwiftThunkTranslator { ) return [DeclSyntax(thunkFunc)] } + + /// Render special thunks for known types like Foundation.Data + func renderSpecificTypeThunks(_ nominal: ImportedNominalType) -> [DeclSyntax] { + guard let knownType = nominal.swiftNominal.knownTypeKind else { + return [] + } + + switch knownType { + case .foundationData, .essentialsData: + return renderFoundationDataThunks(nominal) + default: + return [] + } + } + + /// Render Swift thunks for Foundation.Data helper methods + private func renderFoundationDataThunks(_ nominal: ImportedNominalType) -> [DeclSyntax] { + let thunkName = "swiftjava_\(st.swiftModuleName)_\(nominal.swiftNominal.name)_copyBytes__" + let qualifiedName = nominal.swiftNominal.qualifiedName + + let copyBytesThunk: DeclSyntax = + """ + @_cdecl("\(raw: thunkName)") + public func \(raw: thunkName)( + selfPointer: UnsafeRawPointer, + destinationPointer: UnsafeMutableRawPointer, + count: Int + ) { + let data = selfPointer.assumingMemoryBound(to: \(raw: qualifiedName).self).pointee + data.withUnsafeBytes { buffer in + destinationPointer.copyMemory(from: buffer.baseAddress!, byteCount: count) + } + } + """ + + return [copyBytesThunk] + } } diff --git a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator.swift b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator.swift index ae047284c..22010e82a 100644 --- a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator.swift +++ b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator.swift @@ -230,8 +230,8 @@ extension FFMSwift2JavaGenerator { *
  • This operation does not copy, or retain, the pointed at pointer, so its lifetime must be ensured manually to be valid when wrapping.
  • * */ - public static \(decl.swiftNominal.name) wrapMemoryAddressUnsafe(MemorySegment selfPointer, AllocatingSwiftArena swiftArena) { - return new \(decl.swiftNominal.name)(selfPointer, swiftArena); + public static \(decl.swiftNominal.name) wrapMemoryAddressUnsafe(MemorySegment selfPointer, AllocatingSwiftArena arena) { + return new \(decl.swiftNominal.name)(selfPointer, arena); } """ ) @@ -251,6 +251,9 @@ extension FFMSwift2JavaGenerator { printFunctionDowncallMethods(&printer, funcDecl) } + // Special helper methods for known types (e.g. Data) + printSpecificTypeHelpers(&printer, decl) + // Helper methods and default implementations printToStringMethod(&printer, decl) } @@ -417,5 +420,181 @@ extension FFMSwift2JavaGenerator { } """) } + + /// Print special helper methods for known types like Foundation.Data + func printSpecificTypeHelpers(_ printer: inout CodePrinter, _ decl: ImportedNominalType) { + guard let knownType = decl.swiftNominal.knownTypeKind else { + return + } + + switch knownType { + case .foundationData, .essentialsData: + printFoundationDataHelpers(&printer, decl) + default: + break + } + } + + /// Print Java helper methods for Foundation.Data type + private func printFoundationDataHelpers(_ printer: inout CodePrinter, _ decl: ImportedNominalType) { + let typeName = decl.swiftNominal.name + let thunkNameCopyBytes = "swiftjava_\(swiftModuleName)_\(typeName)_copyBytes__" + + // Print the descriptor class for copyBytes native call + printer.printSeparator("\(typeName) helper methods") + + printer.print( + """ + /** + * {@snippet lang=c : + * void \(thunkNameCopyBytes)(const void *self, void *destination, ptrdiff_t count) + * } + */ + private static class \(thunkNameCopyBytes) { + private static final FunctionDescriptor DESC = FunctionDescriptor.ofVoid( + /* self: */SwiftValueLayout.SWIFT_POINTER, + /* destination: */SwiftValueLayout.SWIFT_POINTER, + /* count: */SwiftValueLayout.SWIFT_INT + ); + private static final MemorySegment ADDR = + \(swiftModuleName).findOrThrow("\(thunkNameCopyBytes)"); + private static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); + public static void call(java.lang.foreign.MemorySegment self, java.lang.foreign.MemorySegment destination, long count) { + try { + if (CallTraces.TRACE_DOWNCALLS) { + CallTraces.traceDowncall(self, destination, count); + } + HANDLE.invokeExact(self, destination, count); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + } + """ + ) + + // Print fromByteArray convenience method + printer.print( + """ + /** + * Creates a new Swift {@link \(typeName)} instance from a byte array. + * + * @param bytes The byte array to copy into the \(typeName) + * @param swiftarena The arena for memory management + * @return A new \(typeName) instance containing a copy of the bytes + */ + public static \(typeName) fromByteArray(byte[] bytes, AllocatingSwiftArena swiftarena) { + Objects.requireNonNull(bytes, "bytes cannot be null"); + return \(typeName).init(bytes, swiftarena); + } + """ + ) + + // Print toMemorySegment - zero-copy after the initial Swift copy + printer.print( + """ + /** + * Copies the contents of this \(typeName) to a new {@link MemorySegment}. + * + * This is the most efficient way to access \(typeName) bytes from Java when you don't + * need a {@code byte[]}. The returned segment is valid for the lifetime of the arena. + * + *

    Copy count: 1 (Swift Data -> MemorySegment) + * + * @param arena The arena to allocate the segment in + * @return A MemorySegment containing a copy of this \(typeName)'s bytes + */ + public MemorySegment toMemorySegment(AllocatingSwiftArena arena) { + $ensureAlive(); + long count = getCount(); + if (count == 0) return MemorySegment.NULL; + MemorySegment segment = arena.allocate(count); + \(thunkNameCopyBytes).call(this.$memorySegment(), segment, count); + return segment; + } + """ + ) + + // Print toByteBuffer - zero-copy view of the segment + printer.print( + """ + /** + * Copies the contents of this \(typeName) to a new {@link ByteBuffer}. + * + * The returned {@link java.nio.ByteBuffer} is a view over native memory and is valid for the + * lifetime of the arena. This avoids an additional copy to the Java heap. + * + *

    Copy count: 1 (Swift Data -> native memory (managed by passed arena), then zero-copy view) + * + * @param arena The arena to allocate the underlying memory in + * @return A ByteBuffer view of the copied bytes + */ + public java.nio.ByteBuffer toByteBuffer(AllocatingSwiftArena arena) { + $ensureAlive(); + long count = getCount(); + if (count == 0) return java.nio.ByteBuffer.allocate(0); + MemorySegment segment = arena.allocate(count); + \(thunkNameCopyBytes).call(this.$memorySegment(), segment, count); + return segment.asByteBuffer(); + } + """ + ) + + // Print toByteArray with arena parameter + printer.print( + """ + /** + * Copies the contents of this \(typeName) to a new byte array. + * The lifetime of the array is independent of the arena, the arena is just used for an intermediary copy. + * + *

    Copy count: 2 (Swift Data -> MemorySegment -> byte[]) + * + *

    For better performance when you can work with {@link MemorySegment} or + * {@link java.nio.ByteBuffer}, prefer {@link #toMemorySegment} or {@link #toByteBuffer}. + * + * @param arena The arena to use for temporary native memory allocation + * @return A byte array containing a copy of this \(typeName)'s bytes + */ + public byte[] toByteArray(AllocatingSwiftArena arena) { + $ensureAlive(); + long count = getCount(); + if (count == 0) return new byte[0]; + MemorySegment segment = arena.allocate(count); + \(thunkNameCopyBytes).call(this.$memorySegment(), segment, count); + return segment.toArray(ValueLayout.JAVA_BYTE); + } + """ + ) + + // Print toByteArray convenience method (creates temporary arena) + printer.print( + """ + /** + * Copies the contents of this \(typeName) to a new byte array. + * The lifetime of the array is independent of the arena, the arena is just used for an intermediary copy. + * + * This is a convenience method that creates a temporary arena for the copy. + * For repeated calls, prefer {@link #toByteArray(AllocatingSwiftArena)} to reuse an arena. + * + *

    Copy count: 2 (Swift Data -> MemorySegment -> byte[]) + * + *

    For better performance when you can work with {@link MemorySegment} or + * {@link java.nio.ByteBuffer}, prefer {@link #toMemorySegment} or {@link #toByteBuffer}. + * + * @return A byte array containing a copy of this \(typeName)'s bytes + */ + public byte[] toByteArray() { + $ensureAlive(); + long count = getCount(); + if (count == 0) return new byte[0]; + try (var arena = Arena.ofConfined()) { + MemorySegment segment = arena.allocate(count); + \(thunkNameCopyBytes).call(this.$memorySegment(), segment, count); + return segment.toArray(ValueLayout.JAVA_BYTE); + } + } + """ + ) + } } diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift index 6b944f435..4b871c370 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift @@ -794,7 +794,11 @@ extension JNISwift2JavaGenerator { public byte[] toByteArray() { return $toByteArray(this.$memoryAddress()); } + """ + ) + printer.print( + """ private static native byte[] $toByteArray(long selfPointer); /** @@ -807,11 +811,11 @@ extension JNISwift2JavaGenerator { * @return A byte array containing a copy of this Data's bytes */ @Deprecated(forRemoval = true) - public byte[] toByteArrayDirect() { - return $toByteArrayIndirectCopyDirect(this.$memoryAddress()); + public byte[] toByteArrayIndirectCopy() { + return $toByteArrayIndirectCopy(this.$memoryAddress()); } - private static native byte[] $toByteArrayIndirectCopyDirect(long selfPointer); + private static native byte[] $toByteArrayIndirectCopy(long selfPointer); """ ) } diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift index 3ec6ceab9..e5f217358 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift @@ -814,7 +814,7 @@ extension JNISwift2JavaGenerator { // Rebind the memory instead of converting, and set the memory directly using 'jniSetArrayRegion' from the buffer printCDecl( &printer, - javaMethodName: "$toByteArrayIndirectCopyDirect", + javaMethodName: "$toByteArray", parentName: type.swiftNominal.qualifiedName, parameters: [ selfPointerParam From cb4b2b0c1ed490bb443b910b43ee15d98bdb02eb Mon Sep 17 00:00:00 2001 From: Konrad Malawski Date: Mon, 9 Feb 2026 21:02:29 +0900 Subject: [PATCH 10/14] remove duplication --- ...FMSwift2JavaGenerator+FoundationData.swift | 95 +++++++++-- .../FFM/FFMSwift2JavaGenerator.swift | 161 ------------------ 2 files changed, 85 insertions(+), 171 deletions(-) diff --git a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+FoundationData.swift b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+FoundationData.swift index 9116158c6..9e175d2c3 100644 --- a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+FoundationData.swift +++ b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+FoundationData.swift @@ -21,7 +21,7 @@ import struct Foundation.URL extension FFMSwift2JavaGenerator { /// Print Java helper methods for Foundation.Data type - private func printFoundationDataHelpers(_ printer: inout CodePrinter, _ decl: ImportedNominalType) { + package func printFoundationDataHelpers(_ printer: inout CodePrinter, _ decl: ImportedNominalType) { let typeName = decl.swiftNominal.name let thunkNameCopyBytes = "swiftjava_\(swiftModuleName)_\(typeName)_copyBytes__" @@ -77,24 +77,93 @@ extension FFMSwift2JavaGenerator { } """ ) + // Print fromByteArray convenience method + printer.print( + """ + /** + * Creates a new Swift {@link \(typeName)} instance from a byte array. + * + * @param bytes The byte array to copy into the \(typeName) + * @param swiftarena The arena for memory management + * @return A new \(typeName) instance containing a copy of the bytes + */ + public static \(typeName) fromByteArray(byte[] bytes, AllocatingSwiftArena swiftarena) { + Objects.requireNonNull(bytes, "bytes cannot be null"); + return \(typeName).init(bytes, swiftarena); + } + """ + ) + + // Print toMemorySegment - zero-copy after the initial Swift copy + printer.print( + """ + /** + * Copies the contents of this \(typeName) to a new {@link MemorySegment}. + * + * This is the most efficient way to access \(typeName) bytes from Java when you don't + * need a {@code byte[]}. The returned segment is valid for the lifetime of the arena. + * + *

    Copy count: 1 (Swift Data -> MemorySegment) + * + * @param arena The arena to allocate the segment in + * @return A MemorySegment containing a copy of this \(typeName)'s bytes + */ + public MemorySegment toMemorySegment(AllocatingSwiftArena arena) { + $ensureAlive(); + long count = getCount(); + if (count == 0) return MemorySegment.NULL; + MemorySegment segment = arena.allocate(count); + \(thunkNameCopyBytes).call(this.$memorySegment(), segment, count); + return segment; + } + """ + ) + + // Print toByteBuffer - zero-copy view of the segment + printer.print( + """ + /** + * Copies the contents of this \(typeName) to a new {@link ByteBuffer}. + * + * The returned {@link java.nio.ByteBuffer} is a view over native memory and is valid for the + * lifetime of the arena. This avoids an additional copy to the Java heap. + * + *

    Copy count: 1 (Swift Data -> native memory (managed by passed arena), then zero-copy view) + * + * @param arena The arena to allocate the underlying memory in + * @return A ByteBuffer view of the copied bytes + */ + public java.nio.ByteBuffer toByteBuffer(AllocatingSwiftArena arena) { + $ensureAlive(); + long count = getCount(); + if (count == 0) return java.nio.ByteBuffer.allocate(0); + MemorySegment segment = arena.allocate(count); + \(thunkNameCopyBytes).call(this.$memorySegment(), segment, count); + return segment.asByteBuffer(); + } + """ + ) // Print toByteArray with arena parameter printer.print( """ /** * Copies the contents of this \(typeName) to a new byte array. + * The lifetime of the array is independent of the arena, the arena is just used for an intermediary copy. + * + *

    Copy count: 2 (Swift Data -> MemorySegment -> byte[]) * - * This is an efficient implementation that copies the Swift \(typeName) bytes - * directly into a native memory segment, then to the Java heap. + *

    For better performance when you can work with {@link MemorySegment} or + * {@link java.nio.ByteBuffer}, prefer {@link #toMemorySegment} or {@link #toByteBuffer}. * - * @param arena$ The arena to use for temporary native memory allocation + * @param arena The arena to use for temporary native memory allocation * @return A byte array containing a copy of this \(typeName)'s bytes */ - public byte[] toByteArray(AllocatingSwiftArena arena$) { + public byte[] toByteArray(AllocatingSwiftArena arena) { $ensureAlive(); long count = getCount(); if (count == 0) return new byte[0]; - MemorySegment segment = arena$.allocate(count); + MemorySegment segment = arena.allocate(count); \(thunkNameCopyBytes).call(this.$memorySegment(), segment, count); return segment.toArray(ValueLayout.JAVA_BYTE); } @@ -106,20 +175,26 @@ extension FFMSwift2JavaGenerator { """ /** * Copies the contents of this \(typeName) to a new byte array. + * The lifetime of the array is independent of the arena, the arena is just used for an intermediary copy. * * This is a convenience method that creates a temporary arena for the copy. * For repeated calls, prefer {@link #toByteArray(AllocatingSwiftArena)} to reuse an arena. * + *

    Copy count: 2 (Swift Data -> MemorySegment -> byte[]) + * + *

    For better performance when you can work with {@link MemorySegment} or + * {@link java.nio.ByteBuffer}, prefer {@link #toMemorySegment} or {@link #toByteBuffer}. + * * @return A byte array containing a copy of this \(typeName)'s bytes */ public byte[] toByteArray() { $ensureAlive(); long count = getCount(); if (count == 0) return new byte[0]; - try (var arena$ = Arena.ofConfined()) { - MemorySegment output = arena$.allocate(count); - \(thunkNameCopyBytes).call(this.$memorySegment(), output, count); - return output.toArray(ValueLayout.JAVA_BYTE); + try (var arena = Arena.ofConfined()) { + MemorySegment segment = arena.allocate(count); + \(thunkNameCopyBytes).call(this.$memorySegment(), segment, count); + return segment.toArray(ValueLayout.JAVA_BYTE); } } """ diff --git a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator.swift b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator.swift index 22010e82a..e9936f697 100644 --- a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator.swift +++ b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator.swift @@ -435,166 +435,5 @@ extension FFMSwift2JavaGenerator { } } - /// Print Java helper methods for Foundation.Data type - private func printFoundationDataHelpers(_ printer: inout CodePrinter, _ decl: ImportedNominalType) { - let typeName = decl.swiftNominal.name - let thunkNameCopyBytes = "swiftjava_\(swiftModuleName)_\(typeName)_copyBytes__" - - // Print the descriptor class for copyBytes native call - printer.printSeparator("\(typeName) helper methods") - - printer.print( - """ - /** - * {@snippet lang=c : - * void \(thunkNameCopyBytes)(const void *self, void *destination, ptrdiff_t count) - * } - */ - private static class \(thunkNameCopyBytes) { - private static final FunctionDescriptor DESC = FunctionDescriptor.ofVoid( - /* self: */SwiftValueLayout.SWIFT_POINTER, - /* destination: */SwiftValueLayout.SWIFT_POINTER, - /* count: */SwiftValueLayout.SWIFT_INT - ); - private static final MemorySegment ADDR = - \(swiftModuleName).findOrThrow("\(thunkNameCopyBytes)"); - private static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); - public static void call(java.lang.foreign.MemorySegment self, java.lang.foreign.MemorySegment destination, long count) { - try { - if (CallTraces.TRACE_DOWNCALLS) { - CallTraces.traceDowncall(self, destination, count); - } - HANDLE.invokeExact(self, destination, count); - } catch (Throwable ex$) { - throw new AssertionError("should not reach here", ex$); - } - } - } - """ - ) - - // Print fromByteArray convenience method - printer.print( - """ - /** - * Creates a new Swift {@link \(typeName)} instance from a byte array. - * - * @param bytes The byte array to copy into the \(typeName) - * @param swiftarena The arena for memory management - * @return A new \(typeName) instance containing a copy of the bytes - */ - public static \(typeName) fromByteArray(byte[] bytes, AllocatingSwiftArena swiftarena) { - Objects.requireNonNull(bytes, "bytes cannot be null"); - return \(typeName).init(bytes, swiftarena); - } - """ - ) - - // Print toMemorySegment - zero-copy after the initial Swift copy - printer.print( - """ - /** - * Copies the contents of this \(typeName) to a new {@link MemorySegment}. - * - * This is the most efficient way to access \(typeName) bytes from Java when you don't - * need a {@code byte[]}. The returned segment is valid for the lifetime of the arena. - * - *

    Copy count: 1 (Swift Data -> MemorySegment) - * - * @param arena The arena to allocate the segment in - * @return A MemorySegment containing a copy of this \(typeName)'s bytes - */ - public MemorySegment toMemorySegment(AllocatingSwiftArena arena) { - $ensureAlive(); - long count = getCount(); - if (count == 0) return MemorySegment.NULL; - MemorySegment segment = arena.allocate(count); - \(thunkNameCopyBytes).call(this.$memorySegment(), segment, count); - return segment; - } - """ - ) - - // Print toByteBuffer - zero-copy view of the segment - printer.print( - """ - /** - * Copies the contents of this \(typeName) to a new {@link ByteBuffer}. - * - * The returned {@link java.nio.ByteBuffer} is a view over native memory and is valid for the - * lifetime of the arena. This avoids an additional copy to the Java heap. - * - *

    Copy count: 1 (Swift Data -> native memory (managed by passed arena), then zero-copy view) - * - * @param arena The arena to allocate the underlying memory in - * @return A ByteBuffer view of the copied bytes - */ - public java.nio.ByteBuffer toByteBuffer(AllocatingSwiftArena arena) { - $ensureAlive(); - long count = getCount(); - if (count == 0) return java.nio.ByteBuffer.allocate(0); - MemorySegment segment = arena.allocate(count); - \(thunkNameCopyBytes).call(this.$memorySegment(), segment, count); - return segment.asByteBuffer(); - } - """ - ) - - // Print toByteArray with arena parameter - printer.print( - """ - /** - * Copies the contents of this \(typeName) to a new byte array. - * The lifetime of the array is independent of the arena, the arena is just used for an intermediary copy. - * - *

    Copy count: 2 (Swift Data -> MemorySegment -> byte[]) - * - *

    For better performance when you can work with {@link MemorySegment} or - * {@link java.nio.ByteBuffer}, prefer {@link #toMemorySegment} or {@link #toByteBuffer}. - * - * @param arena The arena to use for temporary native memory allocation - * @return A byte array containing a copy of this \(typeName)'s bytes - */ - public byte[] toByteArray(AllocatingSwiftArena arena) { - $ensureAlive(); - long count = getCount(); - if (count == 0) return new byte[0]; - MemorySegment segment = arena.allocate(count); - \(thunkNameCopyBytes).call(this.$memorySegment(), segment, count); - return segment.toArray(ValueLayout.JAVA_BYTE); - } - """ - ) - - // Print toByteArray convenience method (creates temporary arena) - printer.print( - """ - /** - * Copies the contents of this \(typeName) to a new byte array. - * The lifetime of the array is independent of the arena, the arena is just used for an intermediary copy. - * - * This is a convenience method that creates a temporary arena for the copy. - * For repeated calls, prefer {@link #toByteArray(AllocatingSwiftArena)} to reuse an arena. - * - *

    Copy count: 2 (Swift Data -> MemorySegment -> byte[]) - * - *

    For better performance when you can work with {@link MemorySegment} or - * {@link java.nio.ByteBuffer}, prefer {@link #toMemorySegment} or {@link #toByteBuffer}. - * - * @return A byte array containing a copy of this \(typeName)'s bytes - */ - public byte[] toByteArray() { - $ensureAlive(); - long count = getCount(); - if (count == 0) return new byte[0]; - try (var arena = Arena.ofConfined()) { - MemorySegment segment = arena.allocate(count); - \(thunkNameCopyBytes).call(this.$memorySegment(), segment, count); - return segment.toArray(ValueLayout.JAVA_BYTE); - } - } - """ - ) - } } From 5745cc95a60fb7ffdf0f01b533eda7c079e6d8e4 Mon Sep 17 00:00:00 2001 From: Konrad Malawski Date: Mon, 9 Feb 2026 21:15:47 +0900 Subject: [PATCH 11/14] Avoid stringly written ffm binding, reuse print method --- .../com/example/swift/DataImportTest.java | 6 +- .../test/java/com/example/swift/DataTest.java | 4 +- ...FMSwift2JavaGenerator+FoundationData.swift | 60 ++++--------------- ...t2JavaGenerator+JavaBindingsPrinting.swift | 21 +++++-- 4 files changed, 34 insertions(+), 57 deletions(-) diff --git a/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/DataImportTest.java b/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/DataImportTest.java index bd7678f36..82fb09464 100644 --- a/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/DataImportTest.java +++ b/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/DataImportTest.java @@ -96,8 +96,8 @@ void test_Data_toMemorySegment() { byte[] original = new byte[] { 10, 20, 30, 40 }; var data = Data.fromByteArray(original, arena); var segment = data.toMemorySegment(arena); + assertEquals(original.length, segment.byteSize()); - // Verify contents for (int i = 0; i < original.length; i++) { assertEquals(original[i], segment.get(ValueLayout.JAVA_BYTE, i)); } @@ -110,8 +110,8 @@ void test_Data_toByteBuffer() { byte[] original = new byte[] { 10, 20, 30, 40 }; var data = Data.fromByteArray(original, arena); var buffer = data.toByteBuffer(arena); + assertEquals(original.length, buffer.capacity()); - // Verify contents for (int i = 0; i < original.length; i++) { assertEquals(original[i], buffer.get(i)); } @@ -134,7 +134,9 @@ void test_Data_toByteBuffer_emptyData() { byte[] original = new byte[0]; var data = Data.fromByteArray(original, arena); var buffer = data.toByteBuffer(arena); + assertEquals(0, buffer.capacity()); } } + } diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/DataTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/DataTest.java index 6acfe70bf..60f881e4a 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/DataTest.java +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/DataTest.java @@ -78,12 +78,12 @@ void data_toByteArray() { } @Test - void data_toByteArray() { + void data_toByteArrayIndirectCopy() { try (var arena = SwiftArena.ofConfined()) { byte[] original = new byte[] { 10, 20, 30, 40 }; var data = Data.fromByteArray(original, arena); - byte[] result = data.toByteArray(); + byte[] result = data.toByteArrayIndirectCopy(); assertEquals(original.length, result.length); assertArrayEquals(original, result); diff --git a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+FoundationData.swift b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+FoundationData.swift index 9e175d2c3..5ad490963 100644 --- a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+FoundationData.swift +++ b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+FoundationData.swift @@ -25,7 +25,6 @@ extension FFMSwift2JavaGenerator { let typeName = decl.swiftNominal.name let thunkNameCopyBytes = "swiftjava_\(swiftModuleName)_\(typeName)_copyBytes__" - // Print the descriptor class for copyBytes native call printer.printSeparator("\(typeName) helper methods") // This is primarily here for API parity with the JNI version and easier discovery @@ -45,54 +44,20 @@ extension FFMSwift2JavaGenerator { """ ) - // TODO: fromByteBuffer also + // TODO: Implement a fromByteBuffer as well - // FIXME: remove the duplication text here - printer.print( - """ - /** - * {@snippet lang=c : - * void \(thunkNameCopyBytes)(const void *self, void *destination, ptrdiff_t count) - * } - */ - private static class \(thunkNameCopyBytes) { - private static final FunctionDescriptor DESC = FunctionDescriptor.ofVoid( - /* self: */SwiftValueLayout.SWIFT_POINTER, - /* destination: */SwiftValueLayout.SWIFT_POINTER, - /* count: */SwiftValueLayout.SWIFT_INT - ); - private static final MemorySegment ADDR = - \(swiftModuleName).findOrThrow("\(thunkNameCopyBytes)"); - private static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); - public static void call(java.lang.foreign.MemorySegment self, java.lang.foreign.MemorySegment destination, long count) { - try { - if (CallTraces.TRACE_DOWNCALLS) { - CallTraces.traceDowncall(self, destination, count); - } - HANDLE.invokeExact(self, destination, count); - } catch (Throwable ex$) { - throw new AssertionError("should not reach here", ex$); - } - } - } - """ - ) - // Print fromByteArray convenience method - printer.print( - """ - /** - * Creates a new Swift {@link \(typeName)} instance from a byte array. - * - * @param bytes The byte array to copy into the \(typeName) - * @param swiftarena The arena for memory management - * @return A new \(typeName) instance containing a copy of the bytes - */ - public static \(typeName) fromByteArray(byte[] bytes, AllocatingSwiftArena swiftarena) { - Objects.requireNonNull(bytes, "bytes cannot be null"); - return \(typeName).init(bytes, swiftarena); - } - """ + // Print the descriptor class for copyBytes native call using the shared helper + let copyBytesCFunc = CFunction( + resultType: .void, + name: thunkNameCopyBytes, + parameters: [ + CParameter(name: "self", type: .qualified(const: true, volatile: false, type: .pointer(.void))), + CParameter(name: "destination", type: .pointer(.void)), + CParameter(name: "count", type: .integral(.ptrdiff_t)) + ], + isVariadic: false ) + printJavaBindingDescriptorClass(&printer, copyBytesCFunc) // Print toMemorySegment - zero-copy after the initial Swift copy printer.print( @@ -201,4 +166,3 @@ extension FFMSwift2JavaGenerator { ) } } - diff --git a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaBindingsPrinting.swift b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaBindingsPrinting.swift index 2ac5506c9..b7630dee8 100644 --- a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaBindingsPrinting.swift +++ b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaBindingsPrinting.swift @@ -44,6 +44,21 @@ extension FFMSwift2JavaGenerator { // 'try!' because we know 'loweredSignature' can be described with C. let cFunc = try! translated.loweredSignature.cFunctionDecl(cName: thunkName) + printJavaBindingDescriptorClass(&printer, cFunc) { printer in + if let outCallback = translated.translatedSignature.result.outCallback { + self.printUpcallParameterDescriptorClasses(&printer, outCallback) + } else { // FIXME: not an "else" + self.printParameterDescriptorClasses(&printer, cFunc) + } + } + } + + /// Reusable function to print the FFM Java binding descriptors for a C function. + package func printJavaBindingDescriptorClass( + _ printer: inout CodePrinter, + _ cFunc: CFunction, + additionalContent: ((inout CodePrinter) -> Void)? = nil + ) { printer.printBraceBlock( """ /** @@ -63,11 +78,7 @@ extension FFMSwift2JavaGenerator { """ ) printJavaBindingDowncallMethod(&printer, cFunc) - if let outCallback = translated.translatedSignature.result.outCallback { - printUpcallParameterDescriptorClasses(&printer, outCallback) - } else { // FIXME: not an "else" - printParameterDescriptorClasses(&printer, cFunc) - } + additionalContent?(&printer) } } From 2f2b6b5afae101a319fb79d9c5dfb9aa93259e68 Mon Sep 17 00:00:00 2001 From: Konrad Malawski Date: Mon, 9 Feb 2026 21:25:09 +0900 Subject: [PATCH 12/14] Revert "remove prints from sample swift code, impacts benchmarks" This reverts commit 84292bf4e5d07c02559389d796271fb085b31a32. --- .../Sources/MySwiftLibrary/MySwiftClass.swift | 11 +++++++++++ .../Sources/MySwiftLibrary/MySwiftLibrary.swift | 3 +++ .../Sources/MySwiftLibrary/MySwiftClass.swift | 14 ++++++++++++++ .../Sources/MySwiftLibrary/MySwiftLibrary.swift | 7 +++++++ .../Sources/MySwiftLibrary/MySwiftStruct.swift | 4 ++++ .../Sources/MySwiftLibrary/MySwiftClass.swift | 3 +++ Sources/ExampleSwiftLibrary/MySwiftLibrary.swift | 4 ++++ 7 files changed, 46 insertions(+) diff --git a/Samples/SwiftAndJavaJarSampleLib/Sources/MySwiftLibrary/MySwiftClass.swift b/Samples/SwiftAndJavaJarSampleLib/Sources/MySwiftLibrary/MySwiftClass.swift index 615873ea6..c842715cd 100644 --- a/Samples/SwiftAndJavaJarSampleLib/Sources/MySwiftLibrary/MySwiftClass.swift +++ b/Samples/SwiftAndJavaJarSampleLib/Sources/MySwiftLibrary/MySwiftClass.swift @@ -31,24 +31,35 @@ public class MySwiftClass { public init(len: Int, cap: Int) { self.len = len self.cap = cap + + p("\(MySwiftClass.self).len = \(self.len)") + p("\(MySwiftClass.self).cap = \(self.cap)") + let addr = unsafeBitCast(self, to: UInt64.self) + p("initializer done, self = 0x\(String(addr, radix: 16, uppercase: true))") } deinit { + let addr = unsafeBitCast(self, to: UInt64.self) + p("Deinit, self = 0x\(String(addr, radix: 16, uppercase: true))") } public var counter: Int32 = 0 public func voidMethod() { + p("") } public func takeIntMethod(i: Int) { + p("i:\(i)") } public func echoIntMethod(i: Int) -> Int { + p("i:\(i)") return i } public func makeIntMethod() -> Int { + p("make int -> 12") return 12 } diff --git a/Samples/SwiftAndJavaJarSampleLib/Sources/MySwiftLibrary/MySwiftLibrary.swift b/Samples/SwiftAndJavaJarSampleLib/Sources/MySwiftLibrary/MySwiftLibrary.swift index f60036c81..e900fdd0f 100644 --- a/Samples/SwiftAndJavaJarSampleLib/Sources/MySwiftLibrary/MySwiftLibrary.swift +++ b/Samples/SwiftAndJavaJarSampleLib/Sources/MySwiftLibrary/MySwiftLibrary.swift @@ -24,12 +24,15 @@ import Darwin.C #endif public func helloWorld() { + p("\(#function)") } public func globalTakeInt(i: Int) { + p("i:\(i)") } public func globalTakeIntInt(i: Int, j: Int) { + p("i:\(i), j:\(j)") } public func globalCallMeRunnable(run: () -> ()) { diff --git a/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftClass.swift b/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftClass.swift index bbac8038a..e1139c2b3 100644 --- a/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftClass.swift +++ b/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftClass.swift @@ -21,9 +21,16 @@ public class MySwiftClass { public init(len: Int, cap: Int) { self.len = len self.cap = cap + + p("\(MySwiftClass.self).len = \(self.len)") + p("\(MySwiftClass.self).cap = \(self.cap)") + let addr = unsafeBitCast(self, to: UInt64.self) + p("initializer done, self = 0x\(String(addr, radix: 16, uppercase: true))") } deinit { + let addr = unsafeBitCast(self, to: UInt64.self) + p("Deinit, self = 0x\(String(addr, radix: 16, uppercase: true))") } public var counter: Int32 = 0 @@ -33,16 +40,20 @@ public class MySwiftClass { } public func voidMethod() { + p("") } public func takeIntMethod(i: Int) { + p("i:\(i)") } public func echoIntMethod(i: Int) -> Int { + p("i:\(i)") return i } public func makeIntMethod() -> Int { + p("make int -> 12") return 12 } @@ -51,11 +62,14 @@ public class MySwiftClass { } public func takeUnsignedChar(arg: UInt16) { + p("\(UInt32.self) = \(arg)") } public func takeUnsignedInt(arg: UInt32) { + p("\(UInt32.self) = \(arg)") } public func takeUnsignedLong(arg: UInt64) { + p("\(UInt64.self) = \(arg)") } } diff --git a/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftLibrary.swift b/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftLibrary.swift index 46ee268ac..c830e9f6c 100644 --- a/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftLibrary.swift +++ b/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftLibrary.swift @@ -26,9 +26,11 @@ import Darwin.C import Foundation public func helloWorld() { + p("\(#function)") } public func globalTakeInt(i: Int) { + p("i:\(i)") } public func globalMakeInt() -> Int { @@ -40,6 +42,7 @@ public func globalWriteString(string: String) -> Int { } public func globalTakeIntInt(i: Int, j: Int) { + p("i:\(i), j:\(j)") } public func globalCallMeRunnable(run: () -> ()) { @@ -89,12 +92,16 @@ public func globalReceiveSomeDataProtocol(data: some DataProtocol) -> Int { public func globalReceiveOptional(o1: Int?, o2: (some DataProtocol)?) -> Int { switch (o1, o2) { case (nil, nil): + p(", ") return 0 case (let v1?, nil): + p("\(v1), ") return 1 case (nil, let v2?): + p(", \(v2)") return 2 case (let v1?, let v2?): + p("\(v1), \(v2)") return 3 } } diff --git a/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftStruct.swift b/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftStruct.swift index 5329f8102..c81c84b12 100644 --- a/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftStruct.swift +++ b/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftStruct.swift @@ -27,16 +27,20 @@ public struct MySwiftStruct { } public func voidMethod() { + p("") } public func takeIntMethod(i: Int) { + p("i:\(i)") } public func echoIntMethod(i: Int) -> Int { + p("i:\(i)") return i } public func makeIntMethod() -> Int { + p("make int -> 12") return 12 } diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftClass.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftClass.swift index 46bed5229..72f2fa357 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftClass.swift +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftClass.swift @@ -45,11 +45,13 @@ public class MySwiftClass { } public static func method() { + p("Hello from static method in a class!") } public init(x: Int64, y: Int64) { self.x = x self.y = y + p("\(self)") } public init() { @@ -66,6 +68,7 @@ public class MySwiftClass { } deinit { + p("deinit called!") } public func sum() -> Int64 { diff --git a/Sources/ExampleSwiftLibrary/MySwiftLibrary.swift b/Sources/ExampleSwiftLibrary/MySwiftLibrary.swift index 4a9ae1eb1..9ccbc164b 100644 --- a/Sources/ExampleSwiftLibrary/MySwiftLibrary.swift +++ b/Sources/ExampleSwiftLibrary/MySwiftLibrary.swift @@ -70,16 +70,20 @@ public class MySwiftClass { public var counter: Int32 = 0 public func voidMethod() { + p("") } public func takeIntMethod(i: Int) { + p("i:\(i)") } public func echoIntMethod(i: Int) -> Int { + p("i:\(i)") return i } public func makeIntMethod() -> Int { + p("make int -> 12") return 12 } From 118743f9539c806f5875b487fcba99ed9e24adc5 Mon Sep 17 00:00:00 2001 From: Konrad Malawski Date: Mon, 9 Feb 2026 21:34:11 +0900 Subject: [PATCH 13/14] add some more docs --- .../Sources/MySwiftLibrary/Data.swift | 2 +- .../java/org/swift/swiftkit/ffm/FFMDataBenchmark.java | 2 +- .../Sources/MySwiftLibrary/Data.swift | 2 +- .../jmh/java/com/example/swift/JNIDataBenchmark.java | 2 +- .../src/test/java/com/example/swift/DataTest.java | 2 +- .../FFM/FFMSwift2JavaGenerator+FoundationData.swift | 6 +----- .../UnsafeRawBufferPointer+getJNIValue.swift | 2 +- .../Documentation.docc/SupportedFeatures.md | 11 +++++++++++ 8 files changed, 18 insertions(+), 11 deletions(-) diff --git a/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/Data.swift b/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/Data.swift index 871d7d007..5ebee848d 100644 --- a/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/Data.swift +++ b/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/Data.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Copyright (c) 2026 Apple Inc. and the Swift.org project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information diff --git a/Samples/SwiftJavaExtractFFMSampleApp/src/jmh/java/org/swift/swiftkit/ffm/FFMDataBenchmark.java b/Samples/SwiftJavaExtractFFMSampleApp/src/jmh/java/org/swift/swiftkit/ffm/FFMDataBenchmark.java index c7b59c903..39a64c7a9 100644 --- a/Samples/SwiftJavaExtractFFMSampleApp/src/jmh/java/org/swift/swiftkit/ffm/FFMDataBenchmark.java +++ b/Samples/SwiftJavaExtractFFMSampleApp/src/jmh/java/org/swift/swiftkit/ffm/FFMDataBenchmark.java @@ -2,7 +2,7 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Copyright (c) 2026 Apple Inc. and the Swift.org project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Data.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Data.swift index d04b30775..b162b2c2b 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Data.swift +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Data.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Copyright (c) 2026 Apple Inc. and the Swift.org project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/jmh/java/com/example/swift/JNIDataBenchmark.java b/Samples/SwiftJavaExtractJNISampleApp/src/jmh/java/com/example/swift/JNIDataBenchmark.java index 7d815dbc5..283f1f7c0 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/src/jmh/java/com/example/swift/JNIDataBenchmark.java +++ b/Samples/SwiftJavaExtractJNISampleApp/src/jmh/java/com/example/swift/JNIDataBenchmark.java @@ -2,7 +2,7 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Copyright (c) 2026 Apple Inc. and the Swift.org project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/DataTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/DataTest.java index 60f881e4a..1088b176d 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/DataTest.java +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/DataTest.java @@ -2,7 +2,7 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Copyright (c) 2026 Apple Inc. and the Swift.org project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information diff --git a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+FoundationData.swift b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+FoundationData.swift index 5ad490963..36c129caa 100644 --- a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+FoundationData.swift +++ b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+FoundationData.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Copyright (c) 2026 Apple Inc. and the Swift.org project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information @@ -59,7 +59,6 @@ extension FFMSwift2JavaGenerator { ) printJavaBindingDescriptorClass(&printer, copyBytesCFunc) - // Print toMemorySegment - zero-copy after the initial Swift copy printer.print( """ /** @@ -84,7 +83,6 @@ extension FFMSwift2JavaGenerator { """ ) - // Print toByteBuffer - zero-copy view of the segment printer.print( """ /** @@ -109,7 +107,6 @@ extension FFMSwift2JavaGenerator { """ ) - // Print toByteArray with arena parameter printer.print( """ /** @@ -135,7 +132,6 @@ extension FFMSwift2JavaGenerator { """ ) - // Print toByteArray convenience method (creates temporary arena) printer.print( """ /** diff --git a/Sources/SwiftJava/BridgedValues/UnsafeRawBufferPointer+getJNIValue.swift b/Sources/SwiftJava/BridgedValues/UnsafeRawBufferPointer+getJNIValue.swift index f40ab9203..056d0489b 100644 --- a/Sources/SwiftJava/BridgedValues/UnsafeRawBufferPointer+getJNIValue.swift +++ b/Sources/SwiftJava/BridgedValues/UnsafeRawBufferPointer+getJNIValue.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Copyright (c) 2026 Apple Inc. and the Swift.org project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information diff --git a/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md b/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md index 19ee87bb5..816844b2f 100644 --- a/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md +++ b/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md @@ -157,8 +157,19 @@ data.withUnsafeBytes((bytes) -> { This API avoids copying the data into the Java heap in order to perform operations on it, as we are able to manipulate it directly thanks to the exposed `MemorySegment`. +It is also possible to use the convenience functions `toByteBuffer` and `toByteArray` to obtain a `java.nio.ByteBuffer` or `[byte]` array respectively. Thos operations incurr a copy by moving the data to the JVM's heap. + +It is also possible to get the underlying memory copied into a new `MemorySegment` by using `toMemorySegment(arena)` which performs a copy from native memory to memory managed by the passed arena. The lifetime of that memory is managed by the arena and may outlive the original `Data`. + +It is preferable to use the `withUnsafeBytes` pattern if using the bytes only during a fixed scope, because it alows us to avoid copies into the JVM heap entirely. However when a JVM byte array is necessary, the price of copying will have to be paid anyway. Consider these various options when optimizing your FFI calls and patterns for performance. + ### Data in jextract JNI mode +Swift methods which pass or accept the Foundation `Data` type are extracted using the wrapper Java `Data` type, +which offers utility methods to efficiently copy the underlying native data into a java byte array (`[byte]`). + +Unlike the FFM mode, a true zero-copy `withUnsafeBytes` is not available. + ### Enums > Note: Enums are currently only supported in JNI mode. From 82d1817e1a698af0c300886c8f5bf2a410d46d5e Mon Sep 17 00:00:00 2001 From: Konrad Malawski Date: Tue, 10 Feb 2026 09:59:38 +0900 Subject: [PATCH 14/14] optimize the Array init(fromJNI:), removes an array alloc --- .../BridgedValues/JavaValue+Array.swift | 59 ++++++++++++++----- 1 file changed, 43 insertions(+), 16 deletions(-) diff --git a/Sources/SwiftJava/BridgedValues/JavaValue+Array.swift b/Sources/SwiftJava/BridgedValues/JavaValue+Array.swift index 4c59b140e..d0d48d92d 100644 --- a/Sources/SwiftJava/BridgedValues/JavaValue+Array.swift +++ b/Sources/SwiftJava/BridgedValues/JavaValue+Array.swift @@ -23,29 +23,56 @@ extension Array: JavaValue where Element: JavaValue { public init(fromJNI value: JNIType, in environment: JNIEnvironment) { let jniCount = environment.interface.GetArrayLength(environment, value) - let jniArray: [Element.JNIType] = - if let value { - .init( - unsafeUninitializedCapacity: Int(jniCount) - ) { buffer, initializedCount in - Element.jniGetArrayRegion(in: environment)( + let count = Int(jniCount) + + guard let value else { + self = [] + return + } + + // Fast path for byte types: Since the memory layout of `jbyte` (Int8) and UInt8/Int8 is identical, + // we can rebind the memory and fill it directly without creating an intermediate array. + // This mirrors the optimization in `getJNIValue` in the reverse direction. + if Element.self == UInt8.self { + let result = [UInt8](unsafeUninitializedCapacity: count) { buffer, initializedCount in + buffer.withMemoryRebound(to: jbyte.self) { jbyteBuffer in + UInt8.jniGetArrayRegion(in: environment)( environment, value, 0, jniCount, - buffer.baseAddress + jbyteBuffer.baseAddress ) - initializedCount = Int(jniCount) } - } else { - [] + initializedCount = count } - - // FIXME: If we have a 1:1 match between the Java layout and the - // Swift layout, as we do for integer/float types, we can do some - // awful alias tricks above to have JNI fill in the contents of the - // array directly without this extra copy. For now, just map. - self = jniArray.map { Element(fromJNI: $0, in: environment) } + self = result as! Self + } else if Element.self == Int8.self { + let result = [Int8](unsafeUninitializedCapacity: Int(jniCount)) { buffer, initializedCount in + Int8.jniGetArrayRegion(in: environment)( + environment, + value, + 0, + jniCount, + buffer.baseAddress + ) + initializedCount = count + } + self = result as! Self + } else { + // Slow path for other types: create intermediate array and map + let jniArray = [Element.JNIType](unsafeUninitializedCapacity: count) { buffer, initializedCount in + Element.jniGetArrayRegion(in: environment)( + environment, + value, + 0, + jniCount, + buffer.baseAddress + ) + initializedCount = Int(jniCount) + } + self = jniArray.map { Element(fromJNI: $0, in: environment) } + } } @inlinable