Skip to content

Commit 3feba7f

Browse files
committed
jextract/jni: handle UnsafeRawBufferPointer params and returns
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. We do allow returning an URBP to Java as well, but this is very dubious and scary, so we don't make anything sneaky here and literarily return this as an `SwiftUnsafeRawBufferPointer` that you may be able to do "something" with in java...
1 parent c4a2934 commit 3feba7f

6 files changed

Lines changed: 182 additions & 1 deletion

File tree

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
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+
}
30+
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 + defer
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
}

Tests/JExtractSwiftTests/ByteArrayTests.swift

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,4 +183,75 @@ 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(input: text, .jni, .swift, detectChunkByInitialLines: 2, expectedChunks: [
194+
"""
195+
@_cdecl("Java_com_example_swift_SwiftModule__00024acceptArray___3B")
196+
public func Java_com_example_swift_SwiftModule__00024acceptArray___3B(environment: UnsafeMutablePointer<JNIEnv?>!, thisClass: jclass, array: jbyteArray?) {
197+
SwiftModule.acceptArray(array: [UInt8](fromJNI: array, in: environment))
198+
}
199+
"""
200+
])
201+
try assertOutput(input: text, .jni, .java, detectChunkByInitialLines: 2, expectedChunks: [
202+
"""
203+
public static void acceptArray(@Unsigned byte[] array) {
204+
SwiftModule.$acceptArray(Objects.requireNonNull(array, "array must not be null"));
205+
}
206+
""",
207+
"private static native void $acceptArray(byte[] array);",
208+
])
209+
}
210+
211+
@Test("Import: return [UInt8] array (JNI)")
212+
func func_return_array_uint8_jni() throws {
213+
let text = "public func returnArray() -> [UInt8]"
214+
try assertOutput(input: text, .jni, .swift, detectChunkByInitialLines: 2, expectedChunks: [
215+
"""
216+
@_cdecl("Java_com_example_swift_SwiftModule__00024returnArray__")
217+
public func Java_com_example_swift_SwiftModule__00024returnArray__(environment: UnsafeMutablePointer<JNIEnv?>!, thisClass: jclass) -> jbyteArray? {
218+
return SwiftModule.returnArray().getJNILocalRefValue(in: environment)
219+
}
220+
"""
221+
])
222+
try assertOutput(input: text, .jni, .java, detectChunkByInitialLines: 2, expectedChunks: [
223+
"""
224+
@Unsigned
225+
public static byte[] returnArray() {
226+
return SwiftModule.$returnArray();
227+
}
228+
""",
229+
"private static native byte[] $returnArray();",
230+
])
231+
}
232+
233+
@Test("Import: accept UnsafeRawBufferPointer (JNI)")
234+
func func_accept_unsafeRawBufferPointer_jni() throws {
235+
let text = "public func receiveBuffer(data: UnsafeRawBufferPointer)"
236+
try assertOutput(input: text, .jni, .swift, detectChunkByInitialLines: 2, expectedChunks: [
237+
"""
238+
@_cdecl("Java_com_example_swift_SwiftModule__00024receiveBuffer___3B")
239+
public func Java_com_example_swift_SwiftModule__00024receiveBuffer___3B(environment: UnsafeMutablePointer<JNIEnv?>!, thisClass: jclass, data: jbyteArray?) {
240+
let data$count = Int(environment.interface.GetArrayLength(environment, data))
241+
let data$ptr = environment.interface.GetByteArrayElements(environment, data, nil)!
242+
defer { environment.interface.ReleaseByteArrayElements(environment, data, data$ptr, jint(JNI_ABORT)) }
243+
let data$rbp = UnsafeRawBufferPointer(start: data$ptr, count: data$count)
244+
SwiftModule.receiveBuffer(data: data$rbp)
245+
}
246+
"""
247+
])
248+
try assertOutput(input: text, .jni, .java, detectChunkByInitialLines: 2, expectedChunks: [
249+
"""
250+
public static void receiveBuffer(byte[] data) {
251+
SwiftModule.$receiveBuffer(data);
252+
}
253+
""",
254+
"private static native void $receiveBuffer(byte[] data);",
255+
])
256+
}
186257
}

0 commit comments

Comments
 (0)