diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/UUID.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/UUID.swift new file mode 100644 index 000000000..0c1613d5e --- /dev/null +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/UUID.swift @@ -0,0 +1,28 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif +import SwiftJava + +public func echoUUID(_ uuid: UUID) -> UUID { + return uuid +} + +public func makeUUID() -> UUID { + return UUID() +} diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/UUIDTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/UUIDTest.java new file mode 100644 index 000000000..b06592e48 --- /dev/null +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/UUIDTest.java @@ -0,0 +1,38 @@ +//===----------------------------------------------------------------------===// +// +// 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.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; + +public class UUIDTest { + @Test + void echoUUID() { + var uuid = UUID.randomUUID(); + assertEquals(uuid, MySwiftLibrary.echoUUID(uuid)); + } + + @Test + void makeUUID() { + var uuid = MySwiftLibrary.makeUUID(); + assertEquals(4, uuid.version()); + } +} \ No newline at end of file diff --git a/Sources/JExtractSwiftLib/FFM/CDeclLowering/CRepresentation.swift b/Sources/JExtractSwiftLib/FFM/CDeclLowering/CRepresentation.swift index 3c0a00267..2cfc4d49a 100644 --- a/Sources/JExtractSwiftLib/FFM/CDeclLowering/CRepresentation.swift +++ b/Sources/JExtractSwiftLib/FFM/CDeclLowering/CRepresentation.swift @@ -129,7 +129,7 @@ extension SwiftKnownTypeDeclKind { case .void: .void case .unsafePointer, .unsafeMutablePointer, .unsafeRawBufferPointer, .unsafeMutableRawBufferPointer, .unsafeBufferPointer, .unsafeMutableBufferPointer, .string, .foundationData, .foundationDataProtocol, - .essentialsData, .essentialsDataProtocol, .optional, .foundationDate, .essentialsDate: + .essentialsData, .essentialsDataProtocol, .optional, .foundationDate, .essentialsDate, .foundationUUID, .essentialsUUID: nil } } diff --git a/Sources/JExtractSwiftLib/JNI/JNIJavaTypeTranslator.swift b/Sources/JExtractSwiftLib/JNI/JNIJavaTypeTranslator.swift index a3d26a117..bde9afbd1 100644 --- a/Sources/JExtractSwiftLib/JNI/JNIJavaTypeTranslator.swift +++ b/Sources/JExtractSwiftLib/JNI/JNIJavaTypeTranslator.swift @@ -45,7 +45,7 @@ enum JNIJavaTypeTranslator { .unsafePointer, .unsafeMutablePointer, .unsafeRawBufferPointer, .unsafeMutableRawBufferPointer, .unsafeBufferPointer, .unsafeMutableBufferPointer, - .optional, .foundationData, .foundationDataProtocol, .essentialsData, .essentialsDataProtocol, .array, .foundationDate, .essentialsDate: + .optional, .foundationData, .foundationDataProtocol, .essentialsData, .essentialsDataProtocol, .array, .foundationDate, .essentialsDate, .foundationUUID, .essentialsUUID: return nil } } @@ -62,7 +62,7 @@ enum JNIJavaTypeTranslator { .unsafeRawBufferPointer, .unsafeMutableRawBufferPointer, .unsafeBufferPointer, .unsafeMutableBufferPointer, .optional, .foundationData, .foundationDataProtocol, .essentialsData, .essentialsDataProtocol, - .array, .foundationDate, .essentialsDate: + .array, .foundationDate, .essentialsDate, .foundationUUID, .essentialsUUID: nil } } @@ -79,7 +79,7 @@ enum JNIJavaTypeTranslator { .unsafeRawBufferPointer, .unsafeMutableRawBufferPointer, .unsafeBufferPointer, .unsafeMutableBufferPointer, .optional, .foundationData, .foundationDataProtocol, .essentialsData, .essentialsDataProtocol, - .array, .foundationDate, .essentialsDate: + .array, .foundationDate, .essentialsDate, .foundationUUID, .essentialsUUID: nil } } diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift index 756b09747..1c791df05 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift @@ -409,6 +409,12 @@ extension JNISwift2JavaGenerator { // Handled as wrapped struct break + case .foundationUUID, .essentialsUUID: + return TranslatedParameter( + parameter: JavaParameter(name: parameterName, type: .javaUtilUUID), + conversion: .method(.placeholder, function: "toString") + ) + default: guard let javaType = JNIJavaTypeTranslator.translate(knownType: knownType, config: self.config) else { throw JavaTranslationError.unsupportedSwiftType(swiftType) @@ -695,6 +701,17 @@ extension JNISwift2JavaGenerator { // Handled as wrapped struct break + case .foundationUUID, .essentialsUUID: + return TranslatedResult( + javaType: .javaUtilUUID, + outParameters: [], + conversion: .method( + .constant("java.util.UUID"), + function: "fromString", + arguments: [.placeholder] + ) + ) + default: guard let javaType = JNIJavaTypeTranslator.translate(knownType: knownType, config: self.config) else { throw JavaTranslationError.unsupportedSwiftType(swiftType) diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift index 583666921..5be38bad8 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift @@ -102,6 +102,33 @@ extension JNISwift2JavaGenerator { // Handled as wrapped struct break + case .foundationUUID, .essentialsUUID: + let uuidStringVariable = "\(parameterName)_string$" + let initUUIDStep = NativeSwiftConversionStep.unwrapOptional( + .method( + .constant("UUID"), + function: "init", + arguments: [("uuidString", .placeholder)] + ), + name: parameterName, + fatalErrorMessage: "Invalid UUID string passed from Java: \\(\(uuidStringVariable))" + ) + + return NativeParameter( + parameters: [ + JavaParameter(name: parameterName, type: .javaLangString) + ], + conversion: .replacingPlaceholder( + .aggregate( + variable: uuidStringVariable, + [initUUIDStep] + ), + placeholder: .initFromJNI(.placeholder, swiftType: self.knownTypes.string) + ), + indirectConversion: nil, + conversionCheck: nil + ) + default: guard let javaType = JNIJavaTypeTranslator.translate(knownType: knownType, config: self.config), javaType.implementsJavaValue else { @@ -515,6 +542,13 @@ extension JNISwift2JavaGenerator { // Handled as wrapped struct break + case .foundationUUID, .essentialsUUID: + return NativeResult( + javaType: .javaLangString, + conversion: .getJNIValue(.member(.placeholder, member: "uuidString")), + outParameters: [] + ) + default: guard let javaType = JNIJavaTypeTranslator.translate(knownType: knownType, config: self.config), javaType.implementsJavaValue else { throw JavaTranslationError.unsupportedSwiftType(swiftResult.type) @@ -766,6 +800,10 @@ extension JNISwift2JavaGenerator { indirect case labelessAssignmentOfVariable(NativeSwiftConversionStep, swiftType: SwiftType) + indirect case aggregate(variable: String, [NativeSwiftConversionStep]) + + indirect case replacingPlaceholder(NativeSwiftConversionStep, placeholder: 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. @@ -1166,8 +1204,21 @@ extension JNISwift2JavaGenerator { } } return printer.finalize() + case .labelessAssignmentOfVariable(let name, let swiftType): return "\(swiftType)(\(JNISwift2JavaGenerator.indirectVariableName(for: name.render(&printer, placeholder))))" + + case .aggregate(let variable, let steps): + precondition(!steps.isEmpty, "Aggregate must contain steps") + printer.print("let \(variable) = \(placeholder)") + let steps = steps.map { + $0.render(&printer, variable) + } + return steps.last! + + case .replacingPlaceholder(let inner, let newPlaceholder): + let newPlaceholder = newPlaceholder.render(&printer, placeholder) + return inner.render(&printer, newPlaceholder) } } } diff --git a/Sources/JExtractSwiftLib/JavaTypes/JavaType+JDK.swift b/Sources/JExtractSwiftLib/JavaTypes/JavaType+JDK.swift index e1aabd7fd..82b58d496 100644 --- a/Sources/JExtractSwiftLib/JavaTypes/JavaType+JDK.swift +++ b/Sources/JExtractSwiftLib/JavaTypes/JavaType+JDK.swift @@ -55,4 +55,8 @@ extension JavaType { static func future(_ T: JavaType) -> JavaType { .class(package: "java.util.concurrent", name: "Future", typeParameters: [T.boxedType]) } + + static var javaUtilUUID: JavaType { + .class(package: "java.util", name: "UUID") + } } diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownModules.swift b/Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownModules.swift index c3bd51e88..8cc4bb449 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownModules.swift +++ b/Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownModules.swift @@ -121,6 +121,8 @@ private let foundationEssentialsSourceFile: SourceFileSyntax = """ /// Returns a `Date` initialized relative to 00:00:00 UTC on 1 January 1970 by a given number of seconds. public init(timeIntervalSince1970: Double) } + + public struct UUID {} """ private var foundationSourceFile: SourceFileSyntax { diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownTypeDecls.swift b/Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownTypeDecls.swift index 7f5b22c94..afbe5bb59 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownTypeDecls.swift +++ b/Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownTypeDecls.swift @@ -49,6 +49,8 @@ enum SwiftKnownTypeDeclKind: String, Hashable { case essentialsData = "FoundationEssentials.Data" case foundationDate = "Foundation.Date" case essentialsDate = "FoundationEssentials.Date" + case foundationUUID = "Foundation.UUID" + case essentialsUUID = "FoundationEssentials.UUID" var moduleAndName: (module: String, name: String) { let qualified = self.rawValue diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownTypes.swift b/Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownTypes.swift index 56c6db464..2152ddcaf 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownTypes.swift +++ b/Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownTypes.swift @@ -35,12 +35,15 @@ struct SwiftKnownTypes { 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 string: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.string])) } + 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])) } - + var foundationUUID: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.foundationUUID])) } + var essentialsUUID: SwiftType { .nominal(SwiftNominalType(nominalTypeDecl: symbolTable[.essentialsUUID]))} + /// `(UnsafeRawPointer, Long) -> ()` function type. /// /// Commonly used to initialize a buffer using the passed bytes and length. diff --git a/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md b/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md index 2f9b7fdad..d36be9a48 100644 --- a/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md +++ b/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md @@ -70,6 +70,7 @@ SwiftJava's `swift-java jextract` tool automates generating Java bindings from S | Existential return types `f() -> any Collection` | ❌ | ❌ | | Foundation Data and DataProtocol: `f(x: any DataProtocol) -> Data` | ✅ | ❌ | | Foundation Date: `f(date: Date) -> Date` | ❌ | ✅ | +| Foundation UUID: `f(uuid: UUID) -> UUID` | ❌ | ✅ | | Opaque parameters: `func take(worker: some Builder) -> some Builder` | ❌ | ✅ | | Opaque return types: `func get() -> some Builder` | ❌ | ❌ | | Optional parameters: `func f(i: Int?, class: MyClass?)` | ✅ | ✅ | diff --git a/Tests/JExtractSwiftTests/UUIDTests.swift b/Tests/JExtractSwiftTests/UUIDTests.swift new file mode 100644 index 000000000..57b2b79a7 --- /dev/null +++ b/Tests/JExtractSwiftTests/UUIDTests.swift @@ -0,0 +1,115 @@ +//===----------------------------------------------------------------------===// +// +// 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 SwiftJavaConfigurationShared +import Testing + +struct UUIDTests { + @Test( + "Import: accept UUID", + arguments: [ + ( + JExtractGenerationMode.jni, + /* expected Java chunks */ + [ + """ + public static void acceptUUID(java.util.UUID uuid) { + SwiftModule.$acceptUUID(uuid.toString()); + } + """ + ], + /* expected Swift chunks */ + [ + """ + @_cdecl("Java_com_example_swift_SwiftModule__00024acceptUUID__Ljava_lang_String_2") + public func Java_com_example_swift_SwiftModule__00024acceptUUID__Ljava_lang_String_2(environment: UnsafeMutablePointer!, thisClass: jclass, uuid: jstring?) { + let uuid_string$ = String(fromJNI: uuid, in: environment) + guard let uuid_unwrapped$ = UUID.init(uuidString: uuid_string$) else { + fatalError("Invalid UUID string passed from Java: \\(uuid_string$)") + } + SwiftModule.acceptUUID(uuid: uuid_unwrapped$) + } + """ + ], + ) + ] + ) + func func_accept_uuid(mode: JExtractGenerationMode, expectedJavaChunks: [String], expectedSwiftChunks: [String]) throws { + let text = + """ + import Foundation + + public func acceptUUID(uuid: UUID) + """ + + try assertOutput( + input: text, + mode, .java, + detectChunkByInitialLines: 1, + expectedChunks: expectedJavaChunks) + + try assertOutput( + input: text, + mode, .swift, + detectChunkByInitialLines: 1, + expectedChunks: expectedSwiftChunks) + } + + @Test( + "Import: return UUID", + arguments: [ + ( + JExtractGenerationMode.jni, + /* expected Java chunks */ + [ + """ + public static java.util.UUID returnUUID() { + return java.util.UUID.fromString(SwiftModule.$returnUUID()); + } + """ + ], + /* expected Swift chunks */ + [ + """ + @_cdecl("Java_com_example_swift_SwiftModule__00024returnUUID__") + public func Java_com_example_swift_SwiftModule__00024returnUUID__(environment: UnsafeMutablePointer!, thisClass: jclass) -> jstring? { + return SwiftModule.returnUUID().uuidString.getJNIValue(in: environment) + } + """ + ] + ) + ] + ) + func func_return_UUID(mode: JExtractGenerationMode, expectedJavaChunks: [String], expectedSwiftChunks: [String]) throws { + let text = + """ + import Foundation + public func returnUUID() -> UUID + """ + + try assertOutput( + input: text, + mode, .java, + detectChunkByInitialLines: 1, + expectedChunks: expectedJavaChunks + ) + + try assertOutput( + input: text, + mode, .swift, + detectChunkByInitialLines: 1, + expectedChunks: expectedSwiftChunks) + } +}