diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Arrays.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Arrays.swift new file mode 100644 index 000000000..b6f050c12 --- /dev/null +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Arrays.swift @@ -0,0 +1,60 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +import SwiftJava + +public func booleanArray(array: [Bool]) -> [Bool] { + return array +} + +public func byteArray(array: [UInt8]) -> [UInt8] { + return array +} + +public func byteArrayExplicit(array: Array) -> Array { + return array +} + +public func charArray(array: [UInt16]) -> [UInt16] { + return array +} + +public func shortArray(array: [Int16]) -> [Int16] { + return array +} + +public func intArray(array: [Int32]) -> [Int32] { + return array +} + +public func longArray(array: [Int64]) -> [Int64] { + return array +} + +public func floatArray(array: [Float]) -> [Float] { + return array +} + +public func doubleArray(array: [Double]) -> [Double] { + return array +} + +public func stringArray(array: [String]) -> [String] { + return array +} + +public func objectArray(array: [MySwiftClass]) -> [MySwiftClass] { + return array +} + diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ArraysTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ArraysTest.java new file mode 100644 index 000000000..bf19b370e --- /dev/null +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ArraysTest.java @@ -0,0 +1,105 @@ +//===----------------------------------------------------------------------===// +// +// 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.SwiftArena; + +import java.util.OptionalLong; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; + +import static org.junit.jupiter.api.Assertions.*; + +public class ArraysTest { + @Test + void booleanArray() { + boolean[] input = new boolean[] { true, false, false, true }; + assertArrayEquals(input, MySwiftLibrary.booleanArray(input)); + } + + @Test + void byteArray() { + byte[] input = new byte[] { 10, 20, 30, 40 }; + assertArrayEquals(input, MySwiftLibrary.byteArray(input)); + } + + @Test + void byteArray_empty() { + byte[] input = new byte[] {}; + assertArrayEquals(input, MySwiftLibrary.byteArray(input)); + } + + @Test + void byteArray_null() { + assertThrows(NullPointerException.class, () -> MySwiftLibrary.byteArray(null)); + } + + @Test + void byteArrayExplicit() { + byte[] input = new byte[] { 10, 20, 30, 40 }; + assertArrayEquals(input, MySwiftLibrary.byteArrayExplicit(input)); + } + + @Test + void charArray() { + char[] input = new char[] { 10, 20, 30, 40 }; + assertArrayEquals(input, MySwiftLibrary.charArray(input)); + } + + @Test + void shortArray() { + short[] input = new short[] { 10, 20, 30, 40 }; + assertArrayEquals(input, MySwiftLibrary.shortArray(input)); + } + + @Test + void intArray() { + int[] input = new int[] { 10, 20, 30, 40 }; + assertArrayEquals(input, MySwiftLibrary.intArray(input)); + } + + @Test + void longArray() { + long[] input = new long[] { 10, 20, 30, 40 }; + assertArrayEquals(input, MySwiftLibrary.longArray(input)); + } + + @Test + void stringArray() { + String[] input = new String[] { "hey", "there", "my", "friend" }; + assertArrayEquals(input, MySwiftLibrary.stringArray(input)); + } + + @Test + void floatArray() { + float[] input = new float[] { 10, 20, 30, 40 }; + assertArrayEquals(input, MySwiftLibrary.floatArray(input)); + } + + @Test + void doubleArray() { + double[] input = new double[] { 10, 20, 30, 40 }; + assertArrayEquals(input, MySwiftLibrary.doubleArray(input)); + } + + @Test + void objectArray() { + try (var arena = SwiftArena.ofConfined()) { + MySwiftClass[] input = new MySwiftClass[]{MySwiftClass.init(arena), MySwiftClass.init(arena), MySwiftClass.init(arena) }; + assertEquals(3, MySwiftLibrary.objectArray(input, arena).length); + } + } +} \ No newline at end of file diff --git a/Sources/JExtractSwiftLib/CodePrinter.swift b/Sources/JExtractSwiftLib/CodePrinter.swift index e6498683c..1497a61ab 100644 --- a/Sources/JExtractSwiftLib/CodePrinter.swift +++ b/Sources/JExtractSwiftLib/CodePrinter.swift @@ -86,12 +86,17 @@ public struct CodePrinter { public mutating func printBraceBlock( _ header: Any, + parameters: [String]? = nil, function: String = #function, file: String = #fileID, line: UInt = #line, body: (inout CodePrinter) throws -> () ) rethrows { - print("\(header) {") + print("\(header) {", .continue) + if let parameters { + print(" (\(parameters.joined(separator: ", "))) in", .continue) + } + println() indent() try body(&self) outdent() diff --git a/Sources/JExtractSwiftLib/FFM/CDeclLowering/CRepresentation.swift b/Sources/JExtractSwiftLib/FFM/CDeclLowering/CRepresentation.swift index 2621dd8bc..3f821a1b4 100644 --- a/Sources/JExtractSwiftLib/FFM/CDeclLowering/CRepresentation.swift +++ b/Sources/JExtractSwiftLib/FFM/CDeclLowering/CRepresentation.swift @@ -68,7 +68,7 @@ extension CType { case .optional(let wrapped) where wrapped.isPointer: try self.init(cdeclType: wrapped) - case .genericParameter, .metatype, .optional, .tuple, .opaque, .existential, .composite: + case .genericParameter, .metatype, .optional, .tuple, .opaque, .existential, .composite, .array: throw CDeclToCLoweringError.invalidCDeclType(cdeclType) } } @@ -127,7 +127,7 @@ extension SwiftKnownTypeDeclKind { case .void: .void case .unsafePointer, .unsafeMutablePointer, .unsafeRawBufferPointer, .unsafeMutableRawBufferPointer, .unsafeBufferPointer, .unsafeMutableBufferPointer, .string, .foundationData, .foundationDataProtocol, - .essentialsData, .essentialsDataProtocol, .optional: + .essentialsData, .essentialsDataProtocol, .optional, .array: nil } } diff --git a/Sources/JExtractSwiftLib/FFM/CDeclLowering/FFMSwift2JavaGenerator+FunctionLowering.swift b/Sources/JExtractSwiftLib/FFM/CDeclLowering/FFMSwift2JavaGenerator+FunctionLowering.swift index 7c8d7ab21..832aff262 100644 --- a/Sources/JExtractSwiftLib/FFM/CDeclLowering/FFMSwift2JavaGenerator+FunctionLowering.swift +++ b/Sources/JExtractSwiftLib/FFM/CDeclLowering/FFMSwift2JavaGenerator+FunctionLowering.swift @@ -347,6 +347,9 @@ struct CdeclLowering { case .composite: throw LoweringError.unhandledType(type) + + case .array: + throw LoweringError.unhandledType(type) } } @@ -415,7 +418,7 @@ struct CdeclLowering { } throw LoweringError.unhandledType(.optional(wrappedType)) - case .function, .metatype, .optional, .composite: + case .function, .metatype, .optional, .composite, .array: throw LoweringError.unhandledType(.optional(wrappedType)) } } @@ -516,7 +519,7 @@ struct CdeclLowering { // Custom types are not supported yet. throw LoweringError.unhandledType(type) - case .genericParameter, .function, .metatype, .optional, .tuple, .existential, .opaque, .composite: + case .genericParameter, .function, .metatype, .optional, .tuple, .existential, .opaque, .composite, .array: // TODO: Implement throw LoweringError.unhandledType(type) } @@ -670,7 +673,7 @@ struct CdeclLowering { conversion: .tupleExplode(conversions, name: outParameterName) ) - case .genericParameter, .function, .optional, .existential, .opaque, .composite: + case .genericParameter, .function, .optional, .existential, .opaque, .composite, .array: throw LoweringError.unhandledType(type) } } diff --git a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift index 5d38a0f95..72da323c1 100644 --- a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift +++ b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift @@ -475,6 +475,9 @@ extension FFMSwift2JavaGenerator { case .composite: throw JavaTranslationError.unhandledType(swiftType) + + case .array(let elementType): + throw JavaTranslationError.unhandledType(swiftType) } } @@ -694,7 +697,7 @@ extension FFMSwift2JavaGenerator { // TODO: Implement. throw JavaTranslationError.unhandledType(swiftType) - case .genericParameter, .optional, .function, .existential, .opaque, .composite: + case .genericParameter, .optional, .function, .existential, .opaque, .composite, .array: throw JavaTranslationError.unhandledType(swiftType) } diff --git a/Sources/JExtractSwiftLib/JNI/JNIJavaTypeTranslator.swift b/Sources/JExtractSwiftLib/JNI/JNIJavaTypeTranslator.swift index 0d1fc675a..9bd9a7d83 100644 --- a/Sources/JExtractSwiftLib/JNI/JNIJavaTypeTranslator.swift +++ b/Sources/JExtractSwiftLib/JNI/JNIJavaTypeTranslator.swift @@ -45,13 +45,14 @@ enum JNIJavaTypeTranslator { case .void: return .void case .string: return .javaLangString + case .int, .uint, // FIXME: why not supported int/uint? .unsafeRawPointer, .unsafeMutableRawPointer, .unsafePointer, .unsafeMutablePointer, .unsafeRawBufferPointer, .unsafeMutableRawBufferPointer, .unsafeBufferPointer, .unsafeMutableBufferPointer, - .optional, .foundationData, .foundationDataProtocol, .essentialsData, .essentialsDataProtocol: + .optional, .foundationData, .foundationDataProtocol, .essentialsData, .essentialsDataProtocol, .array: return nil } } -} \ No newline at end of file +} diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift index 1e59c196d..07d23dc0f 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift @@ -25,7 +25,7 @@ extension JNISwift2JavaGenerator { "java.util.concurrent.atomic.AtomicBoolean", // NonNull, Unsigned and friends - "org.swift.swiftkit.core.annotations.*", + "org.swift.swiftkit.core.annotations.*" ] } diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift index d9a8f8d9e..9c3cdbf1a 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift @@ -384,6 +384,15 @@ extension JNISwift2JavaGenerator { parameterName: parameterName ) + case .array: + guard let elementType = nominalType.genericArguments?.first else { + throw JavaTranslationError.unsupportedSwiftType(swiftType) + } + return try translateArrayParameter( + elementType: elementType, + parameterName: parameterName + ) + default: guard let javaType = JNIJavaTypeTranslator.translate(knownType: knownType, config: self.config) else { throw JavaTranslationError.unsupportedSwiftType(swiftType) @@ -461,6 +470,12 @@ extension JNISwift2JavaGenerator { throw JavaTranslationError.unsupportedSwiftType(swiftType) + case .array(let elementType): + return try translateArrayParameter( + elementType: elementType, + parameterName: parameterName + ) + case .metatype, .tuple, .composite: throw JavaTranslationError.unsupportedSwiftType(swiftType) } @@ -651,6 +666,14 @@ extension JNISwift2JavaGenerator { } return try translateOptionalResult(wrappedType: genericArgs[0], resultName: resultName) + case .array: + guard let elementType = nominalType.genericArguments?.first else { + throw JavaTranslationError.unsupportedSwiftType(swiftType) + } + return try translateArrayResult( + elementType: elementType + ) + default: guard let javaType = JNIJavaTypeTranslator.translate(knownType: knownType, config: self.config) else { throw JavaTranslationError.unsupportedSwiftType(swiftType) @@ -684,6 +707,11 @@ extension JNISwift2JavaGenerator { case .optional(let wrapped): return try translateOptionalResult(wrappedType: wrapped, resultName: resultName) + case .array(let elementType): + return try translateArrayResult( + elementType: elementType + ) + case .metatype, .tuple, .function, .existential, .opaque, .genericParameter, .composite: throw JavaTranslationError.unsupportedSwiftType(swiftType) } @@ -771,6 +799,107 @@ extension JNISwift2JavaGenerator { throw JavaTranslationError.unsupportedSwiftType(swiftType) } } + + func translateArrayParameter( + elementType: SwiftType, + parameterName: String + ) throws -> TranslatedParameter { + let parameterAnnotations: [JavaAnnotation] = getTypeAnnotations(swiftType: elementType, config: config) + + switch elementType { + case .nominal(let nominalType): + let nominalTypeName = nominalType.nominalTypeDecl.qualifiedName + + if let knownType = nominalType.nominalTypeDecl.knownTypeKind { + guard let javaType = JNIJavaTypeTranslator.translate(knownType: knownType, config: self.config) else { + throw JavaTranslationError.unsupportedSwiftType(elementType) + } + + return TranslatedParameter( + parameter: JavaParameter(name: parameterName, type: .array(javaType), annotations: parameterAnnotations), + conversion: .requireNonNull(.placeholder, message: "\(parameterName) must not be null") + ) + } + + guard !nominalType.isJavaKitWrapper else { + throw JavaTranslationError.unsupportedSwiftType(elementType) + } + + // Assume JExtract imported class + return TranslatedParameter( + parameter: JavaParameter( + name: parameterName, + type: .array(.class(package: nil, name: nominalTypeName)), + annotations: parameterAnnotations + ), + conversion: .method( + .method( + .arraysStream(.requireNonNull(.placeholder, message: "\(parameterName) must not be null")), + function: "mapToLong", + arguments: [.constant("\(nominalTypeName)::$memoryAddress")] + ), + function: "toArray", + arguments: [] + ) + ) + + default: + throw JavaTranslationError.unsupportedSwiftType(elementType) + } + } + + func translateArrayResult( + elementType: SwiftType + ) throws -> TranslatedResult { + let annotations: [JavaAnnotation] = getTypeAnnotations(swiftType: elementType, config: config) + + switch elementType { + case .nominal(let nominalType): + let nominalTypeName = nominalType.nominalTypeDecl.qualifiedName + + if let knownType = nominalType.nominalTypeDecl.knownTypeKind { + guard let javaType = JNIJavaTypeTranslator.translate(knownType: knownType, config: self.config) else { + throw JavaTranslationError.unsupportedSwiftType(elementType) + } + + return TranslatedResult( + javaType: .array(javaType), + annotations: annotations, + outParameters: [], + conversion: .placeholder + ) + } + + guard !nominalType.isJavaKitWrapper else { + throw JavaTranslationError.unsupportedSwiftType(elementType) + } + + let objectType = JavaType.class(package: nil, name: nominalTypeName) + // We assume this is a JExtract class. + return TranslatedResult( + javaType: .array(objectType), + annotations: annotations, + outParameters: [], + conversion: .method( + .method( + .arraysStream(.placeholder), + function: "mapToObj", + arguments: [ + .lambda( + args: ["pointer"], + body: .wrapMemoryAddressUnsafe(.constant("pointer"), objectType) + ) + ] + ), + function: "toArray", + arguments: [.constant("\(objectType)[]::new")] + ) + ) + + default: + throw JavaTranslationError.unsupportedSwiftType(elementType) + } + } } struct TranslatedEnumCase { @@ -976,6 +1105,13 @@ extension JNISwift2JavaGenerator { /// Prints the conversion step, ignoring the output. indirect case print(JavaNativeConversionStep) + indirect case requireNonNull(JavaNativeConversionStep, message: String) + + /// `Arrays.stream(args)` + static func arraysStream(_ argument: JavaNativeConversionStep) -> JavaNativeConversionStep { + .method(.constant("Arrays"), function: "stream", arguments: [argument]) + } + /// Returns the conversion string applied to the placeholder. func render(_ printer: inout CodePrinter, _ placeholder: String) -> String { // NOTE: 'printer' is used if the conversion wants to cause side-effects. @@ -1105,6 +1241,10 @@ extension JNISwift2JavaGenerator { let inner = inner.render(&printer, placeholder) printer.print("\(inner);") return "" + + case .requireNonNull(let inner, let message): + let inner = inner.render(&printer, placeholder) + return #"Objects.requireNonNull(\#(inner), "\#(message)")"# } } @@ -1161,6 +1301,9 @@ extension JNISwift2JavaGenerator { case .print(let inner): return inner.requiresSwiftArena + + case .requireNonNull(let inner, _): + return inner.requiresSwiftArena } } } diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift index 13e8c676e..35935038d 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift @@ -91,6 +91,12 @@ extension JNISwift2JavaGenerator { parameterName: parameterName ) + case .array: + guard let elementType = nominalType.genericArguments?.first else { + throw JavaTranslationError.unsupportedSwiftType(type) + } + return try translateArrayParameter(elementType: elementType, parameterName: parameterName) + default: guard let javaType = JNIJavaTypeTranslator.translate(knownType: knownType, config: self.config), javaType.implementsJavaValue else { @@ -188,6 +194,12 @@ extension JNISwift2JavaGenerator { throw JavaTranslationError.unsupportedSwiftType(type) + case .array(let elementType): + return try translateArrayParameter( + elementType: elementType, + parameterName: parameterName + ) + case .metatype, .tuple, .composite: throw JavaTranslationError.unsupportedSwiftType(type) } @@ -356,7 +368,7 @@ extension JNISwift2JavaGenerator { return NativeResult( javaType: .long, conversion: .optionalRaisingIndirectReturn( - .getJNIValue(.allocateSwiftValue(name: "_result", swiftType: swiftType)), + .getJNIValue(.allocateSwiftValue(.placeholder, name: "_result", swiftType: swiftType)), returnType: .long, discriminatorParameterName: discriminatorName, placeholderValue: .constant("0") @@ -400,7 +412,7 @@ extension JNISwift2JavaGenerator { outParameters: [] ) - case .function, .metatype, .optional, .tuple, .existential, .opaque, .genericParameter, .composite: + case .function, .metatype, .optional, .tuple, .existential, .opaque, .genericParameter, .composite, .array: throw JavaTranslationError.unsupportedSwiftType(type) } } @@ -429,7 +441,7 @@ extension JNISwift2JavaGenerator { // Custom types are not supported yet. throw JavaTranslationError.unsupportedSwiftType(type) - case .function, .metatype, .optional, .tuple, .existential, .opaque, .genericParameter, .composite: + case .function, .metatype, .optional, .tuple, .existential, .opaque, .genericParameter, .composite, .array: throw JavaTranslationError.unsupportedSwiftType(type) } } @@ -448,6 +460,12 @@ extension JNISwift2JavaGenerator { } return try translateOptionalResult(wrappedType: genericArgs[0], resultName: resultName) + case .array: + guard let elementType = nominalType.genericArguments?.first else { + throw JavaTranslationError.unsupportedSwiftType(swiftResult.type) + } + return try translateArrayResult(elementType: elementType, resultName: resultName) + default: guard let javaType = JNIJavaTypeTranslator.translate(knownType: knownType, config: self.config), javaType.implementsJavaValue else { throw JavaTranslationError.unsupportedSwiftType(swiftResult.type) @@ -467,7 +485,7 @@ extension JNISwift2JavaGenerator { return NativeResult( javaType: .long, - conversion: .getJNIValue(.allocateSwiftValue(name: resultName, swiftType: swiftResult.type)), + conversion: .getJNIValue(.allocateSwiftValue(.placeholder, name: resultName, swiftType: swiftResult.type)), outParameters: [] ) @@ -481,10 +499,108 @@ extension JNISwift2JavaGenerator { case .optional(let wrapped): return try translateOptionalResult(wrappedType: wrapped, resultName: resultName) + case .array(let elementType): + return try translateArrayResult(elementType: elementType, resultName: resultName) + case .metatype, .tuple, .function, .existential, .opaque, .genericParameter, .composite: throw JavaTranslationError.unsupportedSwiftType(swiftResult.type) } } + + func translateArrayResult( + elementType: SwiftType, + resultName: String + ) throws -> NativeResult { + switch elementType { + case .nominal(let nominalType): + if let knownType = nominalType.nominalTypeDecl.knownTypeKind { + guard let javaType = JNIJavaTypeTranslator.translate(knownType: knownType, config: self.config), + javaType.implementsJavaValue else { + throw JavaTranslationError.unsupportedSwiftType(.array(elementType)) + } + + return NativeResult( + javaType: .array(javaType), + conversion: .getJNIValue(.placeholder), + outParameters: [] + ) + } + + guard !nominalType.isJavaKitWrapper else { + throw JavaTranslationError.unsupportedSwiftType(.array(elementType)) + } + + // Assume JExtract imported class + return NativeResult( + javaType: .array(.long), + conversion: + .getJNIValue( + .method( + .placeholder, + function: "map", + arguments: [ + (nil, .closure( + args: ["object$"], + body: .allocateSwiftValue(.constant("object$"), name: "object$", swiftType: elementType) + )) + ] + ) + ), + outParameters: [] + ) + + default: + throw JavaTranslationError.unsupportedSwiftType(.array(elementType)) + } + } + + func translateArrayParameter( + elementType: SwiftType, + parameterName: String + ) throws -> NativeParameter { + switch elementType { + case .nominal(let nominalType): + if let knownType = nominalType.nominalTypeDecl.knownTypeKind { + guard let javaType = JNIJavaTypeTranslator.translate(knownType: knownType, config: self.config), + javaType.implementsJavaValue else { + throw JavaTranslationError.unsupportedSwiftType(elementType) + } + + return NativeParameter( + parameters: [ + JavaParameter(name: parameterName, type: .array(javaType)), + ], + conversion: .initFromJNI(.placeholder, swiftType: .array(elementType)) + ) + } + + guard !nominalType.isJavaKitWrapper else { + throw JavaTranslationError.unsupportedSwiftType(.array(elementType)) + } + + // Assume JExtract wrapped class + return NativeParameter( + parameters: [JavaParameter(name: parameterName, type: .array(.long))], + conversion: .method( + .initFromJNI(.placeholder, swiftType: .array(self.knownTypes.int64)), + function: "map", + arguments: [ + (nil, .closure( + args: ["pointer$"], + body: .pointee(.extractSwiftValue( + .constant("pointer$"), + swiftType: elementType, + allowNil: false, + convertLongFromJNI: false + )))) + ] + ) + ) + + default: + throw JavaTranslationError.unsupportedSwiftType(elementType) + } + } } struct NativeFunctionSignature { @@ -539,11 +655,12 @@ extension JNISwift2JavaGenerator { indirect case extractSwiftValue( NativeSwiftConversionStep, swiftType: SwiftType, - allowNil: Bool = false + allowNil: Bool = false, + convertLongFromJNI: Bool = true ) /// Allocate memory for a Swift value and outputs the pointer - case allocateSwiftValue(name: String, swiftType: SwiftType) + indirect case allocateSwiftValue(NativeSwiftConversionStep, name: String, swiftType: SwiftType) /// The thing to which the pointer typed, which is the `pointee` property /// of the `Unsafe(Mutable)Pointer` types in Swift. @@ -575,6 +692,9 @@ extension JNISwift2JavaGenerator { isThrowing: Bool ) + /// `{ (args) -> return body }` + indirect case closure(args: [String] = [], body: NativeSwiftConversionStep) + /// Returns the conversion string applied to the placeholder. func render(_ printer: inout CodePrinter, _ placeholder: String) -> String { // NOTE: 'printer' is used if the conversion wants to cause side-effects. @@ -630,18 +750,18 @@ extension JNISwift2JavaGenerator { ) return existentialName - case .extractSwiftValue(let inner, let swiftType, let allowNil): + case .extractSwiftValue(let inner, let swiftType, let allowNil, let convertLongFromJNI): let inner = inner.render(&printer, placeholder) let pointerName = "\(inner)$" if !allowNil { printer.print(#"assert(\#(inner) != 0, "\#(inner) memory address was null")"#) } - printer.print( - """ - let \(inner)Bits$ = Int(Int64(fromJNI: \(inner), in: environment)) - let \(pointerName) = UnsafeMutablePointer<\(swiftType)>(bitPattern: \(inner)Bits$) - """ - ) + if convertLongFromJNI { + printer.print("let \(inner)Bits$ = Int(Int64(fromJNI: \(inner), in: environment))") + } else { + printer.print("let \(inner)Bits$ = Int(\(inner))") + } + printer.print("let \(pointerName) = UnsafeMutablePointer<\(swiftType)>(bitPattern: \(inner)Bits$)") if !allowNil { printer.print( """ @@ -653,13 +773,14 @@ extension JNISwift2JavaGenerator { } return pointerName - case .allocateSwiftValue(let name, let swiftType): + case .allocateSwiftValue(let inner, let name, let swiftType): + let inner = inner.render(&printer, placeholder) let pointerName = "\(name)$" let bitsName = "\(name)Bits$" printer.print( """ let \(pointerName) = UnsafeMutablePointer<\(swiftType)>.allocate(capacity: 1) - \(pointerName).initialize(to: \(placeholder)) + \(pointerName).initialize(to: \(inner)) let \(bitsName) = Int64(Int(bitPattern: \(pointerName))) """ ) @@ -894,6 +1015,16 @@ extension JNISwift2JavaGenerator { } return "" + + case .closure(let args, let body): + var printer = CodePrinter() + printer.printBraceBlock("", parameters: args) { printer in + let body = body.render(&printer, placeholder) + if !body.isEmpty { + printer.print("return \(body)") + } + } + return printer.finalize() } } } diff --git a/Sources/JExtractSwiftLib/Swift2JavaTranslator.swift b/Sources/JExtractSwiftLib/Swift2JavaTranslator.swift index 7de792c0f..60b99a632 100644 --- a/Sources/JExtractSwiftLib/Swift2JavaTranslator.swift +++ b/Sources/JExtractSwiftLib/Swift2JavaTranslator.swift @@ -142,6 +142,8 @@ extension Swift2JavaTranslator { return types.contains(where: check) case .genericParameter: return false + case .array(let ty): + return check(ty) } } diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownModules.swift b/Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownModules.swift index 7acb11997..1be8071c4 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownModules.swift +++ b/Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownModules.swift @@ -94,6 +94,8 @@ private let swiftSourceFile: SourceFileSyntax = """ public struct Optional {} + public struct Array {} + // FIXME: Support 'typealias Void = ()' public struct Void {} @@ -117,4 +119,4 @@ private var foundationSourceFile: SourceFileSyntax { // On platforms other than Darwin, imports such as FoundationEssentials, FoundationNetworking, etc. are used, // so this file should be created by combining the files of the aforementioned modules. foundationEssentialsSourceFile -} \ No newline at end of file +} diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownTypeDecls.swift b/Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownTypeDecls.swift index 809f2aa16..4a0cb9e8a 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownTypeDecls.swift +++ b/Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownTypeDecls.swift @@ -40,6 +40,7 @@ enum SwiftKnownTypeDeclKind: String, Hashable { case optional = "Swift.Optional" case void = "Swift.Void" case string = "Swift.String" + case array = "Swift.Array" // Foundation case foundationDataProtocol = "Foundation.DataProtocol" diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftType.swift b/Sources/JExtractSwiftLib/SwiftTypes/SwiftType.swift index 3840b3e13..b2f8d6eac 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftType.swift +++ b/Sources/JExtractSwiftLib/SwiftTypes/SwiftType.swift @@ -40,6 +40,9 @@ enum SwiftType: Equatable { /// `type1` & `type2` indirect case composite([SwiftType]) + /// `[type]` + indirect case array(SwiftType) + static var void: Self { return .tuple([]) } @@ -48,7 +51,7 @@ enum SwiftType: Equatable { switch self { case .nominal(let nominal): nominal case .tuple(let elements): elements.count == 1 ? elements[0].asNominalType : nil - case .genericParameter, .function, .metatype, .optional, .existential, .opaque, .composite: nil + case .genericParameter, .function, .metatype, .optional, .existential, .opaque, .composite, .array: nil } } @@ -91,7 +94,7 @@ enum SwiftType: Equatable { return nominal.nominalTypeDecl.isReferenceType case .metatype, .function: return true - case .genericParameter, .optional, .tuple, .existential, .opaque, .composite: + case .genericParameter, .optional, .tuple, .existential, .opaque, .composite, .array: return false } } @@ -127,7 +130,7 @@ extension SwiftType: CustomStringConvertible { private var postfixRequiresParentheses: Bool { switch self { case .function, .existential, .opaque, .composite: true - case .genericParameter, .metatype, .nominal, .optional, .tuple: false + case .genericParameter, .metatype, .nominal, .optional, .tuple, .array: false } } @@ -152,6 +155,8 @@ extension SwiftType: CustomStringConvertible { return "some \(constraintType)" case .composite(let types): return types.map(\.description).joined(separator: " & ") + case .array(let type): + return "[\(type)]" } } } @@ -213,7 +218,7 @@ extension SwiftNominalType { extension SwiftType { init(_ type: TypeSyntax, lookupContext: SwiftTypeLookupContext) throws { switch type.as(TypeSyntaxEnum.self) { - case .arrayType, .classRestrictionType, + case .classRestrictionType, .dictionaryType, .missingType, .namedOpaqueReturnType, .packElementType, .packExpansionType, .suppressedType, .inlineArrayType: throw TypeTranslationError.unimplementedType(type) @@ -323,6 +328,10 @@ extension SwiftType { } self = .composite(types) + + case .arrayType(let arrayType): + let elementType = try SwiftType(arrayType.element, lookupContext: lookupContext) + self = .array(elementType) } } diff --git a/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md b/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md index 3178f60d2..7e8c66d3d 100644 --- a/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md +++ b/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md @@ -58,7 +58,7 @@ SwiftJava's `swift-java jextract` tool automates generating Java bindings from S | Stored properties: `var`, `let` (with `willSet`, `didSet`) | ✅ | ✅ | | Computed properties: `var` (incl. `throws`) | ✅ / TODO | ✅ | | Async functions `func async` and properties: `var { get async {} }` | ❌ | ✅ | -| Arrays: `[UInt8]`, `[T]` | ❌ | ❌ | +| Arrays: `[UInt8]`, `[MyType]`, `Array` etc | ❌ | ✅ | | Dictionaries: `[String: Int]`, `[K:V]` | ❌ | ❌ | | Generic parameters in functions: `func f(x: T)` | ❌ | ✅ | | Generic return values in functions: `func f() -> T` | ❌ | ❌ | diff --git a/Tests/JExtractSwiftTests/JNI/JNIArrayTest.swift b/Tests/JExtractSwiftTests/JNI/JNIArrayTest.swift new file mode 100644 index 000000000..ebe3f807b --- /dev/null +++ b/Tests/JExtractSwiftTests/JNI/JNIArrayTest.swift @@ -0,0 +1,188 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +import JExtractSwiftLib +import Testing + +@Suite +struct JNIArrayTest { + + @Test("Import: (Array) -> Array (Java)") + func uint8Array_explicitType_java() throws { + try assertOutput( + input: "public func f(array: Array) -> Array {}", + .jni, .java, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + public static byte[] f(@Unsigned byte[] array) { + return SwiftModule.$f(Objects.requireNonNull(array, "array must not be null")); + } + """, + """ + private static native byte[] $f(byte[] array); + """, + ] + ) + } + + @Test("Import: (Array) -> Array (Swift)") + func uint8Array_explicitType_swift() throws { + try assertOutput( + input: "public func f(array: Array) -> Array {}", + .jni, .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + @_cdecl("Java_com_example_swift_SwiftModule__00024f___3B") + func Java_com_example_swift_SwiftModule__00024f___3B(environment: UnsafeMutablePointer!, thisClass: jclass, array: jbyteArray?) -> jbyteArray? { + return SwiftModule.f(array: [UInt8](fromJNI: array, in: environment)).getJNIValue(in: environment) + } + """ + ] + ) + } + + @Test("Import: ([UInt8]) -> [UInt8] (Java)") + func uint8Array_syntaxSugar_java() throws { + try assertOutput( + input: "public func f(array: Array) -> Array {}", + .jni, .java, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + public static byte[] f(@Unsigned byte[] array) { + return SwiftModule.$f(Objects.requireNonNull(array, "array must not be null")); + } + """, + """ + private static native byte[] $f(byte[] array); + """, + ] + ) + } + + @Test("Import: ([UInt8]) -> [UInt8] (Swift)") + func uint8Array_syntaxSugar_swift() throws { + try assertOutput( + input: "public func f(array: [UInt8]) -> [UInt8] {}", + .jni, .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + @_cdecl("Java_com_example_swift_SwiftModule__00024f___3B") + func Java_com_example_swift_SwiftModule__00024f___3B(environment: UnsafeMutablePointer!, thisClass: jclass, array: jbyteArray?) -> jbyteArray? { + return SwiftModule.f(array: [UInt8](fromJNI: array, in: environment)).getJNIValue(in: environment) + } + """ + ] + ) + } + + @Test("Import: ([Int64]) -> [Int64] (Java)") + func int64Array_syntaxSugar_java() throws { + try assertOutput( + input: "public func f(array: [Int64]) -> [Int64] {}", + .jni, .java, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + public static long[] f(long[] array) { + return SwiftModule.$f(Objects.requireNonNull(array, "array must not be null")); + } + """, + """ + private static native long[] $f(long[] array); + """, + ] + ) + } + + @Test("Import: ([Int64]) -> [Int64] (Swift)") + func int64Array_syntaxSugar_swift() throws { + try assertOutput( + input: "public func f(array: [Int64]) -> [Int64] {}", + .jni, .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + @_cdecl("Java_com_example_swift_SwiftModule__00024f___3J") + func Java_com_example_swift_SwiftModule__00024f___3J(environment: UnsafeMutablePointer!, thisClass: jclass, array: jlongArray?) -> jlongArray? { + return SwiftModule.f(array: [Int64](fromJNI: array, in: environment)).getJNIValue(in: environment) + } + """ + ] + ) + } + + @Test("Import: ([MySwiftClass]) -> [MySwiftClass] (Java)") + func swiftClassArray_syntaxSugar_java() throws { + try assertOutput( + input: """ + public class MySwiftClass {} + public func f(array: [MySwiftClass]) -> [MySwiftClass] {} + """, + .jni, .java, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + public static MySwiftClass[] f(MySwiftClass[] array, SwiftArena swiftArena$) { + return Arrays.stream(SwiftModule.$f(Arrays.stream(Objects.requireNonNull(array, "array must not be null")).mapToLong(MySwiftClass::$memoryAddress).toArray())).mapToObj((pointer) -> { + return MySwiftClass.wrapMemoryAddressUnsafe(pointer, swiftArena$); + } + ).toArray(MySwiftClass[]::new); + } + """, + """ + private static native long[] $f(long[] array); + """, + ] + ) + } + + @Test("Import: ([MySwiftClass]) -> [MySwiftClass] (Swift)") + func swiftClassArray_syntaxSugar_swift() throws { + try assertOutput( + input: """ + public class MySwiftClass {} + public func f(array: [MySwiftClass]) -> [MySwiftClass] {} + """, + .jni, .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + @_cdecl("Java_com_example_swift_SwiftModule__00024f___3J") + func Java_com_example_swift_SwiftModule__00024f___3J(environment: UnsafeMutablePointer!, thisClass: jclass, array: jlongArray?) -> jlongArray? { + return SwiftModule.f(array: [Int64](fromJNI: array, in: environment).map( { (pointer$) in + assert(pointer$ != 0, "pointer$ memory address was null") + let pointer$Bits$ = Int(pointer$) + let pointer$$ = UnsafeMutablePointer(bitPattern: pointer$Bits$) + guard let pointer$$ else { + fatalError("pointer$ memory address was null in call to \\(#function)!") + } + return pointer$$.pointee + } + )).map( { (object$) in + let object$$ = UnsafeMutablePointer.allocate(capacity: 1) + object$$.initialize(to: object$) + let object$Bits$ = Int64(Int(bitPattern: object$$)) + return object$Bits$ + } + ).getJNIValue(in: environment) + } + """ + ] + ) + } +}