diff --git a/.gitignore b/.gitignore index d4e1c5ec4..02563c8e2 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ .sdkmanrc .DS_Store +.metals .build .idea .vscode diff --git a/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftLibrary.swift b/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftLibrary.swift index 9929f888a..c830e9f6c 100644 --- a/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftLibrary.swift +++ b/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftLibrary.swift @@ -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]) diff --git a/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/FFMArraysTest.java b/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/FFMArraysTest.java new file mode 100644 index 000000000..c195f11bf --- /dev/null +++ b/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/FFMArraysTest.java @@ -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); + } +} diff --git a/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/WithBufferTest.java b/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/WithBufferTest.java index 54206423c..9e0654767 100644 --- a/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/WithBufferTest.java +++ b/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/WithBufferTest.java @@ -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(); @@ -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); - } - } } diff --git a/Sources/JExtractSwiftLib/Common/TypeAnnotations.swift b/Sources/JExtractSwiftLib/Common/TypeAnnotations.swift index 0896e4be6..2cb8a0a1d 100644 --- a/Sources/JExtractSwiftLib/Common/TypeAnnotations.swift +++ b/Sources/JExtractSwiftLib/Common/TypeAnnotations.swift @@ -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 [] diff --git a/Sources/JExtractSwiftLib/Convenience/String+Extensions.swift b/Sources/JExtractSwiftLib/Convenience/String+Extensions.swift index 5641c7f23..87578c142 100644 --- a/Sources/JExtractSwiftLib/Convenience/String+Extensions.swift +++ b/Sources/JExtractSwiftLib/Convenience/String+Extensions.swift @@ -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)") + } +} diff --git a/Sources/JExtractSwiftLib/FFM/CDeclLowering/CRepresentation.swift b/Sources/JExtractSwiftLib/FFM/CDeclLowering/CRepresentation.swift index 3f821a1b4..6b0e33991 100644 --- a/Sources/JExtractSwiftLib/FFM/CDeclLowering/CRepresentation.swift +++ b/Sources/JExtractSwiftLib/FFM/CDeclLowering/CRepresentation.swift @@ -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 } } diff --git a/Sources/JExtractSwiftLib/FFM/CDeclLowering/FFMSwift2JavaGenerator+FunctionLowering.swift b/Sources/JExtractSwiftLib/FFM/CDeclLowering/FFMSwift2JavaGenerator+FunctionLowering.swift index a7da370b3..8e09c0b0c 100644 --- a/Sources/JExtractSwiftLib/FFM/CDeclLowering/FFMSwift2JavaGenerator+FunctionLowering.swift +++ b/Sources/JExtractSwiftLib/FFM/CDeclLowering/FFMSwift2JavaGenerator+FunctionLowering.swift @@ -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", + 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) } @@ -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: @@ -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", @@ -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) diff --git a/Sources/JExtractSwiftLib/FFM/ConversionStep.swift b/Sources/JExtractSwiftLib/FFM/ConversionStep.swift index f59f739dd..a295676d5 100644 --- a/Sources/JExtractSwiftLib/FFM/ConversionStep.swift +++ b/Sources/JExtractSwiftLib/FFM/ConversionStep.swift @@ -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. @@ -60,8 +62,12 @@ enum ConversionStep: Equatable { indirect case closureLowering(parameters: [ConversionStep], result: ConversionStep) + /// Access a member of the target, e.g. `.member` indirect case member(ConversionStep, member: String) + /// Call a method with provided parameters. + indirect case method(base: String?, methodName: String?, arguments: [LabeledArgument]) + indirect case optionalChain(ConversionStep) /// Count the number of times that the placeholder occurs within this @@ -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 } } @@ -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) @@ -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 { diff --git a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaBindingsPrinting.swift b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaBindingsPrinting.swift index a4485fff0..b1a879515 100644 --- a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaBindingsPrinting.swift +++ b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaBindingsPrinting.swift @@ -63,7 +63,11 @@ extension FFMSwift2JavaGenerator { """ ) printJavaBindingDowncallMethod(&printer, cFunc) - printParameterDescriptorClasses(&printer, cFunc) + if let outCallback = translated.translatedSignature.result.outCallback { + printUpcallParameterDescriptorClasses(&printer, outCallback) + } else { // FIXME: not an "else" + printParameterDescriptorClasses(&printer, cFunc) + } } } @@ -147,11 +151,19 @@ extension FFMSwift2JavaGenerator { switch param.type { case .pointer(.function): let name = "$\(param.name!)" - printFunctionPointerParameterDescriptorClass(&printer, name, param.type) + printFunctionPointerParameterDescriptorClass(&printer, name, param.type, impl: nil) default: continue } } + } + + func printUpcallParameterDescriptorClasses( + _ printer: inout CodePrinter, + _ outCallback: OutCallback + ) { + let name = outCallback.name + printFunctionPointerParameterDescriptorClass(&printer, name, outCallback.cFunc.functionType, impl: outCallback) } /// Print a class describing a function pointer parameter type. @@ -169,13 +181,24 @@ extension FFMSwift2JavaGenerator { /// } /// } /// ``` + /// + /// If a `functionBody` is provided, a `Function$Impl` class will be emitted as well. func printFunctionPointerParameterDescriptorClass( _ printer: inout CodePrinter, _ name: String, - _ cType: CType + _ cType: CType, + impl: OutCallback? ) { - guard case .pointer(.function(let cResultType, let cParameterTypes, variadic: false)) = cType else { - preconditionFailure("must be a C function pointer type; name=\(name), cType=\(cType)") + let cResultType: CType + let cParameterTypes: [CType] + if case .pointer(.function(let _cResultType, let _cParameterTypes, variadic: false)) = cType { + cResultType = _cResultType + cParameterTypes = _cParameterTypes + } else if case .function(let _cResultType, let _cParameterTypes, variadic: false) = cType { + cResultType = _cResultType + cParameterTypes = _cParameterTypes + } else { + fatalError("must be a C function (pointer) type; name=\(name), cType=\(cType)") } let cParams = cParameterTypes.enumerated().map { i, ty in @@ -201,6 +224,20 @@ extension FFMSwift2JavaGenerator { } """ ) + + if let impl { + printer.print( + """ + public final static class Function$Impl implements Function { + \(impl.members.joinedJavaStatements(indent: 2)) + public \(cResultType.javaType) apply(\(paramDecls.joined(separator: ", "))) { + \(impl.body) + } + } + """ + ) + } + printFunctionDescriptorDefinition(&printer, cResultType, cParams) printer.print( """ @@ -419,26 +456,52 @@ extension FFMSwift2JavaGenerator { downCallArguments.append(varName) } - //=== Part 3: Downcall. let thunkName = thunkNameRegistry.functionThunkName(decl: decl) + + if let outCallback = translatedSignature.result.outCallback { + let funcName = outCallback.name + assert(funcName.first == "$", "OutCallback names must start with $") + let varName = funcName.dropFirst() + downCallArguments.append( + """ + \(thunkName).\(outCallback.name).toUpcallStub(\(varName), arena$) + """ + ) + } + + //=== Part 3: Downcall. let downCall = "\(thunkName).call(\(downCallArguments.joined(separator: ", ")))" //=== Part 4: Convert the return value. if translatedSignature.result.javaResultType == .void { + // Trivial downcall with no conversion needed, no callback either printer.print("\(downCall);") } else { let placeholder: String - if translatedSignature.result.outParameters.isEmpty { + let placeholderForDowncall: String? + + if let outCallback = translatedSignature.result.outCallback { + placeholder = "\(outCallback.name)" // the result will be read out from the _result_initialize java class + placeholderForDowncall = "\(downCall)" + } else if translatedSignature.result.outParameters.isEmpty { placeholder = downCall + placeholderForDowncall = nil } else { // FIXME: Support cdecl thunk returning a value while populating the out parameters. printer.print("\(downCall);") + placeholderForDowncall = nil placeholder = "_result" } - let result = translatedSignature.result.conversion.render(&printer, placeholder) + let result = translatedSignature.result.conversion.render(&printer, placeholder, placeholderForDowncall: placeholderForDowncall) if translatedSignature.result.javaResultType != .void { - printer.print("return \(result);") + switch translatedSignature.result.conversion { + case .initializeResultWithUpcall(_, let extractResult): + printer.print("\(result);") // the result in the callback situation is a series of setup steps + printer.print("return \(extractResult.render(&printer, placeholder));") // extract the actual result + default: + printer.print("return \(result);") + } } else { printer.print("\(result);") } @@ -465,16 +528,31 @@ extension FFMSwift2JavaGenerator.JavaConversionStep { /// Whether the conversion uses SwiftArena. var requiresSwiftArena: Bool { switch self { - case .placeholder, .explodedName, .constant, .readMemorySegment: + case .placeholder, .placeholderForDowncall, .placeholderForSwiftThunkName: + return false + case .explodedName, .constant, .readMemorySegment, .javaNew: return false case .constructSwiftValue, .wrapMemoryAddressUnsafe: return true + case .temporaryArena: + return true + + case .initializeResultWithUpcall(let steps, let result): + return steps.contains { $0.requiresSwiftArena } || result.requiresSwiftArena + case .introduceVariable(_, let value): + return value.requiresSwiftArena - case .call(let inner, _, _), .cast(let inner, _), .construct(let inner, _), - .method(let inner, _, _, _), .swiftValueSelfSegment(let inner): + case .call(let inner, let base, _, _): + return inner.requiresSwiftArena || (base?.requiresSwiftArena == true) + + case .cast(let inner, _), + .construct(let inner, _), + .method(let inner, _, _, _), + .property(let inner, _), + .swiftValueSelfSegment(let inner): return inner.requiresSwiftArena - case .commaSeparated(let list): + case .commaSeparated(let list, _): return list.contains(where: { $0.requiresSwiftArena }) } } @@ -482,68 +560,125 @@ extension FFMSwift2JavaGenerator.JavaConversionStep { /// Whether the conversion uses temporary Arena. var requiresTemporaryArena: Bool { switch self { - case .placeholder, .explodedName, .constant: + case .placeholder, .placeholderForDowncall, .placeholderForSwiftThunkName: + return false + case .explodedName, .constant, .javaNew: return false + case .temporaryArena: + return true case .readMemorySegment: return true + case .initializeResultWithUpcall: + return true + case .introduceVariable(_, let value): + return value.requiresTemporaryArena case .cast(let inner, _), .construct(let inner, _), .constructSwiftValue(let inner, _), .swiftValueSelfSegment(let inner), .wrapMemoryAddressUnsafe(let inner, _): return inner.requiresSwiftArena - case .call(let inner, _, let withArena): - return withArena || inner.requiresTemporaryArena + case .call(let inner, let base, _, let withArena): + return withArena || (base?.requiresTemporaryArena == true) || inner.requiresTemporaryArena case .method(let inner, _, let args, let withArena): return withArena || inner.requiresTemporaryArena || args.contains(where: { $0.requiresTemporaryArena }) - case .commaSeparated(let list): + case .property(let inner, _): + return inner.requiresTemporaryArena + case .commaSeparated(let list, _): return list.contains(where: { $0.requiresTemporaryArena }) } } /// Returns the conversion string applied to the placeholder. - func render(_ printer: inout CodePrinter, _ placeholder: String) -> String { + func render(_ printer: inout CodePrinter, _ placeholder: String, placeholderForDowncall: String? = nil) -> String { // NOTE: 'printer' is used if the conversion wants to cause side-effects. // E.g. storing a temporary values into a variable. switch self { case .placeholder: return placeholder + case .placeholderForDowncall: + if let placeholderForDowncall { + return "\(placeholderForDowncall)" + } else { + return "/*placeholderForDowncall undefined!*/" + } + case .placeholderForSwiftThunkName: + if let placeholderForDowncall { + let downcall = "\(placeholderForDowncall)" + return String(downcall[..<(downcall.firstIndex(of: ".") ?? downcall.endIndex)]) // . separates thunk name from the `.call` + } else { + return "/*placeholderForDowncall undefined!*/" + } + + case .temporaryArena: + return "arena$" + case .explodedName(let component): return "\(placeholder)_\(component)" case .swiftValueSelfSegment: return "\(placeholder).$memorySegment()" - - case .call(let inner, let function, let withArena): + + case .javaNew(let value): + return "new \(value.render(&printer, placeholder, placeholderForDowncall: placeholderForDowncall))" + + case .initializeResultWithUpcall(let steps, _): + // TODO: could we use the printing to introduce the upcall handle instead? + return steps.map { step in + var printer = CodePrinter() + var out = "" + out += step.render(&printer, placeholder, placeholderForDowncall: placeholderForDowncall) + out += printer.contents + return out + }.joined(separator: ";\n") + + case .call(let inner, let base, let function, let withArena): let inner = inner.render(&printer, placeholder) let arenaArg = withArena ? ", arena$" : "" - return "\(function)(\(inner)\(arenaArg))" + let baseStr : String = + if let base { + base.render(&printer, placeholder) + "." + } else { + "" + } + return "\(baseStr)\(function)(\(inner)\(arenaArg))" + // TODO: deduplicate with 'method' case .method(let inner, let methodName, let arguments, let withArena): - let inner = inner.render(&printer, placeholder) - let args = arguments.map { $0.render(&printer, placeholder) } + let inner = inner.render(&printer, placeholder, placeholderForDowncall: placeholderForDowncall) + let args = arguments.map { $0.render(&printer, placeholder, placeholderForDowncall: placeholderForDowncall) } let argsStr = (args + (withArena ? ["arena$"] : [])).joined(separator: " ,") return "\(inner).\(methodName)(\(argsStr))" + case .property(let inner, let propertyName): + let inner = inner.render(&printer, placeholder, placeholderForDowncall: placeholderForDowncall) + return "\(inner).\(propertyName)" + case .constructSwiftValue(let inner, let javaType): - let inner = inner.render(&printer, placeholder) + let inner = inner.render(&printer, placeholder, placeholderForDowncall: placeholderForDowncall) return "new \(javaType.className!)(\(inner), swiftArena$)" case .wrapMemoryAddressUnsafe(let inner, let javaType): - let inner = inner.render(&printer, placeholder) + let inner = inner.render(&printer, placeholder, placeholderForDowncall: placeholderForDowncall) return "\(javaType.className!).wrapMemoryAddressUnsafe(\(inner), swiftArena$)" case .construct(let inner, let javaType): - let inner = inner.render(&printer, placeholder) + let inner = inner.render(&printer, placeholder, placeholderForDowncall: placeholderForDowncall) return "new \(javaType)(\(inner))" + + case .introduceVariable(let name, let value): + let value = value.render(&printer, placeholder, placeholderForDowncall: placeholderForDowncall) + return "var \(name) = \(value);" case .cast(let inner, let javaType): - let inner = inner.render(&printer, placeholder) + let inner = inner.render(&printer, placeholder, placeholderForDowncall: placeholderForDowncall) return "(\(javaType)) \(inner)" - case .commaSeparated(let list): - return list.map({ $0.render(&printer, placeholder)}).joined(separator: ", ") + case .commaSeparated(let list, let separator): + return list.map({ + $0.render(&printer, placeholder, placeholderForDowncall: placeholderForDowncall) + }).joined(separator: separator) case .constant(let value): return value diff --git a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift index 76284b787..d3a2626bb 100644 --- a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift +++ b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift @@ -63,13 +63,26 @@ extension FFMSwift2JavaGenerator { /// 'JavaParameter.name' is the suffix for the receiver variable names. For example /// /// var _result_pointer = MemorySegment.allocate(...) - /// var _result_count = MemroySegment.allocate(...) + /// var _result_count = MemorySegment.allocate(...) /// downCall(_result_pointer, _result_count) /// return constructResult(_result_pointer, _result_count) /// /// This case, there're two out parameter, named '_pointer' and '_count'. var outParameters: [JavaParameter] + /// Similar to out parameters, but instead of parameters we "fill in" in native, + /// we create an upcall handle before the downcall and pass it to the downcall. + /// Swift then invokes the upcall in order to populate some data in Java (our callback). + /// + /// After the call is made, we may need to further extact the result from the called-back-into + /// Java function class, for example: + /// + /// var _result_initialize = new $result_initialize.Function(); + /// downCall($result_initialize.toUpcallHandle(_result_initialize, arena)) + /// return _result_initialize.result + /// + var outCallback: OutCallback? + /// Describes how to construct the Java result from the foreign function return /// value and/or the out parameters. var conversion: JavaConversionStep @@ -298,7 +311,7 @@ extension FFMSwift2JavaGenerator { } // Result. - let result = try self.translate( + let result = try self.translateResult( swiftResult: swiftSignature.result, loweredResult: loweredFunctionSignature.result ) @@ -476,7 +489,24 @@ extension FFMSwift2JavaGenerator { case .composite: throw JavaTranslationError.unhandledType(swiftType) - case .array(let elementType): + case .array(let wrapped) where wrapped == knownTypes.uint8: + return TranslatedParameter( + javaParameters: [ + JavaParameter(name: parameterName, type: .array(.byte), annotations: parameterAnnotations), + ], + conversion: + .commaSeparated([ + .call( + .commaSeparated([.constant("ValueLayout.JAVA_BYTE"), .placeholder]), + base: .temporaryArena, + function: "allocateFrom", + withArena: false // this would pass the arena as last argument, but instead we make a call on the arena + ), + .property(.placeholder, propertyName: "length"), + ]) + ) + + case .array: throw JavaTranslationError.unhandledType(swiftType) } } @@ -595,7 +625,7 @@ extension FFMSwift2JavaGenerator { } /// Translate a Swift API result to the user-facing Java API result. - func translate( + func translateResult( swiftResult: SwiftResult, loweredResult: LoweredResult ) throws -> TranslatedResult { @@ -697,6 +727,46 @@ extension FFMSwift2JavaGenerator { // TODO: Implement. throw JavaTranslationError.unhandledType(swiftType) + case .array(let wrapped) where wrapped == knownTypes.uint8: + return TranslatedResult( + javaResultType: + .array(.byte), + annotations: [.unsigned], + outParameters: [], // no out parameters, but we do an "out" callback + outCallback: OutCallback( + name: "$_result_initialize", + members: [ + "byte[] result = null" + ], + parameters: [ + JavaParameter(name: "pointer", type: .javaForeignMemorySegment), + JavaParameter(name: "count", type: .long), + ], + cFunc: CFunction( + resultType: .void, + name: "apply", + parameters: [ + CParameter(type: .pointer(.void)), + CParameter(type: .integral(.size_t)), + ], + isVariadic: false), + body: "this.result = _0.reinterpret(_1).toArray(ValueLayout.JAVA_BYTE); // copy native Swift array to Java heap array" + ), + conversion: .initializeResultWithUpcall([ + .introduceVariable( + name: "_result_initialize", + initializeWith: .javaNew(.commaSeparated([ + // We need to refer to the nested class that is created for this function. + // The class that contains all the related functional interfaces is called the same + // as the downcall function, so we use the thunk name to find this class/ + .placeholderForSwiftThunkName, .constant("$_result_initialize.Function$Impl()") + ], separator: "."))), + // .constant("var = new \(.placeholderForDowncallThunkName).."), + .placeholderForDowncall, // perform the downcall here + ], + extractResult: .property(.constant("_result_initialize"), propertyName: "result")) + ) + case .genericParameter, .optional, .function, .existential, .opaque, .composite, .array: throw JavaTranslationError.unhandledType(swiftType) } @@ -715,42 +785,86 @@ extension FFMSwift2JavaGenerator { /// Describes how to convert values between Java types and FFM types. enum JavaConversionStep { - // The input + /// The input case placeholder - - // The input exploded into components. + + /// The "downcall", e.g. `swiftjava_SwiftModule_returnArray.call(...)`. + /// This can be used in combination with aggregate conversion steps to prepare a setup and processing of the downcall. + case placeholderForDowncall + + /// Placeholder for Swift thunk name, e.g. "swiftjava_SwiftModule_returnArray". + /// + /// This is derived from the placeholderForDowncall substitution - could be done more cleanly, + /// however this has the benefit of not needing to pass the name substituion separately. + case placeholderForSwiftThunkName + + /// The temporary `arena$` that is necessary to complete the conversion steps. + /// + /// This is distinct from just a constant 'arena$' string, since it forces the creation of a temporary arena. + case temporaryArena + + /// The input exploded into components. case explodedName(component: String) - // A fixed value + /// A fixed value case constant(String) - // 'value.$memorySegment()' + /// The result of the function will be initialized with a callback to Java (an upcall). + /// + /// The `extractResult` is used for the actual `return ...` statement, because we need to extract + /// the return value from the called back into class, e.g. `return _result_initialize.result`. + indirect case initializeResultWithUpcall([JavaConversionStep], extractResult: JavaConversionStep) + + /// 'value.$memorySegment()' indirect case swiftValueSelfSegment(JavaConversionStep) - // call specified function using the placeholder as arguments. - // If `withArena` is true, `arena$` argument is added. - indirect case call(JavaConversionStep, function: String, withArena: Bool) + /// Call specified function using the placeholder as arguments. + /// + /// The 'base' is if the call should be performed as 'base.function', + /// otherwise the function is assumed to be a free function. + /// + /// If `withArena` is true, `arena$` argument is added. + indirect case call(JavaConversionStep, base: JavaConversionStep?, function: String, withArena: Bool) + + static func call(_ step: JavaConversionStep, function: String, withArena: Bool) -> Self { + .call(step, base: nil, function: function, withArena: withArena) + } - // Apply a method on the placeholder. - // If `withArena` is true, `arena$` argument is added. + // TODO: just use make call more powerful and use it instead? + /// Apply a method on the placeholder. + /// If `withArena` is true, `arena$` argument is added. indirect case method(JavaConversionStep, methodName: String, arguments: [JavaConversionStep] = [], withArena: Bool) + + /// Fetch a property from the placeholder. + /// Similar to 'method', however for a property i.e. without adding the '()' after the name + indirect case property(JavaConversionStep, propertyName: String) - // Call 'new \(Type)(\(placeholder), swiftArena$)'. + /// Call 'new \(Type)(\(placeholder), swiftArena$)'. indirect case constructSwiftValue(JavaConversionStep, JavaType) + /// Construct the type using the placeholder as arguments. + indirect case construct(JavaConversionStep, JavaType) + /// Call the `MyType.wrapMemoryAddressUnsafe` in order to wrap a memory address using the Java binding type indirect case wrapMemoryAddressUnsafe(JavaConversionStep, JavaType) - // Construct the type using the placeholder as arguments. - indirect case construct(JavaConversionStep, JavaType) + /// Introduce a local variable, e.g. `var result = new Something()` + indirect case introduceVariable(name: String, initializeWith: JavaConversionStep) - // Casting the placeholder to the certain type. + /// Casting the placeholder to the certain type. indirect case cast(JavaConversionStep, JavaType) - - // Convert the results of the inner steps to a comma separated list. - indirect case commaSeparated([JavaConversionStep]) - - // Refer an exploded argument suffixed with `_\(name)`. + + /// Prefix the conversion step with a java `new`. + /// + /// This is useful if constructing the value is complex and we use + /// a combination of separated values and constants to do so; Generally prefer using `construct` + /// if you only want to construct a "wrapper" for the current `.placeholder`. + indirect case javaNew(JavaConversionStep) + + /// Convert the results of the inner steps to a comma separated list. + indirect case commaSeparated([JavaConversionStep], separator: String = ", ") + + /// Refer an exploded argument suffixed with `_\(name)`. indirect case readMemorySegment(JavaConversionStep, as: JavaType) var isPlaceholder: Bool { diff --git a/Sources/JExtractSwiftLib/JavaParameter.swift b/Sources/JExtractSwiftLib/JavaParameter.swift index 0b243b3a4..34f3b254e 100644 --- a/Sources/JExtractSwiftLib/JavaParameter.swift +++ b/Sources/JExtractSwiftLib/JavaParameter.swift @@ -85,3 +85,19 @@ struct JavaParameter { return "\(annotationsStr) \(type) \(name)" } } + +struct OutCallback { + /// Name of the "function" that we'll use as the out callback / upcall, e.g. "$_upcall_initialize" + var name: String { + willSet { + precondition("\(newValue)".starts(with: "$"), "OutCallback class names should stat with $") + } + } + var members: [String + ] + var parameters: [JavaParameter] + // FIXME: compute this instead + var cFunc: CFunction + var body: String + +} \ No newline at end of file diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownTypeDecls.swift b/Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownTypeDecls.swift index 363663217..5401f0fab 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownTypeDecls.swift +++ b/Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownTypeDecls.swift @@ -30,8 +30,8 @@ enum SwiftKnownTypeDeclKind: String, Hashable { case float = "Swift.Float" case double = "Swift.Double" case unsafeRawPointer = "Swift.UnsafeRawPointer" - case unsafeMutableRawPointer = "Swift.UnsafeMutableRawPointer" case unsafeRawBufferPointer = "Swift.UnsafeRawBufferPointer" + case unsafeMutableRawPointer = "Swift.UnsafeMutableRawPointer" case unsafeMutableRawBufferPointer = "Swift.UnsafeMutableRawBufferPointer" case unsafePointer = "Swift.UnsafePointer" case unsafeMutablePointer = "Swift.UnsafeMutablePointer" diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownTypes.swift b/Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownTypes.swift index 25b1135ae..56c6db464 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownTypes.swift +++ b/Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownTypes.swift @@ -33,12 +33,26 @@ struct SwiftKnownTypes { var float: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.float])) } var double: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.double])) } var unsafeRawPointer: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.unsafeRawPointer])) } + var unsafeRawBufferPointer: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.unsafeRawBufferPointer])) } var unsafeMutableRawPointer: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.unsafeMutableRawPointer])) } - + var foundationDataProtocol: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.foundationDataProtocol])) } var foundationData: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.foundationData])) } var essentialsDataProtocol: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.essentialsDataProtocol])) } var essentialsData: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.essentialsData])) } + + /// `(UnsafeRawPointer, Long) -> ()` function type. + /// + /// Commonly used to initialize a buffer using the passed bytes and length. + var functionInitializeByteBuffer: SwiftType { + .function(SwiftFunctionType( + convention: .c, + parameters: [ + SwiftParameter(convention: .byValue, parameterName: nil, type: self.unsafeRawPointer), // array base pointer + SwiftParameter(convention: .byValue, parameterName: nil, type: self.int), // array length + ], + resultType: .void)) + } func unsafePointer(_ pointeeType: SwiftType) -> SwiftType { .nominal( diff --git a/Tests/JExtractSwiftTests/Asserts/TextAssertions.swift b/Tests/JExtractSwiftTests/Asserts/TextAssertions.swift index 67671548b..301524bbb 100644 --- a/Tests/JExtractSwiftTests/Asserts/TextAssertions.swift +++ b/Tests/JExtractSwiftTests/Asserts/TextAssertions.swift @@ -159,6 +159,13 @@ func assertOutput( currentGotLine += 1 } else { + guard gotLines.count > currentGotLine else { + print("WARNING: index out of bounds when asserting text: \(currentGotLine)") + print("got lines ======") + print(gotLines.joined(separator: "\n").red) + currentExpectedLine += 1 + continue + } let gottenLine = gotLines[currentGotLine].trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) if gottenLine.commonPrefix(with: expectedLine) != expectedLine { diffLineNumbers.append(currentExpectedLine + matchingOutputOffset) diff --git a/Tests/JExtractSwiftTests/ByteArrayTests.swift b/Tests/JExtractSwiftTests/ByteArrayTests.swift new file mode 100644 index 000000000..052157281 --- /dev/null +++ b/Tests/JExtractSwiftTests/ByteArrayTests.swift @@ -0,0 +1,170 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 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 JExtractSwiftLib +import SwiftJavaConfigurationShared +import Testing + +final class ByteArrayTests { + + @Test( + "Import: accept [UInt8] array", + arguments: [ + // TODO: implement JNI mode here + ( + JExtractGenerationMode.ffm, + /* expected Java chunks */ + [ + """ + /** + * {@snippet lang=c : + * void swiftjava_SwiftModule_acceptArray_array(const void *array_pointer, ptrdiff_t array_count) + * } + */ + private static class swiftjava_SwiftModule_acceptArray_array { + private static final FunctionDescriptor DESC = FunctionDescriptor.ofVoid( + /* array_pointer: */SwiftValueLayout.SWIFT_POINTER, + /* array_count: */SwiftValueLayout.SWIFT_INT + ); + """, + """ + /** + * Downcall to Swift: + * {@snippet lang=swift : + * public func acceptArray(array: [UInt8]) + * } + */ + public static void acceptArray(@Unsigned byte[] array) { + try(var arena$ = Arena.ofConfined()) { + swiftjava_SwiftModule_acceptArray_array.call(arena$.allocateFrom(ValueLayout.JAVA_BYTE, array), array.length); + } + } + """ + ], + /* expected Swift chunks */ + [ + """ + @_cdecl("swiftjava_SwiftModule_acceptArray_array") + public func swiftjava_SwiftModule_acceptArray_array(_ array_pointer: UnsafeRawPointer, _ array_count: Int) { + acceptArray(array: [UInt8](UnsafeRawBufferPointer(start: array_pointer, count: array_count))) + } + """ + ] + ) + ] + ) + func func_accept_array_uint8(mode: JExtractGenerationMode, expectedJavaChunks: [String], expectedSwiftChunks: [String]) throws { + let text = + """ + public func acceptArray(array: [UInt8]) + """ + + try assertOutput( + input: text, + mode, .java, + expectedChunks: expectedJavaChunks) + + try assertOutput( + input: text, + mode, .swift, + expectedChunks: expectedSwiftChunks) + } + + @Test( + "Import: return [UInt8] array", + arguments: [ + // TODO: implement JNI mode here + ( + JExtractGenerationMode.ffm, + /* expected Java chunks */ + [ + """ + /** + * {snippet lang=c : + * void (void *, size_t) + * } + */ + private static class $_result_initialize { + @FunctionalInterface + public interface Function { + void apply(java.lang.foreign.MemorySegment _0, long _1); + } + public final static class Function$Impl implements Function { + byte[] result = null; + public void apply(java.lang.foreign.MemorySegment _0, long _1) { + this.result = _0.reinterpret(_1).toArray(ValueLayout.JAVA_BYTE); + } + } + private static final FunctionDescriptor DESC = FunctionDescriptor.ofVoid( + /* _0: */SwiftValueLayout.SWIFT_POINTER, + /* _1: */SwiftValueLayout.SWIFT_INT + ); + private static final MethodHandle HANDLE = SwiftRuntime.upcallHandle(Function.class, "apply", DESC); + private static MemorySegment toUpcallStub(Function fi, Arena arena) { + return Linker.nativeLinker().upcallStub(HANDLE.bindTo(fi), DESC, arena); + } + } + """, + """ + /** + * Downcall to Swift: + * {@snippet lang=swift : + * public func returnArray() -> [UInt8] + * } + */ + @Unsigned + public static byte[] returnArray() { + try(var arena$ = Arena.ofConfined()) { + var _result_initialize = new swiftjava_SwiftModule_returnArray.$_result_initialize.Function$Impl(); + swiftjava_SwiftModule_returnArray.call(swiftjava_SwiftModule_returnArray.$_result_initialize.toUpcallStub(_result_initialize, arena$)); + return _result_initialize.result; + } + } + """, + ], + /* expected Swift chunks */ + [ + """ + @_cdecl("swiftjava_SwiftModule_returnArray") + public func swiftjava_SwiftModule_returnArray(_ _result_initialize: @convention(c) (UnsafeRawPointer, Int) -> ()) { + let _result = returnArray() + _result.withUnsafeBufferPointer({ (_0) in + return _result_initialize(_0.baseAddress!, _0.count) + }) + } + """ + ] + ) + ] + ) + func func_return_array_uint8(mode: JExtractGenerationMode, expectedJavaChunks: [String], expectedSwiftChunks: [String]) throws { + let text = + """ + public func returnArray() -> [UInt8] + """ + + var config = Configuration() + config.logLevel = .trace + + try assertOutput( + input: text, + mode, .java, + expectedChunks: expectedJavaChunks) + + try assertOutput( + input: text, + mode, .swift, + expectedChunks: expectedSwiftChunks) + } +} \ No newline at end of file diff --git a/Tests/JExtractSwiftTests/FFM/FFMSubscriptsTests.swift b/Tests/JExtractSwiftTests/FFM/FFMSubscriptsTests.swift index 89029dd70..7147a5524 100644 --- a/Tests/JExtractSwiftTests/FFM/FFMSubscriptsTests.swift +++ b/Tests/JExtractSwiftTests/FFM/FFMSubscriptsTests.swift @@ -15,9 +15,6 @@ import JExtractSwiftLib import Testing -import JExtractSwiftLib -import Testing - @Suite struct FFMSubscriptsTests { private let noParamsSubscriptSource = """