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