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);" + ]) + } + }