Skip to content
Open
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
1 change: 1 addition & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,7 @@ let package = Package(
name: "SwiftJavaTests",
dependencies: [
"SwiftJava",
"SwiftJavaRuntimeSupport",
"JavaNet",
],
swiftSettings: [
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
//===----------------------------------------------------------------------===//
//
// 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 Foundation

/// Foundation values cross as standard Java types (`foundationTypesAsJavaTypes`).
public final class Bookmark {
public var url: URL
public var icon: URL?
public var created: Date
public var thumbnail: Data
public var locale: Locale
public var timeZone: TimeZone
public var zoom: CGFloat
public var onOpen: ((URL) -> Void)?

public init(url: URL, icon: URL?, created: Date, thumbnail: Data, locale: Locale, timeZone: TimeZone, zoom: CGFloat) {
self.url = url
self.icon = icon
self.created = created
self.thumbnail = thumbnail
self.locale = locale
self.timeZone = timeZone
self.zoom = zoom
}

public func open() {
onOpen?(url)
}

public func observeVisits(_ onVisit: @escaping (URL, Date) -> Void) {
onVisit(url, created)
}

public func related() -> [URL] {
[url, url.appendingPathComponent("more")]
}

public func refreshed() async -> Date {
created.addingTimeInterval(60)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@
"payloadlessEnumsAsJavaEnums": true,
"optionalsAsNullable": true,
"nullableAnnotation": "org.jetbrains.annotations.Nullable",
"nonNullAnnotation": "org.jetbrains.annotations.NotNull"
"nonNullAnnotation": "org.jetbrains.annotations.NotNull",
"foundationTypesAsJavaTypes": true
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
//===----------------------------------------------------------------------===//
//
// 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.kotlin

import java.net.URI
import java.util.Date
import java.util.Locale
import java.util.TimeZone
import kotlin.test.Test
import kotlin.test.assertContentEquals
import kotlin.test.assertEquals
import kotlin.test.assertNull

// URL, Date, Data, Locale and TimeZone are java.net.URI, java.util.Date, ByteArray, java.util.Locale and
// java.util.TimeZone in Kotlin, and CGFloat is a Double.
class FoundationValuesTest {
private fun bookmark() = Bookmark.init(
URI.create("https://swift.org/docs"),
null,
Date(1_700_000_000_123),
byteArrayOf(1, 2, -1),
Locale.forLanguageTag("pt-BR"),
TimeZone.getTimeZone("Europe/Rome"),
1.5,
)

@Test
fun valuesRoundTrip() {
val bookmark = bookmark()
assertEquals(URI.create("https://swift.org/docs"), bookmark.url)
assertNull(bookmark.icon)
bookmark.icon = URI.create("https://swift.org/favicon.ico")
assertEquals("https://swift.org/favicon.ico", bookmark.icon.toString())
assertEquals(1_700_000_000_123, bookmark.created.time)
assertContentEquals(byteArrayOf(1, 2, -1), bookmark.thumbnail)
assertEquals("pt-BR", bookmark.locale.toLanguageTag())
assertEquals("Europe/Rome", bookmark.timeZone.id)
assertEquals(1.5, bookmark.zoom)
assertEquals(listOf("https://swift.org/docs", "https://swift.org/docs/more"), bookmark.related().map { it.toString() })
}

@Test
fun closures() {
val bookmark = bookmark()
val opened = mutableListOf<URI>()
bookmark.onOpen = { opened += it }
bookmark.open()
assertEquals(listOf(URI.create("https://swift.org/docs")), opened)

var visit: Pair<URI, Date>? = null
bookmark.observeVisits { url, date -> visit = url to date }
assertEquals(URI.create("https://swift.org/docs") to Date(1_700_000_000_123), visit)
}

@Test
fun asyncResult() {
assertEquals(1_700_000_060_123, bookmark().refreshed().get().time)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,16 @@ extension JavaType {
case .float: "Optional<Float>"
case .double: "OptionalDouble"
case .javaLangString: "Optional<String>"
case .javaNetURI, .javaUtilDate, .javaUtilLocale, .javaUtilTimeZone, .array(.byte): "Optional<\(self)>"
default: nil
}
}

var optionalWrapperType: String? {
switch self {
case .boolean, .byte, .char, .short, .float, .javaLangString: "Optional"
case .boolean, .byte, .char, .short, .float, .javaLangString,
.javaNetURI, .javaUtilDate, .javaUtilLocale, .javaUtilTimeZone, .array(.byte):
"Optional"
case .int: "OptionalInt"
case .long: "OptionalLong"
case .double: "OptionalDouble"
Expand Down Expand Up @@ -130,7 +133,8 @@ extension JavaType {
/// Returns whether this type returns `JavaValue` from SwiftJava
var implementsJavaValue: Bool {
switch self {
case .boolean, .byte, .char, .short, .int, .long, .float, .double, .void, .javaLangString:
case .boolean, .byte, .char, .short, .int, .long, .float, .double, .void, .javaLangString,
.javaNetURI, .javaUtilDate, .javaUtilLocale, .javaUtilTimeZone:
true
case .array(let element):
element.implementsJavaValue
Expand Down
30 changes: 23 additions & 7 deletions Sources/JExtractSwiftLib/JNI/JNIJavaTypeTranslator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,21 +42,31 @@ enum JNIJavaTypeTranslator {

case .string: return .javaLangString

case .foundationCGFloat: return .double

case .foundationURL, .essentialsURL:
return config.effectiveFoundationTypesAsJavaTypes ? .javaNetURI : nil
case .foundationDate, .essentialsDate:
return config.effectiveFoundationTypesAsJavaTypes ? .javaUtilDate : nil
case .foundationData, .essentialsData:
return config.effectiveFoundationTypesAsJavaTypes ? .array(.byte) : nil
case .foundationLocale, .essentialsLocale:
return config.effectiveFoundationTypesAsJavaTypes ? .javaUtilLocale : nil
case .foundationTimeZone, .essentialsTimeZone:
return config.effectiveFoundationTypesAsJavaTypes ? .javaUtilTimeZone : nil

case .unsafeRawPointer, .unsafeMutableRawPointer,
.unsafePointer, .unsafeMutablePointer,
.unsafeRawBufferPointer, .unsafeMutableRawBufferPointer,
.unsafeBufferPointer, .unsafeMutableBufferPointer,
.opaquePointer,
.optional,
.foundationData, .foundationDataProtocol,
.essentialsData, .essentialsDataProtocol,
.foundationDataProtocol, .essentialsDataProtocol,
.array,
.dictionary,
.set,
.asyncStream, .asyncThrowingStream, .error,
.foundationDate, .essentialsDate,
.foundationUUID, .essentialsUUID,
.foundationURL, .essentialsURL:
.foundationUUID, .essentialsUUID:
return nil
}
}
Expand All @@ -68,6 +78,7 @@ enum JNIJavaTypeTranslator {
switch knownKind {
case .int: knownTypes.int64
case .uint: knownTypes.uint64
case .foundationCGFloat: knownTypes.double

case .bool, .int8, .uint8, .int16, .uint16, .int32, .uint32, .int64, .uint64,
.float, .double, .void, .string,
Expand All @@ -85,7 +96,9 @@ enum JNIJavaTypeTranslator {
.asyncStream, .asyncThrowingStream, .error,
.foundationDate, .essentialsDate,
.foundationUUID, .essentialsUUID,
.foundationURL, .essentialsURL:
.foundationURL, .essentialsURL,
.foundationLocale, .essentialsLocale,
.foundationTimeZone, .essentialsTimeZone:
nil
}
}
Expand Down Expand Up @@ -114,7 +127,10 @@ enum JNIJavaTypeTranslator {
.asyncStream, .asyncThrowingStream, .error,
.foundationDate, .essentialsDate,
.foundationUUID, .essentialsUUID,
.foundationURL, .essentialsURL:
.foundationURL, .essentialsURL,
.foundationLocale, .essentialsLocale,
.foundationTimeZone, .essentialsTimeZone,
.foundationCGFloat:
nil
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ extension JNISwift2JavaGenerator {
}

do {
// wrap-java maps `java.net.URI` and the other Foundation Java types on its own terms, so
// protocols implemented in Java don't pass Foundation values as they are; closures do.
let translator = JavaInterfaceProtocolWrapperGenerator(asyncFuncMode: config.effectiveAsyncFuncMode)
wrappers[type] = try translator.generate(for: type)
} catch {
Expand Down Expand Up @@ -105,6 +107,13 @@ extension JNISwift2JavaGenerator {

struct JavaInterfaceProtocolWrapperGenerator {
var asyncFuncMode: JExtractAsyncFuncMode = .default
/// Foundation values are `JavaValue`s that cross as Java types, as `String` does. Only for closures, whose
/// `@JavaInterface` jextract prints itself.
var foundationTypesAsJavaTypes: Bool = false

private func crossesAsIs(_ type: SwiftType) -> Bool {
type.isDirectlyTranslatedToWrapJava || (foundationTypesAsJavaTypes && type.isFoundationJavaValue)
}

func generate(for type: ExtractedNominalType) throws -> JavaInterfaceSwiftWrapper {
if !type.initializers.isEmpty
Expand Down Expand Up @@ -238,7 +247,7 @@ extension JNISwift2JavaGenerator {

private func translateParameter(parameterName: String, type: SwiftType) throws -> UpcallConversionStep {

if type.isDirectlyTranslatedToWrapJava {
if crossesAsIs(type) {
return .placeholder
}
if type.isSwiftError {
Expand Down Expand Up @@ -322,7 +331,7 @@ extension JNISwift2JavaGenerator {
methodName: String,
allowNilForObjects: Bool = false
) throws -> UpcallConversionStep {
if type.isDirectlyTranslatedToWrapJava {
if crossesAsIs(type) {
return .placeholder
}

Expand Down Expand Up @@ -507,6 +516,20 @@ enum UpcallConversionStep {
}

extension SwiftType {
/// A Foundation value that conforms to `JavaValue` in SwiftJavaRuntimeSupport, used with `foundationTypesAsJavaTypes`
var isFoundationJavaValue: Bool {
guard case .nominal(let nominal) = self, let kind = nominal.nominalTypeDecl.knownTypeKind else {
return false
}
switch kind {
case .foundationURL, .essentialsURL, .foundationDate, .essentialsDate, .foundationData, .essentialsData,
.foundationLocale, .essentialsLocale, .foundationTimeZone, .essentialsTimeZone:
return true
default:
return false
}
}

/// Indicates whether this type is translated by `wrap-java`
/// into the same type as `jextract`.
///
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -664,7 +664,12 @@ extension JNISwift2JavaGenerator {
genericRequirements: genericRequirements,
)

case .foundationDate, .essentialsDate, .foundationData, .essentialsData, .foundationURL, .essentialsURL:
case .foundationDate where !config.effectiveFoundationTypesAsJavaTypes,
.essentialsDate where !config.effectiveFoundationTypesAsJavaTypes,
.foundationData where !config.effectiveFoundationTypesAsJavaTypes,
.essentialsData where !config.effectiveFoundationTypesAsJavaTypes,
.foundationURL where !config.effectiveFoundationTypesAsJavaTypes,
.essentialsURL where !config.effectiveFoundationTypesAsJavaTypes:
break // Handled as wrapped struct

case .unsafeRawBufferPointer, .unsafeMutableRawBufferPointer:
Expand Down Expand Up @@ -1105,14 +1110,17 @@ extension JNISwift2JavaGenerator {
guard case .class(_, let name, let typeParameters) = type else {
return nil
}
// Spelled as one class name, such as `Optional<Boolean>` or `Optional<java.net.URI>`. The dots in
// the latter split it into a package and a name, so match the whole spelling.
let spelling = type.description
if typeParameters.isEmpty, spelling.hasPrefix("Optional<"), spelling.hasSuffix(">") {
return (JavaType(className: String(spelling.dropFirst("Optional<".count).dropLast())), "get")
}
switch name {
case "OptionalLong": return (.class(package: "java.lang", name: "Long"), "getAsLong")
case "OptionalInt": return (.class(package: "java.lang", name: "Integer"), "getAsInt")
case "OptionalDouble": return (.class(package: "java.lang", name: "Double"), "getAsDouble")
case "Optional" where typeParameters.count == 1: return (typeParameters[0], "get")
// Primitive optionals spelled as one class name, such as `Optional<Boolean>`
case _ where name.hasPrefix("Optional<") && name.hasSuffix(">"):
return (JavaType(className: name.dropFirst("Optional<".count).dropLast()), "get")
default: return nil
}
}
Expand Down Expand Up @@ -1169,11 +1177,13 @@ extension JNISwift2JavaGenerator {

if let knownType = nominalType.nominalTypeDecl.knownTypeKind {
switch knownType {
case .foundationDate, .essentialsDate:
case .foundationDate where !config.effectiveFoundationTypesAsJavaTypes,
.essentialsDate where !config.effectiveFoundationTypesAsJavaTypes:
// Handled as wrapped struct
break

case .foundationData, .essentialsData:
case .foundationData where !config.effectiveFoundationTypesAsJavaTypes,
.essentialsData where !config.effectiveFoundationTypesAsJavaTypes:
// Handled as wrapped struct
break

Expand Down Expand Up @@ -1309,7 +1319,12 @@ extension JNISwift2JavaGenerator {
genericRequirements: genericRequirements,
)

case .foundationDate, .essentialsDate, .foundationData, .essentialsData, .foundationURL, .essentialsURL:
case .foundationDate where !config.effectiveFoundationTypesAsJavaTypes,
.essentialsDate where !config.effectiveFoundationTypesAsJavaTypes,
.foundationData where !config.effectiveFoundationTypesAsJavaTypes,
.essentialsData where !config.effectiveFoundationTypesAsJavaTypes,
.foundationURL where !config.effectiveFoundationTypesAsJavaTypes,
.essentialsURL where !config.effectiveFoundationTypesAsJavaTypes:
// Handled as wrapped struct
break

Expand Down Expand Up @@ -1499,16 +1514,19 @@ extension JNISwift2JavaGenerator {
)
return .swiftSet(elementJavaType)

case .foundationDate, .essentialsDate:
case .foundationDate where !config.effectiveFoundationTypesAsJavaTypes,
.essentialsDate where !config.effectiveFoundationTypesAsJavaTypes:
return .class(package: nil, name: "Date")

case .foundationData, .essentialsData:
case .foundationData where !config.effectiveFoundationTypesAsJavaTypes,
.essentialsData where !config.effectiveFoundationTypesAsJavaTypes:
return .class(package: nil, name: "Data")

case .foundationDataProtocol, .essentialsDataProtocol:
return .class(package: nil, name: "DataProtocol")

case .foundationURL, .essentialsURL:
case .foundationURL where !config.effectiveFoundationTypesAsJavaTypes,
.essentialsURL where !config.effectiveFoundationTypesAsJavaTypes:
return .class(package: nil, name: "URL")

case .foundationUUID, .essentialsUUID:
Expand Down
Loading
Loading