diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/FoundationCallbacks.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/FoundationCallbacks.swift new file mode 100644 index 000000000..89f3866b8 --- /dev/null +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/FoundationCallbacks.swift @@ -0,0 +1,56 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2026 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 + +public protocol FoundationCallback { + func data(_ value: Data) -> Data + func date(_ value: Date) -> Date + func url(_ value: URL) -> URL + func uuid(_ value: UUID) -> UUID +} + +public func invokeDataCallback(_ callback: some FoundationCallback, _ value: Data) -> Data { + callback.data(value) +} + +public func invokeDateCallback(_ callback: some FoundationCallback, _ value: Date) -> Date { + callback.date(value) +} + +public func invokeURLCallback(_ callback: some FoundationCallback, _ value: URL) -> URL { + callback.url(value) +} + +public func invokeUUIDCallback(_ callback: some FoundationCallback, _ value: UUID) -> UUID { + callback.uuid(value) +} + +public protocol OptionalFoundationCallback { + func optionalData(_ value: Data?) -> Data? + func optionalDate(_ value: Date?) -> Date? +} + +public func invokeOptionalDataCallback(_ callback: some OptionalFoundationCallback, present: Bool) -> [UInt8] { + let result = callback.optionalData(present ? Data([1, 2, 3]) : nil) + return result.map(Array.init) ?? [] +} + +public func invokeOptionalDateCallback(_ callback: some OptionalFoundationCallback, present: Bool) -> Double { + callback.optionalDate(present ? Date(timeIntervalSince1970: 1234.5) : nil)?.timeIntervalSince1970 ?? -1 +} diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/FoundationCallbackTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/FoundationCallbackTest.java new file mode 100644 index 000000000..00cf05598 --- /dev/null +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/FoundationCallbackTest.java @@ -0,0 +1,113 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2026 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.concurrent.atomic.AtomicBoolean; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; + +public class FoundationCallbackTest { + private FoundationCallback callback(AtomicBoolean called) { + return new FoundationCallback() { + public Data data(Data value, SwiftArena arena) { + called.set(true); + assertArrayEquals(new byte[] {1, 2, 3}, value.toByteArray()); + return value; + } + public Date date(Date value, SwiftArena arena) { + called.set(true); + assertEquals(1234.5, value.getTimeIntervalSince1970()); + return value; + } + public URL url(URL value, SwiftArena arena) { + called.set(true); + return value; + } + public java.util.UUID uuid(java.util.UUID value) { + called.set(true); + assertEquals(java.util.UUID.fromString("12345678-1234-5678-9abc-def012345678"), value); + return java.util.UUID.fromString("87654321-4321-8765-cba9-876543210fed"); + } + }; + } + + @Test + void dataRoundTrip() { + try (var arena = SwiftArena.ofConfined()) { + var called = new AtomicBoolean(); + Data input = Data.fromByteArray(new byte[] {1, 2, 3}, arena); + Data result = MySwiftLibrary.invokeDataCallback(callback(called), input, arena); + assertTrue(called.get()); + assertArrayEquals(input.toByteArray(), result.toByteArray()); + } + } + + @Test + void dateRoundTrip() { + try (var arena = SwiftArena.ofConfined()) { + var called = new AtomicBoolean(); + Date input = MySwiftLibrary.dateFromSeconds(1234.5, arena); + Date result = MySwiftLibrary.invokeDateCallback(callback(called), input, arena); + assertTrue(called.get()); + assertEquals(1234.5, result.getTimeIntervalSince1970()); + } + } + + @Test + void urlRoundTrip() { + try (var arena = SwiftArena.ofConfined()) { + var called = new AtomicBoolean(); + URL input = URL.init("https://example.com/callback", arena).orElseThrow(); + URL result = MySwiftLibrary.invokeURLCallback(callback(called), input, arena); + assertTrue(called.get()); + assertEquals(input.getAbsoluteString(), result.getAbsoluteString()); + } + } + + @Test + void uuidRoundTripUsesJavaUtilUUID() { + var called = new AtomicBoolean(); + var input = java.util.UUID.fromString("12345678-1234-5678-9abc-def012345678"); + var result = MySwiftLibrary.invokeUUIDCallback(callback(called), input); + assertTrue(called.get()); + assertEquals(java.util.UUID.fromString("87654321-4321-8765-cba9-876543210fed"), result); + } + private OptionalFoundationCallback optionalCallback() { + return new OptionalFoundationCallback() { + public Optional optionalData(Optional value, SwiftArena arena) { + value.ifPresent(data -> assertArrayEquals(new byte[] {1, 2, 3}, data.toByteArray())); + return value; + } + public Optional optionalDate(Optional value, SwiftArena arena) { + value.ifPresent(date -> assertEquals(1234.5, date.getTimeIntervalSince1970())); + return value; + } + }; + } + + @Test + void optionalValuesRoundTrip() { + var callback = optionalCallback(); + assertArrayEquals(new byte[] {1, 2, 3}, MySwiftLibrary.invokeOptionalDataCallback(callback, true)); + assertArrayEquals(new byte[] {}, MySwiftLibrary.invokeOptionalDataCallback(callback, false)); + assertEquals(1234.5, MySwiftLibrary.invokeOptionalDateCallback(callback, true)); + assertEquals(-1, MySwiftLibrary.invokeOptionalDateCallback(callback, false)); + } + +} diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+InterfaceWrapperGeneration.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+InterfaceWrapperGeneration.swift index 236977e5e..e7a699cd7 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+InterfaceWrapperGeneration.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+InterfaceWrapperGeneration.swift @@ -221,6 +221,13 @@ extension JNISwift2JavaGenerator { elementType: element ) + case .foundationData, .essentialsData, .foundationDate, .essentialsDate, + .foundationURL, .essentialsURL: + break + + case .foundationUUID, .essentialsUUID: + return .toJavaUUID(.placeholder) + default: throw JavaTranslationError.unsupportedSwiftType(type) } @@ -299,6 +306,17 @@ extension JNISwift2JavaGenerator { case .array(let element): return try self.translateArrayResult(elementType: element) + case .foundationData, .essentialsData, .foundationDate, .essentialsDate, + .foundationURL, .essentialsURL: + break + + case .foundationUUID, .essentialsUUID: + let inner: UpcallConversionStep = + !allowNilForObjects + ? .unwrapOptional(.placeholder, message: "Upcall to \(methodName) unexpectedly returned nil") + : .placeholder + return .fromJavaUUID(inner) + default: throw JavaTranslationError.unsupportedSwiftType(type) } @@ -386,6 +404,10 @@ enum UpcallConversionStep { message: String ) + indirect case toJavaUUID(UpcallConversionStep) + + indirect case fromJavaUUID(UpcallConversionStep) + indirect case toJavaOptional(UpcallConversionStep) indirect case fromJavaOptional(UpcallConversionStep) @@ -439,6 +461,14 @@ enum UpcallConversionStep { return "unwrapped$" + case .toJavaUUID(let inner): + let inner = inner.render(&printer, placeholder) + return "try! JavaClass(environment: JavaVirtualMachine.shared().environment()).fromString(\(inner).uuidString)" + + case .fromJavaUUID(let inner): + let inner = inner.render(&printer, placeholder) + return "UUID(uuidString: \(inner).toString())!" + case .toJavaOptional(let inner): let inner = inner.render(&printer, placeholder) return "\(inner).toJavaOptional()" diff --git a/Sources/SwiftJavaDocumentation/Documentation.docc/FeaturesJextract.md b/Sources/SwiftJavaDocumentation/Documentation.docc/FeaturesJextract.md index 24d50e508..92164e754 100644 --- a/Sources/SwiftJavaDocumentation/Documentation.docc/FeaturesJextract.md +++ b/Sources/SwiftJavaDocumentation/Documentation.docc/FeaturesJextract.md @@ -618,6 +618,13 @@ expose both their own and their inherited requirements. A handful of Foundation value types are recognized by name and bridged to a matching Java representation, rather than being treated as opaque Swift values. +With `enableJavaCallbacks` enabled in JNI mode, protocol methods can accept and +return `Data`, `Date`, `URL`, and `UUID` from either `Foundation` or +`FoundationEssentials`. `Data`, `Date`, and `URL` use the generated Swift wrapper +classes; `UUID` uses `java.util.UUID`. Optional `Data` and `Date` values are also +supported. Optional `URL`/`UUID` parameters and arrays of Foundation values remain +unsupported by JNI method extraction. + #### Data Swift methods accepting or returning `Foundation.Data` are extracted using the diff --git a/Sources/SwiftJavaRuntimeSupport/JavaUtilUUID.swift b/Sources/SwiftJavaRuntimeSupport/JavaUtilUUID.swift new file mode 100644 index 000000000..e3acb843f --- /dev/null +++ b/Sources/SwiftJavaRuntimeSupport/JavaUtilUUID.swift @@ -0,0 +1,24 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2026 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 + +/// The Java UUID value used by generated JNI callback bindings. +@JavaClass("java.util.UUID") +public class JavaUtilUUID: JavaObject {} + +extension JavaClass { + @JavaStaticMethod + public func fromString(_ value: String) -> JavaUtilUUID! +} diff --git a/Sources/SwiftJavaRuntimeSupport/swift-java.config b/Sources/SwiftJavaRuntimeSupport/swift-java.config index 4fa790e26..b2aafb80f 100644 --- a/Sources/SwiftJavaRuntimeSupport/swift-java.config +++ b/Sources/SwiftJavaRuntimeSupport/swift-java.config @@ -3,6 +3,7 @@ "classes" : { "org.swift.swiftkit.core.SwiftInstance" : "JavaSwiftInstance", "org.swift.swiftkit.core.JNISwiftInstance" : "JavaJNISwiftInstance", - "org.swift.swiftkit.core.SwiftArena" : "JavaSwiftArena" + "org.swift.swiftkit.core.SwiftArena" : "JavaSwiftArena", + "java.util.UUID" : "JavaUtilUUID" } } diff --git a/Tests/JExtractSwiftTests/JNI/JNIFoundationCallbackTests.swift b/Tests/JExtractSwiftTests/JNI/JNIFoundationCallbackTests.swift new file mode 100644 index 000000000..8f5991a0c --- /dev/null +++ b/Tests/JExtractSwiftTests/JNI/JNIFoundationCallbackTests.swift @@ -0,0 +1,94 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2026 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 + +@Suite +struct JNIFoundationCallbackTests { + var config: Configuration { + var config = Configuration() + config.enableJavaCallbacks = true + return config + } + + @Test(arguments: ["Foundation", "FoundationEssentials"]) + func wrappedFoundationValues(module: String) throws { + for type in ["Data", "Date", "URL"] { + let source = """ + import \(module) + public protocol Callback { func transform(_ value: \(type)) -> \(type) } + public func invoke(_ callback: some Callback, _ value: \(type)) -> \(type) + """ + try assertOutput( + input: source, + config: config, + .jni, + .swift, + expectedChunks: [ + "protocol SwiftJavaCallbackWrapper: Callback", + "let valuePointer = UnsafeMutablePointer<\(type)>.allocate(capacity: 1)", + "unwrapped$.as(JavaJNISwiftInstance.self)!.memoryAddress()", + ] + ) + } + } + + @Test(arguments: ["Foundation", "FoundationEssentials"]) + func uuidValues(module: String) throws { + let source = """ + import \(module) + public protocol Callback { func transform(_ value: UUID) -> UUID } + public func invoke(_ callback: some Callback, _ value: UUID) -> UUID + """ + try assertOutput( + input: source, + config: config, + .jni, + .swift, + expectedChunks: [ + "protocol SwiftJavaCallbackWrapper: Callback", + "value.uuidString", + "UUID(uuidString:", + ], + notExpectedChunks: ["let valuePointer = UnsafeMutablePointer"] + ) + } + + @Test(arguments: ["Foundation", "FoundationEssentials"]) + func optionalValues(module: String) throws { + for type in ["Data", "Date"] { + let source = """ + import \(module) + public protocol Callback { + func optional(_ value: \(type)?) -> \(type)? + } + public func invoke(_ callback: some Callback) + """ + try assertOutput( + input: source, + config: config, + .jni, + .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + "protocol SwiftJavaCallbackWrapper: Callback", + "Optional(javaOptional:", + "toJavaOptional()", + ] + ) + } + } +}