Skip to content

Commit 5ff12b1

Browse files
authored
jextract/ffm: Support [UInt8] parameters (#477)
1 parent 5e874ab commit 5ff12b1

File tree

17 files changed

+731
-95
lines changed

17 files changed

+731
-95
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
.sdkmanrc
33

44
.DS_Store
5+
.metals
56
.build
67
.idea
78
.vscode

Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftLibrary.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,9 @@ public func sumAllByteArrayElements(actuallyAnArray: UnsafeRawPointer, count: In
7676
public func sumAllByteArrayElements(array: [UInt8]) -> Int {
7777
return Int(array.reduce(0, { partialResult, element in partialResult + element }))
7878
}
79+
public func returnSwiftArray() -> [UInt8] {
80+
return [1, 2, 3, 4]
81+
}
7982

8083
public func withArray(body: ([UInt8]) -> Void) {
8184
body([1, 2, 3])
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2025 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+
import org.swift.swiftkit.core.*;
19+
import org.swift.swiftkit.ffm.*;
20+
21+
import static org.junit.jupiter.api.Assertions.*;
22+
23+
import java.lang.foreign.ValueLayout;
24+
import java.util.Arrays;
25+
import java.util.concurrent.atomic.AtomicLong;
26+
import java.util.stream.IntStream;
27+
28+
public class FFMArraysTest {
29+
30+
@Test
31+
void test_sumAllByteArrayElements_throughMemorySegment() {
32+
byte[] bytes = new byte[124];
33+
Arrays.fill(bytes, (byte) 1);
34+
35+
try (var arena = AllocatingSwiftArena.ofConfined()) {
36+
// NOTE: We cannot use MemorySegment.ofArray because that creates a HEAP backed segment and therefore cannot pass into native:
37+
// java.lang.IllegalArgumentException: Heap segment not allowed: MemorySegment{ kind: heap, heapBase: [B@5b6ec132, address: 0x0, byteSize: 124 }
38+
// MemorySegment bytesSegment = MemorySegment.ofArray(bytes); // NO COPY (!)
39+
// MySwiftLibrary.sumAllByteArrayElements(bytesSegment, bytes.length);
40+
41+
var bytesCopy = arena.allocateFrom(ValueLayout.JAVA_BYTE, bytes);
42+
var swiftSideSum = MySwiftLibrary.sumAllByteArrayElements(bytesCopy, bytes.length);
43+
44+
int javaSideSum = IntStream.range(0, bytes.length).map(i -> bytes[i]).sum();
45+
assertEquals(javaSideSum, swiftSideSum);
46+
}
47+
}
48+
49+
@Test
50+
void test_sumAllByteArrayElements_arrayCopy() {
51+
byte[] bytes = new byte[124];
52+
Arrays.fill(bytes, (byte) 1);
53+
54+
var swiftSideSum = MySwiftLibrary.sumAllByteArrayElements(bytes);
55+
56+
int javaSideSum = IntStream.range(0, bytes.length).map(i -> bytes[i]).sum();
57+
assertEquals(javaSideSum, swiftSideSum);
58+
}
59+
60+
@Test
61+
void test_getArray() {
62+
AtomicLong bufferSize = new AtomicLong();
63+
byte[] javaBytes = MySwiftLibrary.getArray(); // automatically converted [UInt8] to byte[]
64+
65+
assertArrayEquals(new byte[]{1, 2, 3}, javaBytes);
66+
}
67+
}

Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/WithBufferTest.java

Lines changed: 3 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,14 @@
2020

2121
import static org.junit.jupiter.api.Assertions.*;
2222

23-
import java.lang.foreign.ValueLayout;
23+
import java.lang.foreign.*;
24+
import java.lang.invoke.MethodHandle;
2425
import java.util.Arrays;
2526
import java.util.concurrent.atomic.AtomicLong;
2627
import java.util.stream.IntStream;
2728

2829
public class WithBufferTest {
30+
2931
@Test
3032
void test_withBuffer() {
3133
AtomicLong bufferSize = new AtomicLong();
@@ -37,24 +39,4 @@ void test_withBuffer() {
3739
assertEquals(124, bufferSize.get());
3840
}
3941

40-
@Test
41-
void test_sumAllByteArrayElements_throughMemorySegment() {
42-
byte[] bytes = new byte[124];
43-
Arrays.fill(bytes, (byte) 1);
44-
45-
try (var arena = AllocatingSwiftArena.ofConfined()) {
46-
// NOTE: We cannot use MemorySegment.ofArray because that creates a HEAP backed segment and therefore cannot pass into native:
47-
// java.lang.IllegalArgumentException: Heap segment not allowed: MemorySegment{ kind: heap, heapBase: [B@5b6ec132, address: 0x0, byteSize: 124 }
48-
// MemorySegment bytesSegment = MemorySegment.ofArray(bytes); // NO COPY (!)
49-
// MySwiftLibrary.sumAllByteArrayElements(bytesSegment, bytes.length);
50-
51-
var bytesCopy = arena.allocateFrom(ValueLayout.JAVA_BYTE, bytes);
52-
var swiftSideSum = MySwiftLibrary.sumAllByteArrayElements(bytesCopy, bytes.length);
53-
54-
System.out.println("swiftSideSum = " + swiftSideSum);
55-
56-
int javaSideSum = IntStream.range(0, bytes.length).map(i -> bytes[i]).sum();
57-
assertEquals(javaSideSum, swiftSideSum);
58-
}
59-
}
6042
}

Sources/JExtractSwiftLib/Common/TypeAnnotations.swift

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,15 @@ import SwiftJavaConfigurationShared
1818
/// Determine if the given type needs any extra annotations that should be included
1919
/// in Java sources when the corresponding Java type is rendered.
2020
func getTypeAnnotations(swiftType: SwiftType, config: Configuration) -> [JavaAnnotation] {
21-
if swiftType.isUnsignedInteger, config.effectiveUnsignedNumbersMode == .annotate {
22-
return [JavaAnnotation.unsigned]
21+
if config.effectiveUnsignedNumbersMode == .annotate {
22+
switch swiftType {
23+
case .array(let wrapped) where wrapped.isUnsignedInteger:
24+
return [JavaAnnotation.unsigned]
25+
case _ where swiftType.isUnsignedInteger:
26+
return [JavaAnnotation.unsigned]
27+
default:
28+
break
29+
}
2330
}
2431

2532
return []

Sources/JExtractSwiftLib/Convenience/String+Extensions.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,3 +79,13 @@ extension String {
7979
return .class(package: javaPackageName, name: javaClassName)
8080
}
8181
}
82+
83+
extension Array where Element == String {
84+
func joinedJavaStatements(indent: Int) -> String {
85+
if self.count == 1 {
86+
return "\(self.first!);"
87+
}
88+
let indentation = String(repeating: " ", count: indent)
89+
return self.joined(separator: ";\n\(indentation)")
90+
}
91+
}

Sources/JExtractSwiftLib/FFM/CDeclLowering/CRepresentation.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,10 +124,12 @@ extension SwiftKnownTypeDeclKind {
124124
case .unsafeRawPointer: .pointer(
125125
.qualified(const: true, volatile: false, type: .void)
126126
)
127+
case .array:
128+
.pointer(.qualified(const: false, volatile: false, type: .void))
127129
case .void: .void
128130
case .unsafePointer, .unsafeMutablePointer, .unsafeRawBufferPointer, .unsafeMutableRawBufferPointer,
129131
.unsafeBufferPointer, .unsafeMutableBufferPointer, .string, .foundationData, .foundationDataProtocol,
130-
.essentialsData, .essentialsDataProtocol, .optional, .array:
132+
.essentialsData, .essentialsDataProtocol, .optional:
131133
nil
132134
}
133135
}

Sources/JExtractSwiftLib/FFM/CDeclLowering/FFMSwift2JavaGenerator+FunctionLowering.swift

Lines changed: 89 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,45 @@ struct CdeclLowering {
348348
case .composite:
349349
throw LoweringError.unhandledType(type)
350350

351+
case .array(let wrapped) where wrapped == knownTypes.uint8:
352+
// Lower an array as 'address' raw pointer and 'count' integer
353+
let cdeclParameters = [
354+
SwiftParameter(
355+
convention: .byValue,
356+
parameterName: "\(parameterName)_pointer",
357+
type: knownTypes.unsafeRawPointer
358+
),
359+
SwiftParameter(
360+
convention: .byValue,
361+
parameterName: "\(parameterName)_count",
362+
type: knownTypes.int
363+
),
364+
]
365+
366+
let bufferPointerInit = ConversionStep.initialize(
367+
knownTypes.unsafeRawBufferPointer,
368+
arguments: [
369+
LabeledArgument(
370+
label: "start",
371+
argument: .explodedComponent(.placeholder, component: "pointer")
372+
),
373+
LabeledArgument(
374+
label: "count",
375+
argument: .explodedComponent(.placeholder, component: "count")
376+
),
377+
]
378+
)
379+
380+
let arrayInit = ConversionStep.initialize(
381+
type,
382+
arguments: [LabeledArgument(argument: bufferPointerInit)]
383+
)
384+
385+
return LoweredParameter(
386+
cdeclParameters: cdeclParameters,
387+
conversion: arrayInit
388+
)
389+
351390
case .array:
352391
throw LoweringError.unhandledType(type)
353392
}
@@ -525,6 +564,24 @@ struct CdeclLowering {
525564
}
526565
}
527566

567+
/// Create "out" parameter names when we're returning an array-like result.
568+
fileprivate func makeBufferIndirectReturnParameters(_ outParameterName: String, isMutable: Bool) -> [SwiftParameter] {
569+
[
570+
SwiftParameter(
571+
convention: .byValue,
572+
parameterName: "\(outParameterName)_pointer",
573+
type: knownTypes.unsafeMutablePointer(
574+
.optional(isMutable ? knownTypes.unsafeMutableRawPointer : knownTypes.unsafeRawPointer)
575+
)
576+
),
577+
SwiftParameter(
578+
convention: .byValue,
579+
parameterName: "\(outParameterName)_count",
580+
type: knownTypes.unsafeMutablePointer(knownTypes.int)
581+
),
582+
]
583+
}
584+
528585
/// Lower a Swift result type to cdecl out parameters and return type.
529586
///
530587
/// - Parameters:
@@ -580,20 +637,7 @@ struct CdeclLowering {
580637
let isMutable = knownType == .unsafeMutableRawBufferPointer
581638
return LoweredResult(
582639
cdeclResultType: .void,
583-
cdeclOutParameters: [
584-
SwiftParameter(
585-
convention: .byValue,
586-
parameterName: "\(outParameterName)_pointer",
587-
type: knownTypes.unsafeMutablePointer(
588-
.optional(isMutable ? knownTypes.unsafeMutableRawPointer : knownTypes.unsafeRawPointer)
589-
)
590-
),
591-
SwiftParameter(
592-
convention: .byValue,
593-
parameterName: "\(outParameterName)_count",
594-
type: knownTypes.unsafeMutablePointer(knownTypes.int)
595-
),
596-
],
640+
cdeclOutParameters: makeBufferIndirectReturnParameters(outParameterName, isMutable: isMutable),
597641
conversion: .aggregate([
598642
.populatePointer(
599643
name: "\(outParameterName)_pointer",
@@ -672,6 +716,37 @@ struct CdeclLowering {
672716
cdeclOutParameters: parameters,
673717
conversion: .tupleExplode(conversions, name: outParameterName)
674718
)
719+
720+
case .array(let wrapped) where wrapped == knownTypes.uint8:
721+
let resultName = "_result"
722+
723+
return LoweredResult(
724+
cdeclResultType: .void, // we call into the _result_initialize instead
725+
cdeclOutParameters: [
726+
SwiftParameter(
727+
convention: .byValue,
728+
parameterName: "\(outParameterName)_initialize",
729+
type: knownTypes.functionInitializeByteBuffer
730+
)
731+
],
732+
conversion: .aggregate([
733+
.method(base: resultName, methodName: "withUnsafeBufferPointer", arguments: [
734+
.init(argument:
735+
.closureLowering(
736+
parameters: [.placeholder],
737+
result: .method(
738+
base: "\(outParameterName)_initialize",
739+
methodName: nil, // just `(...)` apply the closure
740+
arguments: [
741+
.init(label: nil, argument: .member(.constant("_0"), member: "baseAddress!")),
742+
.init(label: nil, argument: .member(.constant("_0"), member: "count")),
743+
]
744+
)
745+
)
746+
)
747+
])
748+
], name: resultName)
749+
)
675750

676751
case .genericParameter, .function, .optional, .existential, .opaque, .composite, .array:
677752
throw LoweringError.unhandledType(type)

Sources/JExtractSwiftLib/FFM/ConversionStep.swift

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ import SwiftSyntaxBuilder
2121
enum ConversionStep: Equatable {
2222
/// The value being lowered.
2323
case placeholder
24+
25+
case constant(String)
2426

2527
/// A reference to a component in a value that has been exploded, such as
2628
/// a tuple element or part of a buffer pointer.
@@ -60,8 +62,12 @@ enum ConversionStep: Equatable {
6062

6163
indirect case closureLowering(parameters: [ConversionStep], result: ConversionStep)
6264

65+
/// Access a member of the target, e.g. `<target>.member`
6366
indirect case member(ConversionStep, member: String)
6467

68+
/// Call a method with provided parameters.
69+
indirect case method(base: String?, methodName: String?, arguments: [LabeledArgument<ConversionStep>])
70+
6571
indirect case optionalChain(ConversionStep)
6672

6773
/// Count the number of times that the placeholder occurs within this
@@ -77,8 +83,12 @@ enum ConversionStep: Equatable {
7783
inner.placeholderCount
7884
case .initialize(_, arguments: let arguments):
7985
arguments.reduce(0) { $0 + $1.argument.placeholderCount }
86+
case .method(_, _, let arguments):
87+
arguments.reduce(0) { $0 + $1.argument.placeholderCount }
8088
case .placeholder, .tupleExplode, .closureLowering:
8189
1
90+
case .constant:
91+
0
8292
case .tuplify(let elements), .aggregate(let elements, _):
8393
elements.reduce(0) { $0 + $1.placeholderCount }
8494
}
@@ -98,6 +108,9 @@ enum ConversionStep: Equatable {
98108
case .placeholder:
99109
return "\(raw: placeholder)"
100110

111+
case .constant(let name):
112+
return "\(raw: name)"
113+
101114
case .explodedComponent(let step, component: let component):
102115
return step.asExprSyntax(placeholder: "\(placeholder)_\(component)", bodyItems: &bodyItems)
103116

@@ -162,6 +175,29 @@ enum ConversionStep: Equatable {
162175
let inner = step.asExprSyntax(placeholder: placeholder, bodyItems: &bodyItems)
163176
return "\(inner).\(raw: member)"
164177

178+
case .method(let base, let methodName, let arguments):
179+
// TODO: this is duplicated, try to dedupe it a bit
180+
let renderedArguments: [String] = arguments.map { labeledArgument in
181+
let argExpr = labeledArgument.argument.asExprSyntax(placeholder: placeholder, bodyItems: &bodyItems)
182+
return LabeledExprSyntax(label: labeledArgument.label, expression: argExpr!).description
183+
}
184+
185+
// FIXME: Should be able to use structured initializers here instead of splatting out text.
186+
let renderedArgumentList = renderedArguments.joined(separator: ", ")
187+
188+
let methodApply: String =
189+
if let methodName {
190+
".\(methodName)"
191+
} else {
192+
"" // this is equivalent to calling `base(...)`
193+
}
194+
195+
if let base {
196+
return "\(raw: base)\(raw: methodApply)(\(raw: renderedArgumentList))"
197+
} else {
198+
return "\(raw: methodApply)(\(raw: renderedArgumentList))"
199+
}
200+
165201
case .aggregate(let steps, let name):
166202
let toExplode: String
167203
if let name {

0 commit comments

Comments
 (0)