Skip to content

Commit f5b6c66

Browse files
authored
jextract/jni: handle UnsafeRawBufferPointer params (#668)
* jextract/jni: handle UnsafeRawBufferPointer params This allows passing a Java byte array to an `UnsafeRawBufferPointer` accepting API in Swift. This conversion needs to copy a bunch but makes pretty much sense. This is in JNI mode specifically. In FFM mode we could do better by allocating and passing a MemorySegment but this is to unblock APIs in JNI mode specifically. * jextract/jni: handle UnsafeRawBufferPointer params This allows passing a Java byte array to an `UnsafeRawBufferPointer` accepting API in Swift. This conversion needs to copy a bunch but makes pretty much sense. This is in JNI mode specifically. In FFM mode we could do better by allocating and passing a MemorySegment but this is to unblock APIs in JNI mode specifically. * formatting * Update Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift
1 parent 04cf92c commit f5b6c66

7 files changed

Lines changed: 220 additions & 3 deletions

File tree

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2026 Apple Inc. and the Swift.org project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of Swift.org project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import SwiftJava
16+
17+
/// Sum all bytes in the buffer
18+
public func sumOfBytes(data: UnsafeRawBufferPointer) -> Int64 {
19+
var sum: Int64 = 0
20+
for byte in data {
21+
sum += Int64(byte)
22+
}
23+
return sum
24+
}
25+
26+
/// Return the count of bytes in the buffer
27+
public func bufferCount(data: UnsafeRawBufferPointer) -> Int64 {
28+
Int64(data.count)
29+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2026 Apple Inc. and the Swift.org project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of Swift.org project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
package com.example.swift;
16+
17+
import org.junit.jupiter.api.Test;
18+
19+
import static org.junit.jupiter.api.Assertions.*;
20+
21+
public class UnsafeRawBufferPointerTest {
22+
@Test
23+
void sumOfBytes() {
24+
byte[] input = new byte[] { 1, 2, 3, 4, 5 };
25+
assertEquals(15, MySwiftLibrary.sumOfBytes(input));
26+
}
27+
28+
@Test
29+
void sumOfBytes_empty() {
30+
byte[] input = new byte[] {};
31+
assertEquals(0, MySwiftLibrary.sumOfBytes(input));
32+
}
33+
34+
@Test
35+
void bufferCount() {
36+
byte[] input = new byte[] { 10, 20, 30, 40 };
37+
assertEquals(4, MySwiftLibrary.bufferCount(input));
38+
}
39+
40+
@Test
41+
void bufferCount_empty() {
42+
byte[] input = new byte[] {};
43+
assertEquals(0, MySwiftLibrary.bufferCount(input));
44+
}
45+
}

Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,7 @@ extension JNISwift2JavaGenerator {
242242
let javaName = javaIdentifiers.makeJavaMethodName(decl)
243243

244244
// Swift -> Java
245-
var translatedFunctionSignature = try translate(
245+
var translatedFunctionSignature = try self.translate(
246246
functionSignature: decl.functionSignature,
247247
methodName: javaName,
248248
parentName: parentName,
@@ -509,6 +509,12 @@ extension JNISwift2JavaGenerator {
509509
case .foundationData, .essentialsData:
510510
break // Handled as wrapped struct
511511

512+
case .unsafeRawBufferPointer, .unsafeMutableRawBufferPointer:
513+
return TranslatedParameter(
514+
parameter: JavaParameter(name: parameterName, type: .array(.byte)),
515+
conversion: .placeholder
516+
)
517+
512518
case .foundationUUID, .essentialsUUID:
513519
return TranslatedParameter(
514520
parameter: JavaParameter(name: parameterName, type: .javaUtilUUID),

Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,16 @@ extension JNISwift2JavaGenerator {
125125
parameterName: parameterName
126126
)
127127

128+
case .unsafeRawBufferPointer, .unsafeMutableRawBufferPointer:
129+
return NativeParameter(
130+
parameters: [
131+
JavaParameter(name: parameterName, type: .array(.byte))
132+
],
133+
conversion: .jniByteArrayToUnsafeRawBufferPointer(.placeholder, name: parameterName),
134+
indirectConversion: nil,
135+
conversionCheck: nil
136+
)
137+
128138
case .foundationDate, .essentialsDate, .foundationData, .essentialsData:
129139
// Handled as wrapped struct
130140
break
@@ -1154,6 +1164,9 @@ extension JNISwift2JavaGenerator {
11541164
/// `SwiftType(inner)`
11551165
indirect case labelessInitializer(NativeSwiftConversionStep, swiftType: SwiftType)
11561166

1167+
/// Converts a jbyteArray to UnsafeRawBufferPointer via GetByteArrayElements
1168+
indirect case jniByteArrayToUnsafeRawBufferPointer(NativeSwiftConversionStep, name: String)
1169+
11571170
/// Constructs a Swift tuple from individually-converted elements.
11581171
/// E.g. `(label0: conv0, conv1)` for `(label0: Int, String)`
11591172
indirect case tupleConstruct(elements: [(label: String?, conversion: NativeSwiftConversionStep)])
@@ -1713,6 +1726,21 @@ extension JNISwift2JavaGenerator {
17131726
let inner = inner.render(&printer, placeholder)
17141727
return "\(swiftType)(\(inner))"
17151728

1729+
case .jniByteArrayToUnsafeRawBufferPointer(let inner, let name):
1730+
let inner = inner.render(&printer, placeholder)
1731+
let countVar = "\(name)$count"
1732+
let ptrVar = "\(name)$ptr"
1733+
let rbpVar = "\(name)$rbp"
1734+
printer.print(
1735+
"""
1736+
let \(countVar) = Int(environment.interface.GetArrayLength(environment, \(inner)))
1737+
let \(ptrVar) = environment.interface.GetByteArrayElements(environment, \(inner), nil)!
1738+
defer { environment.interface.ReleaseByteArrayElements(environment, \(inner), \(ptrVar), jint(JNI_ABORT)) }
1739+
let \(rbpVar) = UnsafeRawBufferPointer(start: \(ptrVar), count: \(countVar))
1740+
"""
1741+
)
1742+
return rbpVar
1743+
17161744
case .tupleConstruct(let elements):
17171745
let parts = elements.enumerated().map { idx, element in
17181746
let converted = element.conversion.render(&printer, "\(placeholder)_\(idx)")

Sources/JExtractSwiftLib/JavaTypes/JavaType+JDK.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,4 +58,5 @@ extension JavaType {
5858
static var javaUtilUUID: JavaType {
5959
.class(package: "java.util", name: "UUID")
6060
}
61+
6162
}

Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,8 @@ SwiftJava's `swift-java jextract` tool automates generating Java bindings from S
9393
| Operators: `+`, `-`, user defined |||
9494
| Subscripts: `subscript()` |||
9595
| Equatable |||
96-
| Pointers: `UnsafeRawPointer`, UnsafeBufferPointer (?) | 🟡 ||
96+
| Pointers: `UnsafeRawPointer` | 🟡 ||
97+
| Pointers as parameters: `UnsafeRawBufferPointer` (as `byte[]`) |||
9798
| Nested types: `struct Hello { struct World {} }` |||
9899
| Inheritance: `class Caplin: Capybara` |||
99100
| Non-escaping `Void` closures: `func callMe(maybe: () -> ())` |||
@@ -461,4 +462,4 @@ public final class FishBox ... {
461462
```
462463

463464
> NOTE: Currently no helpers are available to convert between unspecialized types to specialized ones, but this can be offered
464-
> as additional `box.as(FishBox.class)` conversion methods in the future.
465+
> as additional `box.as(FishBox.class)` conversion methods in the future.

Tests/JExtractSwiftTests/ByteArrayTests.swift

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,4 +183,111 @@ final class ByteArrayTests {
183183
expectedChunks: expectedSwiftChunks
184184
)
185185
}
186+
187+
// ==== -----------------------------------------------------------------------
188+
// MARK: JNI mode tests
189+
190+
@Test("Import: accept [UInt8] array (JNI)")
191+
func func_accept_array_uint8_jni() throws {
192+
let text = "public func acceptArray(array: [UInt8])"
193+
try assertOutput(
194+
input: text,
195+
.jni,
196+
.swift,
197+
detectChunkByInitialLines: 2,
198+
expectedChunks: [
199+
"""
200+
@_cdecl("Java_com_example_swift_SwiftModule__00024acceptArray___3B")
201+
public func Java_com_example_swift_SwiftModule__00024acceptArray___3B(environment: UnsafeMutablePointer<JNIEnv?>!, thisClass: jclass, array: jbyteArray?) {
202+
SwiftModule.acceptArray(array: [UInt8](fromJNI: array, in: environment))
203+
}
204+
"""
205+
]
206+
)
207+
try assertOutput(
208+
input: text,
209+
.jni,
210+
.java,
211+
detectChunkByInitialLines: 2,
212+
expectedChunks: [
213+
"""
214+
public static void acceptArray(@Unsigned byte[] array) {
215+
SwiftModule.$acceptArray(Objects.requireNonNull(array, "array must not be null"));
216+
}
217+
""",
218+
"private static native void $acceptArray(byte[] array);",
219+
]
220+
)
221+
}
222+
223+
@Test("Import: return [UInt8] array (JNI)")
224+
func func_return_array_uint8_jni() throws {
225+
let text = "public func returnArray() -> [UInt8]"
226+
try assertOutput(
227+
input: text,
228+
.jni,
229+
.swift,
230+
detectChunkByInitialLines: 2,
231+
expectedChunks: [
232+
"""
233+
@_cdecl("Java_com_example_swift_SwiftModule__00024returnArray__")
234+
public func Java_com_example_swift_SwiftModule__00024returnArray__(environment: UnsafeMutablePointer<JNIEnv?>!, thisClass: jclass) -> jbyteArray? {
235+
return SwiftModule.returnArray().getJNILocalRefValue(in: environment)
236+
}
237+
"""
238+
]
239+
)
240+
try assertOutput(
241+
input: text,
242+
.jni,
243+
.java,
244+
detectChunkByInitialLines: 2,
245+
expectedChunks: [
246+
"""
247+
@Unsigned
248+
public static byte[] returnArray() {
249+
return SwiftModule.$returnArray();
250+
}
251+
""",
252+
"private static native byte[] $returnArray();",
253+
]
254+
)
255+
}
256+
257+
@Test("Import: accept UnsafeRawBufferPointer (JNI)")
258+
func func_accept_unsafeRawBufferPointer_jni() throws {
259+
let text = "public func receiveBuffer(data: UnsafeRawBufferPointer)"
260+
try assertOutput(
261+
input: text,
262+
.jni,
263+
.swift,
264+
detectChunkByInitialLines: 2,
265+
expectedChunks: [
266+
"""
267+
@_cdecl("Java_com_example_swift_SwiftModule__00024receiveBuffer___3B")
268+
public func Java_com_example_swift_SwiftModule__00024receiveBuffer___3B(environment: UnsafeMutablePointer<JNIEnv?>!, thisClass: jclass, data: jbyteArray?) {
269+
let data$count = Int(environment.interface.GetArrayLength(environment, data))
270+
let data$ptr = environment.interface.GetByteArrayElements(environment, data, nil)!
271+
defer { environment.interface.ReleaseByteArrayElements(environment, data, data$ptr, jint(JNI_ABORT)) }
272+
let data$rbp = UnsafeRawBufferPointer(start: data$ptr, count: data$count)
273+
SwiftModule.receiveBuffer(data: data$rbp)
274+
}
275+
"""
276+
]
277+
)
278+
try assertOutput(
279+
input: text,
280+
.jni,
281+
.java,
282+
detectChunkByInitialLines: 2,
283+
expectedChunks: [
284+
"""
285+
public static void receiveBuffer(byte[] data) {
286+
SwiftModule.$receiveBuffer(data);
287+
}
288+
""",
289+
"private static native void $receiveBuffer(byte[] data);",
290+
]
291+
)
292+
}
186293
}

0 commit comments

Comments
 (0)