Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
.sdkmanrc

.DS_Store
.metals
.build
.idea
.vscode
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ public func sumAllByteArrayElements(actuallyAnArray: UnsafeRawPointer, count: In
public func sumAllByteArrayElements(array: [UInt8]) -> Int {
return Int(array.reduce(0, { partialResult, element in partialResult + element }))
}
public func returnSwiftArray() -> [UInt8] {
return [1, 2, 3, 4]
}

public func withArray(body: ([UInt8]) -> Void) {
body([1, 2, 3])
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2025 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.*;
import org.swift.swiftkit.ffm.*;

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

import java.lang.foreign.ValueLayout;
import java.util.Arrays;
import java.util.concurrent.atomic.AtomicLong;
import java.util.stream.IntStream;

public class FFMArraysTest {

@Test
void test_sumAllByteArrayElements_throughMemorySegment() {
byte[] bytes = new byte[124];
Arrays.fill(bytes, (byte) 1);

try (var arena = AllocatingSwiftArena.ofConfined()) {
// NOTE: We cannot use MemorySegment.ofArray because that creates a HEAP backed segment and therefore cannot pass into native:
// java.lang.IllegalArgumentException: Heap segment not allowed: MemorySegment{ kind: heap, heapBase: [B@5b6ec132, address: 0x0, byteSize: 124 }
// MemorySegment bytesSegment = MemorySegment.ofArray(bytes); // NO COPY (!)
// MySwiftLibrary.sumAllByteArrayElements(bytesSegment, bytes.length);

var bytesCopy = arena.allocateFrom(ValueLayout.JAVA_BYTE, bytes);
var swiftSideSum = MySwiftLibrary.sumAllByteArrayElements(bytesCopy, bytes.length);

int javaSideSum = IntStream.range(0, bytes.length).map(i -> bytes[i]).sum();
assertEquals(javaSideSum, swiftSideSum);
}
}

@Test
void test_sumAllByteArrayElements_arrayCopy() {
byte[] bytes = new byte[124];
Arrays.fill(bytes, (byte) 1);

var swiftSideSum = MySwiftLibrary.sumAllByteArrayElements(bytes);

int javaSideSum = IntStream.range(0, bytes.length).map(i -> bytes[i]).sum();
assertEquals(javaSideSum, swiftSideSum);
}

@Test
void test_getArray() {
AtomicLong bufferSize = new AtomicLong();
byte[] javaBytes = MySwiftLibrary.getArray(); // automatically converted [UInt8] to byte[]

assertArrayEquals(new byte[]{1, 2, 3}, javaBytes);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,14 @@

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

import java.lang.foreign.ValueLayout;
import java.lang.foreign.*;
import java.lang.invoke.MethodHandle;
import java.util.Arrays;
import java.util.concurrent.atomic.AtomicLong;
import java.util.stream.IntStream;

public class WithBufferTest {

@Test
void test_withBuffer() {
AtomicLong bufferSize = new AtomicLong();
Expand All @@ -37,24 +39,4 @@ void test_withBuffer() {
assertEquals(124, bufferSize.get());
}

@Test
void test_sumAllByteArrayElements_throughMemorySegment() {
byte[] bytes = new byte[124];
Arrays.fill(bytes, (byte) 1);

try (var arena = AllocatingSwiftArena.ofConfined()) {
// NOTE: We cannot use MemorySegment.ofArray because that creates a HEAP backed segment and therefore cannot pass into native:
// java.lang.IllegalArgumentException: Heap segment not allowed: MemorySegment{ kind: heap, heapBase: [B@5b6ec132, address: 0x0, byteSize: 124 }
// MemorySegment bytesSegment = MemorySegment.ofArray(bytes); // NO COPY (!)
// MySwiftLibrary.sumAllByteArrayElements(bytesSegment, bytes.length);

var bytesCopy = arena.allocateFrom(ValueLayout.JAVA_BYTE, bytes);
var swiftSideSum = MySwiftLibrary.sumAllByteArrayElements(bytesCopy, bytes.length);

System.out.println("swiftSideSum = " + swiftSideSum);

int javaSideSum = IntStream.range(0, bytes.length).map(i -> bytes[i]).sum();
assertEquals(javaSideSum, swiftSideSum);
}
}
}
11 changes: 9 additions & 2 deletions Sources/JExtractSwiftLib/Common/TypeAnnotations.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,15 @@ import SwiftJavaConfigurationShared
/// Determine if the given type needs any extra annotations that should be included
/// in Java sources when the corresponding Java type is rendered.
func getTypeAnnotations(swiftType: SwiftType, config: Configuration) -> [JavaAnnotation] {
if swiftType.isUnsignedInteger, config.effectiveUnsignedNumbersMode == .annotate {
return [JavaAnnotation.unsigned]
if config.effectiveUnsignedNumbersMode == .annotate {
switch swiftType {
case .array(let wrapped) where wrapped.isUnsignedInteger:
return [JavaAnnotation.unsigned]
case _ where swiftType.isUnsignedInteger:
return [JavaAnnotation.unsigned]
default:
break
}
}

return []
Expand Down
10 changes: 10 additions & 0 deletions Sources/JExtractSwiftLib/Convenience/String+Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,13 @@ extension String {
return .class(package: javaPackageName, name: javaClassName)
}
}

extension Array where Element == String {
func joinedJavaStatements(indent: Int) -> String {
if self.count == 1 {
return "\(self.first!);"
}
let indentation = String(repeating: " ", count: indent)
return self.joined(separator: ";\n\(indentation)")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -124,10 +124,12 @@ extension SwiftKnownTypeDeclKind {
case .unsafeRawPointer: .pointer(
.qualified(const: true, volatile: false, type: .void)
)
case .array:
.pointer(.qualified(const: false, volatile: false, type: .void))
case .void: .void
case .unsafePointer, .unsafeMutablePointer, .unsafeRawBufferPointer, .unsafeMutableRawBufferPointer,
.unsafeBufferPointer, .unsafeMutableBufferPointer, .string, .foundationData, .foundationDataProtocol,
.essentialsData, .essentialsDataProtocol, .optional, .array:
.essentialsData, .essentialsDataProtocol, .optional:
nil
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,45 @@ struct CdeclLowering {
case .composite:
throw LoweringError.unhandledType(type)

case .array(let wrapped) where wrapped == knownTypes.uint8:
// Lower an array as 'address' raw pointer and 'count' integer
let cdeclParameters = [
SwiftParameter(
convention: .byValue,
parameterName: "\(parameterName)_pointer",
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have naming inconsistency; these should be $ and not _ but seems we got this inconsistency already from the Data work -- lets fix it separately, since this made the PR kinda big

type: knownTypes.unsafeRawPointer
),
SwiftParameter(
convention: .byValue,
parameterName: "\(parameterName)_count",
type: knownTypes.int
),
]

let bufferPointerInit = ConversionStep.initialize(
knownTypes.unsafeRawBufferPointer,
arguments: [
LabeledArgument(
label: "start",
argument: .explodedComponent(.placeholder, component: "pointer")
),
LabeledArgument(
label: "count",
argument: .explodedComponent(.placeholder, component: "count")
),
]
)

let arrayInit = ConversionStep.initialize(
type,
arguments: [LabeledArgument(argument: bufferPointerInit)]
)

return LoweredParameter(
cdeclParameters: cdeclParameters,
conversion: arrayInit
)

case .array:
throw LoweringError.unhandledType(type)
}
Expand Down Expand Up @@ -525,6 +564,24 @@ struct CdeclLowering {
}
}

/// Create "out" parameter names when we're returning an array-like result.
fileprivate func makeBufferIndirectReturnParameters(_ outParameterName: String, isMutable: Bool) -> [SwiftParameter] {
[
SwiftParameter(
convention: .byValue,
parameterName: "\(outParameterName)_pointer",
type: knownTypes.unsafeMutablePointer(
.optional(isMutable ? knownTypes.unsafeMutableRawPointer : knownTypes.unsafeRawPointer)
)
),
SwiftParameter(
convention: .byValue,
parameterName: "\(outParameterName)_count",
type: knownTypes.unsafeMutablePointer(knownTypes.int)
),
]
}

/// Lower a Swift result type to cdecl out parameters and return type.
///
/// - Parameters:
Expand Down Expand Up @@ -580,20 +637,7 @@ struct CdeclLowering {
let isMutable = knownType == .unsafeMutableRawBufferPointer
return LoweredResult(
cdeclResultType: .void,
cdeclOutParameters: [
SwiftParameter(
convention: .byValue,
parameterName: "\(outParameterName)_pointer",
type: knownTypes.unsafeMutablePointer(
.optional(isMutable ? knownTypes.unsafeMutableRawPointer : knownTypes.unsafeRawPointer)
)
),
SwiftParameter(
convention: .byValue,
parameterName: "\(outParameterName)_count",
type: knownTypes.unsafeMutablePointer(knownTypes.int)
),
],
cdeclOutParameters: makeBufferIndirectReturnParameters(outParameterName, isMutable: isMutable),
conversion: .aggregate([
.populatePointer(
name: "\(outParameterName)_pointer",
Expand Down Expand Up @@ -672,6 +716,37 @@ struct CdeclLowering {
cdeclOutParameters: parameters,
conversion: .tupleExplode(conversions, name: outParameterName)
)

case .array(let wrapped) where wrapped == knownTypes.uint8:
let resultName = "_result"

return LoweredResult(
cdeclResultType: .void, // we call into the _result_initialize instead
cdeclOutParameters: [
SwiftParameter(
convention: .byValue,
parameterName: "\(outParameterName)_initialize",
type: knownTypes.functionInitializeByteBuffer
)
],
conversion: .aggregate([
.method(base: resultName, methodName: "withUnsafeBufferPointer", arguments: [
.init(argument:
.closureLowering(
parameters: [.placeholder],
result: .method(
base: "\(outParameterName)_initialize",
methodName: nil, // just `(...)` apply the closure
arguments: [
.init(label: nil, argument: .member(.constant("_0"), member: "baseAddress!")),
.init(label: nil, argument: .member(.constant("_0"), member: "count")),
]
)
)
)
])
], name: resultName)
)

case .genericParameter, .function, .optional, .existential, .opaque, .composite, .array:
throw LoweringError.unhandledType(type)
Expand Down
36 changes: 36 additions & 0 deletions Sources/JExtractSwiftLib/FFM/ConversionStep.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import SwiftSyntaxBuilder
enum ConversionStep: Equatable {
/// The value being lowered.
case placeholder

case constant(String)

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

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

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

/// Call a method with provided parameters.
indirect case method(base: String?, methodName: String?, arguments: [LabeledArgument<ConversionStep>])

indirect case optionalChain(ConversionStep)

/// Count the number of times that the placeholder occurs within this
Expand All @@ -77,8 +83,12 @@ enum ConversionStep: Equatable {
inner.placeholderCount
case .initialize(_, arguments: let arguments):
arguments.reduce(0) { $0 + $1.argument.placeholderCount }
case .method(_, _, let arguments):
arguments.reduce(0) { $0 + $1.argument.placeholderCount }
case .placeholder, .tupleExplode, .closureLowering:
1
case .constant:
0
case .tuplify(let elements), .aggregate(let elements, _):
elements.reduce(0) { $0 + $1.placeholderCount }
}
Expand All @@ -98,6 +108,9 @@ enum ConversionStep: Equatable {
case .placeholder:
return "\(raw: placeholder)"

case .constant(let name):
return "\(raw: name)"

case .explodedComponent(let step, component: let component):
return step.asExprSyntax(placeholder: "\(placeholder)_\(component)", bodyItems: &bodyItems)

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

case .method(let base, let methodName, let arguments):
// TODO: this is duplicated, try to dedupe it a bit
let renderedArguments: [String] = arguments.map { labeledArgument in
let argExpr = labeledArgument.argument.asExprSyntax(placeholder: placeholder, bodyItems: &bodyItems)
return LabeledExprSyntax(label: labeledArgument.label, expression: argExpr!).description
}

// FIXME: Should be able to use structured initializers here instead of splatting out text.
let renderedArgumentList = renderedArguments.joined(separator: ", ")

let methodApply: String =
if let methodName {
".\(methodName)"
} else {
"" // this is equivalent to calling `base(...)`
}

if let base {
return "\(raw: base)\(raw: methodApply)(\(raw: renderedArgumentList))"
} else {
return "\(raw: methodApply)(\(raw: renderedArgumentList))"
}

case .aggregate(let steps, let name):
let toExplode: String
if let name {
Expand Down
Loading
Loading