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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -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<Data> optionalData(Optional<Data> value, SwiftArena arena) {
value.ifPresent(data -> assertArrayEquals(new byte[] {1, 2, 3}, data.toByteArray()));
return value;
}
public Optional<Date> optionalDate(Optional<Date> 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));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -386,6 +404,10 @@ enum UpcallConversionStep {
message: String
)

indirect case toJavaUUID(UpcallConversionStep)

indirect case fromJavaUUID(UpcallConversionStep)

indirect case toJavaOptional(UpcallConversionStep)

indirect case fromJavaOptional(UpcallConversionStep)
Expand Down Expand Up @@ -439,6 +461,14 @@ enum UpcallConversionStep {

return "unwrapped$"

case .toJavaUUID(let inner):
let inner = inner.render(&printer, placeholder)
return "try! JavaClass<JavaUtilUUID>(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()"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
24 changes: 24 additions & 0 deletions Sources/SwiftJavaRuntimeSupport/JavaUtilUUID.swift
Original file line number Diff line number Diff line change
@@ -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<JavaUtilUUID> {
@JavaStaticMethod
public func fromString(_ value: String) -> JavaUtilUUID!
}
3 changes: 2 additions & 1 deletion Sources/SwiftJavaRuntimeSupport/swift-java.config
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
94 changes: 94 additions & 0 deletions Tests/JExtractSwiftTests/JNI/JNIFoundationCallbackTests.swift
Original file line number Diff line number Diff line change
@@ -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<UUID>"]
)
}

@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()",
]
)
}
}
}
Loading