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,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 compareDates(date1: Date, date2: Date) -> Bool {
return date1 == date2
}

public func dateFromSeconds(_ seconds: Double) -> Date {
return Date(timeIntervalSince1970: seconds)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
//===----------------------------------------------------------------------===//
//
// 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 static org.junit.jupiter.api.Assertions.*;

public class DateTest {
@Test
void date_functions() {
try (var arena = SwiftArena.ofConfined()) {
var date = MySwiftLibrary.dateFromSeconds(1000.50, arena);
assertEquals(1000.5, date.getTimeIntervalSince1970());

var date2 = Date.init(1000.5, arena);
assertTrue(MySwiftLibrary.compareDates(date, date2));

var date3 = Date.init(1000.49, arena);
assertFalse(MySwiftLibrary.compareDates(date, date3));
}
}

@Test
void date_helpers() {
try (var arena = SwiftArena.ofConfined()) {
var nowInstant = Instant.now();
var date = Date.fromInstant(nowInstant, arena);
var converted = date.toInstant();

long diffNanos = Math.abs(ChronoUnit.NANOS.between(nowInstant, converted));
System.out.println(diffNanos);
assertTrue(diffNanos < 1000,
"Precision loss should be contained to sub-microseconds. Actual drift: " + diffNanos + "ns");
assertEquals(nowInstant.getEpochSecond(), converted.getEpochSecond());
}
}

@Test
void date_timeIntervalSince1970() {
try (var arena = SwiftArena.ofConfined()) {
var date = Date.init(1000, arena);
assertEquals(1000, date.getTimeIntervalSince1970());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ extension SwiftKnownTypeDeclKind {
case .void: .void
case .unsafePointer, .unsafeMutablePointer, .unsafeRawBufferPointer, .unsafeMutableRawBufferPointer,
.unsafeBufferPointer, .unsafeMutableBufferPointer, .string, .foundationData, .foundationDataProtocol,
.essentialsData, .essentialsDataProtocol, .optional:
.essentialsData, .essentialsDataProtocol, .optional, .foundationDate, .essentialsDate:
nil
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ extension FFMSwift2JavaGenerator {

""")

printSwiftThunkImports(&printer)
self.lookupContext.symbolTable.printImportedModules(&printer)

for thunk in stt.renderGlobalThunks() {
printer.print(thunk)
Expand Down Expand Up @@ -140,67 +140,13 @@ extension FFMSwift2JavaGenerator {
"""
)

printSwiftThunkImports(&printer)
self.lookupContext.symbolTable.printImportedModules(&printer)

for thunk in stt.renderThunks(forType: ty) {
printer.print("\(thunk)")
printer.print("")
}
}

func printSwiftThunkImports(_ printer: inout CodePrinter) {
let mainSymbolSourceModules = Set(
self.lookupContext.symbolTable.importedModules.values.filter { $0.alternativeModules?.isMainSourceOfSymbols ?? false }.map(\.moduleName)
)

for module in self.lookupContext.symbolTable.importedModules.keys.sorted() {
guard module != "Swift" else {
continue
}

guard let alternativeModules = self.lookupContext.symbolTable.importedModules[module]?.alternativeModules else {
printer.print("import \(module)")
continue
}

// Try to print only on main module from relation chain as it has every other module.
guard !mainSymbolSourceModules.isDisjoint(with: alternativeModules.moduleNames) || alternativeModules.isMainSourceOfSymbols else {
if !alternativeModules.isMainSourceOfSymbols {
printer.print("import \(module)")
}
continue
}

var importGroups: [String: [String]] = [:]
for name in alternativeModules.moduleNames {
guard let otherModule = self.lookupContext.symbolTable.importedModules[name] else { continue }

let groupKey = otherModule.requiredAvailablityOfModuleWithName ?? otherModule.moduleName
importGroups[groupKey, default: []].append(otherModule.moduleName)
}

for (index, group) in importGroups.keys.sorted().enumerated() {
if index > 0 && importGroups.keys.count > 1 {
printer.print("#elseif canImport(\(group))")
} else {
printer.print("#if canImport(\(group))")
}

for groupModule in importGroups[group] ?? [] {
printer.print("import \(groupModule)")
}
}

if (importGroups.keys.isEmpty) {
printer.print("import \(module)")
} else {
printer.print("#else")
printer.print("import \(module)")
printer.print("#endif")
}
}
printer.println()
}
}

struct SwiftThunkTranslator {
Expand Down
6 changes: 3 additions & 3 deletions Sources/JExtractSwiftLib/JNI/JNIJavaTypeTranslator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ enum JNIJavaTypeTranslator {
.unsafePointer, .unsafeMutablePointer,
.unsafeRawBufferPointer, .unsafeMutableRawBufferPointer,
.unsafeBufferPointer, .unsafeMutableBufferPointer,
.optional, .foundationData, .foundationDataProtocol, .essentialsData, .essentialsDataProtocol, .array:
.optional, .foundationData, .foundationDataProtocol, .essentialsData, .essentialsDataProtocol, .array, .foundationDate, .essentialsDate:
return nil
}
}
Expand All @@ -62,7 +62,7 @@ enum JNIJavaTypeTranslator {
.unsafeRawBufferPointer, .unsafeMutableRawBufferPointer,
.unsafeBufferPointer, .unsafeMutableBufferPointer,
.optional, .foundationData, .foundationDataProtocol, .essentialsData, .essentialsDataProtocol,
.array:
.array, .foundationDate, .essentialsDate:
nil
}
}
Expand All @@ -79,7 +79,7 @@ enum JNIJavaTypeTranslator {
.unsafeRawBufferPointer, .unsafeMutableRawBufferPointer,
.unsafeBufferPointer, .unsafeMutableBufferPointer,
.optional, .foundationData, .foundationDataProtocol, .essentialsData, .essentialsDataProtocol,
.array:
.array, .foundationDate, .essentialsDate:
nil
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -265,12 +265,26 @@ extension JNISwift2JavaGenerator {
printToStringMethods(&printer, decl)
printer.println()

printSpecificTypeHelpers(&printer, decl)

printTypeMetadataAddressFunction(&printer, decl)
printer.println()
printDestroyFunction(&printer, decl)
}
}

/// Prints helpers for specific types like `Foundation.Date`
private func printSpecificTypeHelpers(_ printer: inout CodePrinter, _ decl: ImportedNominalType) {
guard let knownType = decl.swiftNominal.knownTypeKind else { return }

switch knownType {
case .foundationDate, .essentialsDate:
printFoundationDateHelpers(&printer, decl)

default:
break
}
}

private func printToStringMethods(_ printer: inout CodePrinter, _ decl: ImportedNominalType) {
printer.printBraceBlock("public String toString()") { printer in
Expand Down Expand Up @@ -689,4 +703,57 @@ extension JNISwift2JavaGenerator {
)
}
}

private func printFoundationDateHelpers(_ printer: inout CodePrinter, _ decl: ImportedNominalType) {
printer.print(
"""
/**
* Converts this wrapped date to a Java {@link java.time.Instant}.
* <p>
* This method constructs the {@code Instant} using the underlying {@code double} value
* representing seconds since the Unix Epoch (January 1, 1970).
* </p>
*
* @return A {@code java.time.Instant} derived from the floating-point timestamp.
*/
public java.time.Instant toInstant() {
long seconds = (long) this.getTimeIntervalSince1970();
long nanos = Math.round((this.getTimeIntervalSince1970() - seconds) * 1_000_000_000);
return java.time.Instant.ofEpochSecond(seconds, nanos);
}
"""
)
printer.println()
printer.print(
"""
/**
* Initializes a Swift {@code Foundation.Date} from a Java {@link java.time.Instant}.
*
* <h3>Warning: Precision Loss</h3>
* <p>
* <strong>The input precision will be degraded.</strong>
* </p>
* <p>
* Java's {@code Instant} stores time with <strong>nanosecond</strong> precision (9 decimal places).
* However, this class stores time as a 64-bit floating-point value.
* </p>
* <p>
* This leaves enough capacity for <strong>microsecond</strong> precision (approx. 6 decimal places).
* </p>
* <p>
* Consequently, the last ~3 digits of the {@code Instant}'s nanosecond field will be
* truncated or subjected to rounding errors during conversion.
* </p>
*
* @param instant The source timestamp to convert.
* @return A date derived from the input instant with microsecond precision.
*/
public static Date fromInstant(java.time.Instant instant, SwiftArena swiftArena$) {
Objects.requireNonNull(instant, "Instant cannot be null");
double timeIntervalSince1970 = instant.getEpochSecond() + (instant.getNano() / 1_000_000_000.0);
return Date.init(timeIntervalSince1970, swiftArena$);
}
"""
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,10 @@ extension JNISwift2JavaGenerator {
parameterName: parameterName
)

case .foundationDate, .essentialsDate:
// Handled as wrapped struct
break

default:
guard let javaType = JNIJavaTypeTranslator.translate(knownType: knownType, config: self.config) else {
throw JavaTranslationError.unsupportedSwiftType(swiftType)
Expand Down Expand Up @@ -687,6 +691,10 @@ extension JNISwift2JavaGenerator {
elementType: elementType
)

case .foundationDate, .essentialsDate:
// Handled as wrapped struct
break

default:
guard let javaType = JNIJavaTypeTranslator.translate(knownType: knownType, config: self.config) else {
throw JavaTranslationError.unsupportedSwiftType(swiftType)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,10 @@ extension JNISwift2JavaGenerator {
}
return try translateArrayParameter(elementType: elementType, parameterName: parameterName)

case .foundationDate, .essentialsDate:
// Handled as wrapped struct
break

default:
guard let javaType = JNIJavaTypeTranslator.translate(knownType: knownType, config: self.config),
javaType.implementsJavaValue else {
Expand Down Expand Up @@ -507,6 +511,10 @@ extension JNISwift2JavaGenerator {
}
return try translateArrayResult(elementType: elementType, resultName: resultName)

case .foundationDate, .essentialsDate:
// Handled as wrapped struct
break

default:
guard let javaType = JNIJavaTypeTranslator.translate(knownType: knownType, config: self.config), javaType.implementsJavaValue else {
throw JavaTranslationError.unsupportedSwiftType(swiftResult.type)
Expand Down Expand Up @@ -1175,4 +1183,4 @@ extension JNISwift2JavaGenerator {
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -745,6 +745,8 @@ extension JNISwift2JavaGenerator {

"""
)

self.lookupContext.symbolTable.printImportedModules(&printer)
}

private func printTypeMetadataAddressThunk(_ printer: inout CodePrinter, _ type: ImportedNominalType) {
Expand Down
13 changes: 12 additions & 1 deletion Sources/JExtractSwiftLib/Swift2JavaTranslator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -100,14 +100,25 @@ extension Swift2JavaTranslator {
visitor.visit(inputFile: input)
}

// If any API uses 'Foundation.Data' or 'FoundationEssentials.Data',
self.visitFoundationDeclsIfNeeded(with: visitor)
}

private func visitFoundationDeclsIfNeeded(with visitor: Swift2JavaVisitor) {
// If any API uses 'Foundation.Data' or 'FoundationEssentials.Data',
// import 'Data' as if it's declared in this module.
if let dataDecl = self.symbolTable[.foundationData] ?? self.symbolTable[.essentialsData] {
let dataProtocolDecl = (self.symbolTable[.foundationDataProtocol] ?? self.symbolTable[.essentialsDataProtocol])!
if self.isUsing(where: { $0 == dataDecl || $0 == dataProtocolDecl }) {
visitor.visit(nominalDecl: dataDecl.syntax!.asNominal!, in: nil, sourceFilePath: "Foundation/FAKE_FOUNDATION_DATA.swift")
}
}

// Foundation.Date
if let dateDecl = self.symbolTable[.foundationDate] ?? self.symbolTable[.essentialsDate] {
if self.isUsing(where: { $0 == dateDecl }) {
visitor.visit(nominalDecl: dateDecl.syntax!.asNominal!, in: nil, sourceFilePath: "Foundation/FAKE_FOUNDATION_DATE.swift")
}
}
}

package func prepareForTranslation() {
Expand Down
8 changes: 8 additions & 0 deletions Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownModules.swift
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,14 @@ private let foundationEssentialsSourceFile: SourceFileSyntax = """
public var count: Int { get }
public func withUnsafeBytes(_ body: (UnsafeRawBufferPointer) -> Void)
}

public struct Date {
/// The interval between the date object and 00:00:00 UTC on 1 January 1970.
public var timeIntervalSince1970: Double { get }

/// Returns a `Date` initialized relative to 00:00:00 UTC on 1 January 1970 by a given number of seconds.
public init(timeIntervalSince1970: Double)
}
"""

private var foundationSourceFile: SourceFileSyntax {
Expand Down
Loading