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/
diff --git a/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/Data.swift b/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/Data.swift
new file mode 100644
index 000000000..5ebee848d
--- /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) 2026 Apple Inc. and the Swift.org project authors
+// Licensed under Apache License v2.0
+//
+// See LICENSE.txt for license information
+// See CONTRIBUTORS.txt for the list of Swift.org project authors
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+//===----------------------------------------------------------------------===//
+
+#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..39a64c7a9
--- /dev/null
+++ b/Samples/SwiftJavaExtractFFMSampleApp/src/jmh/java/org/swift/swiftkit/ffm/FFMDataBenchmark.java
@@ -0,0 +1,125 @@
+//===----------------------------------------------------------------------===//
+//
+// This source file is part of the Swift.org open source project
+//
+// Copyright (c) 2026 Apple Inc. and the Swift.org project authors
+// Licensed under Apache License v2.0
+//
+// See LICENSE.txt for license information
+// See CONTRIBUTORS.txt for the list of Swift.org project authors
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+//===----------------------------------------------------------------------===//
+
+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.lang.foreign.MemorySegment;
+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)
+@OutputTimeUnit(TimeUnit.NANOSECONDS)
+@State(Scope.Benchmark)
+@Fork(value = 2, jvmArgsAppend = { "--enable-native-access=ALL-UNNAMED" })
+public class FFMDataBenchmark {
+
+ private static class Holder {
+ T value;
+ }
+
+ @Param({"4", "100", "1000"})
+ public int dataSize;
+
+ ClosableAllocatingSwiftArena arena;
+ Data data;
+
+ @Setup(Level.Trial)
+ public void beforeAll() {
+ arena = AllocatingSwiftArena.ofConfined();
+ 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;
+ }
+
+ @Benchmark
+ public long ffm_baseline_globalMakeInt() {
+ return MySwiftLibrary.globalMakeInt();
+ }
+
+ @Benchmark
+ public long ffm_passDataToSwift() {
+ return MySwiftLibrary.getDataCount(data);
+ }
+
+ @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 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);
+ bh.consume(result.getCount());
+ return result;
+ }
+
+ @Benchmark
+ public Data ffm_echoData(Blackhole bh) {
+ Data echoed = MySwiftLibrary.echoData(data, arena);
+ bh.consume(echoed.getCount());
+ return echoed;
+ }
+}
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..82fb09464 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,95 @@ 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());
+ 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());
+ 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/Sources/MySwiftLibrary/Data.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Data.swift
new file mode 100644
index 000000000..b162b2c2b
--- /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) 2026 Apple Inc. and the Swift.org project authors
+// Licensed under Apache License v2.0
+//
+// See LICENSE.txt for license information
+// See CONTRIBUTORS.txt for the list of Swift.org project authors
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+//===----------------------------------------------------------------------===//
+
+#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..283f1f7c0
--- /dev/null
+++ b/Samples/SwiftJavaExtractJNISampleApp/src/jmh/java/com/example/swift/JNIDataBenchmark.java
@@ -0,0 +1,90 @@
+//===----------------------------------------------------------------------===//
+//
+// This source file is part of the Swift.org open source project
+//
+// Copyright (c) 2026 Apple Inc. and the Swift.org project authors
+// Licensed under Apache License v2.0
+//
+// See LICENSE.txt for license information
+// See CONTRIBUTORS.txt for the list of Swift.org project authors
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+//===----------------------------------------------------------------------===//
+
+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 = 2, 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 byte[] jni_data_toByteArray() {
+ return data.toByteArray();
+ }
+
+ @Benchmark
+ public byte[] jni_data_toByteArrayIndirectCopy() {
+ return data.toByteArrayIndirectCopy();
+ }
+
+ @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..1088b176d
--- /dev/null
+++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/DataTest.java
@@ -0,0 +1,131 @@
+//===----------------------------------------------------------------------===//
+//
+// This source file is part of the Swift.org open source project
+//
+// Copyright (c) 2026 Apple Inc. and the Swift.org project authors
+// Licensed under Apache License v2.0
+//
+// See LICENSE.txt for license information
+// See CONTRIBUTORS.txt for the list of Swift.org project authors
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+//===----------------------------------------------------------------------===//
+
+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));
+ }
+ }
+
+ @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_toByteArrayIndirectCopy() {
+ try (var arena = SwiftArena.ofConfined()) {
+ byte[] original = new byte[] { 10, 20, 30, 40 };
+ var data = Data.fromByteArray(original, arena);
+
+ byte[] result = data.toByteArrayIndirectCopy();
+
+ 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);
+ }
+ }
+
+ @Test
+ 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.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..36c129caa
--- /dev/null
+++ b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+FoundationData.swift
@@ -0,0 +1,164 @@
+//===----------------------------------------------------------------------===//
+//
+// This source file is part of the Swift.org open source project
+//
+// Copyright (c) 2026 Apple Inc. and the Swift.org project authors
+// Licensed under Apache License v2.0
+//
+// See LICENSE.txt for license information
+// See CONTRIBUTORS.txt for the list of Swift.org project authors
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+//===----------------------------------------------------------------------===//
+
+import JavaTypes
+import SwiftSyntax
+import SwiftSyntaxBuilder
+import SwiftJavaConfigurationShared
+import struct Foundation.URL
+
+extension FFMSwift2JavaGenerator {
+
+ /// Print Java helper methods for Foundation.Data type
+ package func printFoundationDataHelpers(_ printer: inout CodePrinter, _ decl: ImportedNominalType) {
+ let typeName = decl.swiftNominal.name
+ let thunkNameCopyBytes = "swiftjava_\(swiftModuleName)_\(typeName)_copyBytes__"
+
+ 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: Implement a fromByteBuffer as well
+
+ // 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)
+
+ 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;
+ }
+ """
+ )
+
+ 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();
+ }
+ """
+ )
+
+ 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);
+ }
+ """
+ )
+
+ 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/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)
}
}
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/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..e9936f697 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,20 @@ 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
+ }
+ }
+
}
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..4b871c370 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,64 @@ extension JNISwift2JavaGenerator {
"""
)
}
+
+ private func printFoundationDataHelpers(_ printer: inout CodePrinter, _ decl: ImportedNominalType) {
+ printer.print(
+ """
+ /**
+ * 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
+ * @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$);
+ }
+ """
+ )
+
+ printer.print(
+ """
+ /**
+ * Copies the contents of this Data to a new byte array.
+ *
+ * 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
+ */
+ public byte[] toByteArray() {
+ return $toByteArray(this.$memoryAddress());
+ }
+ """
+ )
+
+ printer.print(
+ """
+ private static native byte[] $toByteArray(long selfPointer);
+
+ /**
+ * 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[] toByteArrayIndirectCopy() {
+ return $toByteArrayIndirectCopy(this.$memoryAddress());
+ }
+
+ private static native byte[] $toByteArrayIndirectCopy(long selfPointer);
+ """
+ )
+ }
}
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/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift
index dd82f82d9..e5f217358 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,67 @@ 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
+
+ // Rebind the memory instead of converting, and set the memory directly using 'jniSetArrayRegion' from the buffer
+ 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(
+ """
+ return \(selfVar).pointee.withUnsafeBytes { buffer in
+ return buffer.getJNIValue(in: environment)
+ }
+ """
+ )
+ }
+
+ // Legacy API, also to compare with as a baseline, we could remove it
+ printCDecl(
+ &printer,
+ javaMethodName: "$toByteArrayIndirectCopy",
+ parentName: type.swiftNominal.qualifiedName,
+ parameters: [
+ selfPointerParam
+ ],
+ resultType: .array(.byte)
+ ) { printer in
+ let selfVar = self.printSelfJLongToUnsafeMutablePointer(&printer, swiftParentName: parentName, selfPointerParam)
+
+ printer.print(
+ """
+ // 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/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/SwiftJava/BridgedValues/JavaValue+Array.swift b/Sources/SwiftJava/BridgedValues/JavaValue+Array.swift
index 3db307891..d0d48d92d 100644
--- a/Sources/SwiftJava/BridgedValues/JavaValue+Array.swift
+++ b/Sources/SwiftJava/BridgedValues/JavaValue+Array.swift
@@ -23,46 +23,83 @@ 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
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 jniArray = Element.jniNewArray(in: environment)(environment, Int32(count))!
- let jniElementBuffer: [Element.JNIType] = map {
- $0.getJNIValue(in: environment)
+ let count = self.count
+ var jniArray = Element.jniNewArray(in: environment)(environment, Int32(count))!
+
+ if Element.self == UInt8.self || Element.self == Int8.self {
+ // Fast path, Since the memory layout of `jbyte`` and those is the same, we rebind the memory
+ // rather than convert every element independently. This allows us to avoid another Swift array creation.
+ self.withUnsafeBytes { buffer in
+ buffer.getJNIValue(into: &jniArray, in: environment)
+ }
+ } 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..056d0489b
--- /dev/null
+++ b/Sources/SwiftJava/BridgedValues/UnsafeRawBufferPointer+getJNIValue.swift
@@ -0,0 +1,53 @@
+//===----------------------------------------------------------------------===//
+//
+// This source file is part of the Swift.org open source project
+//
+// Copyright (c) 2026 Apple Inc. and the Swift.org project authors
+// Licensed under Apache License v2.0
+//
+// See LICENSE.txt for license information
+// See CONTRIBUTORS.txt for the list of Swift.org project authors
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+//===----------------------------------------------------------------------===//
+
+#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/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md b/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md
index d36be9a48..816844b2f 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,41 @@ 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`.
+
+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.
@@ -281,7 +316,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/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 bad4174db..111076c20 100644
--- a/Tests/JExtractSwiftTests/DataImportTests.swift
+++ b/Tests/JExtractSwiftTests/DataImportTests.swift
@@ -449,5 +449,85 @@ 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$) {",
+
+ "public byte[] toByteArray() {",
+ "private static native byte[] $toByteArray(long selfPointer);",
+
+ "public byte[] toByteArrayIndirectCopy() {",
+ "private static native byte[] $toByteArrayIndirectCopy(long selfPointer);"
+ ])
+ }
+
}