From 4b94ec4fcfc0417c84271a27d146df4f0ff0fe46 Mon Sep 17 00:00:00 2001 From: Mads Odgaard Date: Wed, 18 Feb 2026 12:10:11 +0100 Subject: [PATCH 1/3] add support for optional date and data --- .../Sources/MySwiftLibrary/Optionals.swift | 14 ++ .../java/com/example/swift/OptionalsTest.java | 21 +++ ...ISwift2JavaGenerator+JavaTranslation.swift | 132 ++++++++++------- ...wift2JavaGenerator+NativeTranslation.swift | 138 ++++++++++-------- 4 files changed, 192 insertions(+), 113 deletions(-) diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Optionals.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Optionals.swift index 113ecc969..3c08cb56a 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Optionals.swift +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Optionals.swift @@ -14,6 +14,12 @@ import SwiftJava +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + public func optionalBool(input: Bool?) -> Bool? { input } @@ -54,6 +60,14 @@ public func optionalClass(input: MySwiftClass?) -> MySwiftClass? { input } +public func optionalDate(input: Date?) -> Date? { + input +} + +public func optionalData(input: Data?) -> Data? { + input +} + public func optionalJavaKitLong(input: JavaLong?) -> Int64? { if let input { return input.longValue() diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/OptionalsTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/OptionalsTest.java index d26de1d1f..771cffc33 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/OptionalsTest.java +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/OptionalsTest.java @@ -18,6 +18,7 @@ import org.junit.jupiter.api.Test; import org.swift.swiftkit.core.SwiftArena; +import java.time.Instant; import java.util.Optional; import java.util.OptionalDouble; import java.util.OptionalInt; @@ -80,6 +81,26 @@ void optionalString() { assertEquals(Optional.of("Hello Swift!"), MySwiftLibrary.optionalString(Optional.of("Hello Swift!"))); } + @Test + void optionalDate() { + try (var arena = SwiftArena.ofConfined()) { + assertEquals(Optional.empty(), MySwiftLibrary.optionalDate(Optional.empty(), arena)); + var date = Date.fromInstant(Instant.now(), arena); + Optional optionalDate = MySwiftLibrary.optionalDate(Optional.of(date), arena); + assertTrue(optionalDate.isPresent()); + } + } + + @Test + void optionalData() { + try (var arena = SwiftArena.ofConfined()) { + assertEquals(Optional.empty(), MySwiftLibrary.optionalData(Optional.empty(), arena)); + var data = Data.fromByteArray(new byte[] { 1, 2 }, arena); + Optional optionalData = MySwiftLibrary.optionalData(Optional.of(data), arena); + assertTrue(optionalData.isPresent()); + } + } + @Test void optionalClass() { try (var arena = SwiftArena.ofConfined()) { diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift index 436956d2b..7b47f09f2 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift @@ -651,26 +651,37 @@ extension JNISwift2JavaGenerator { let nominalTypeName = nominalType.nominalTypeDecl.name if let knownType = nominalType.nominalTypeDecl.knownTypeKind { - guard let javaType = JNIJavaTypeTranslator.translate(knownType: knownType, config: self.config) else { - throw JavaTranslationError.unsupportedSwiftType(swiftType) - } + switch knownType { + case .foundationDate, .essentialsDate: + // Handled as wrapped struct + break - guard let translatedClass = javaType.optionalType, let placeholderValue = javaType.optionalPlaceholderValue - else { - throw JavaTranslationError.unsupportedSwiftType(swiftType) - } + case .foundationData, .essentialsData: + // Handled as wrapped struct + break - return TranslatedParameter( - parameter: JavaParameter( - name: parameterName, - type: JavaType(className: translatedClass), - annotations: parameterAnnotations - ), - conversion: .commaSeparated([ - .isOptionalPresent, - .method(.placeholder, function: "orElse", arguments: [.constant(placeholderValue)]), - ]) - ) + default: + guard let javaType = JNIJavaTypeTranslator.translate(knownType: knownType, config: self.config) else { + throw JavaTranslationError.unsupportedSwiftType(swiftType) + } + + guard let translatedClass = javaType.optionalType, let placeholderValue = javaType.optionalPlaceholderValue + else { + throw JavaTranslationError.unsupportedSwiftType(swiftType) + } + + return TranslatedParameter( + parameter: JavaParameter( + name: parameterName, + type: JavaType(className: translatedClass), + annotations: parameterAnnotations + ), + conversion: .commaSeparated([ + .isOptionalPresent, + .method(.placeholder, function: "orElse", arguments: [.constant(placeholderValue)]), + ]) + ) + } } if nominalType.isSwiftJavaWrapper { @@ -809,47 +820,58 @@ extension JNISwift2JavaGenerator { let nominalTypeName = nominalType.nominalTypeDecl.name if let knownType = nominalType.nominalTypeDecl.knownTypeKind { - guard let javaType = JNIJavaTypeTranslator.translate(knownType: knownType, config: self.config) else { - throw JavaTranslationError.unsupportedSwiftType(swiftType) - } + switch knownType { + case .foundationDate, .essentialsDate: + // Handled as wrapped struct + break - guard let returnType = javaType.optionalType, let optionalClass = javaType.optionalWrapperType else { - throw JavaTranslationError.unsupportedSwiftType(swiftType) - } + case .foundationData, .essentialsData: + // Handled as wrapped struct + break - // Check if we can fit the value and a discriminator byte in a primitive. - // so the return JNI value will be (value, discriminator) - if let nextIntergralTypeWithSpaceForByte = javaType.nextIntergralTypeWithSpaceForByte { - return TranslatedResult( - javaType: .class(package: nil, name: returnType), - annotations: parameterAnnotations, - outParameters: [], - conversion: .combinedValueToOptional( - .placeholder, - nextIntergralTypeWithSpaceForByte.javaType, - resultName: resultName, - valueType: javaType, - valueSizeInBytes: nextIntergralTypeWithSpaceForByte.valueBytes, - optionalType: optionalClass + default: + guard let javaType = JNIJavaTypeTranslator.translate(knownType: knownType, config: self.config) else { + throw JavaTranslationError.unsupportedSwiftType(swiftType) + } + + guard let returnType = javaType.optionalType, let optionalClass = javaType.optionalWrapperType else { + throw JavaTranslationError.unsupportedSwiftType(swiftType) + } + + // Check if we can fit the value and a discriminator byte in a primitive. + // so the return JNI value will be (value, discriminator) + if let nextIntergralTypeWithSpaceForByte = javaType.nextIntergralTypeWithSpaceForByte { + return TranslatedResult( + javaType: .class(package: nil, name: returnType), + annotations: parameterAnnotations, + outParameters: [], + conversion: .combinedValueToOptional( + .placeholder, + nextIntergralTypeWithSpaceForByte.javaType, + resultName: resultName, + valueType: javaType, + valueSizeInBytes: nextIntergralTypeWithSpaceForByte.valueBytes, + optionalType: optionalClass + ) ) - ) - } else { - // Otherwise, we return the result as normal, but - // use an indirect return for the discriminator. - return TranslatedResult( - javaType: .class(package: nil, name: returnType), - annotations: parameterAnnotations, - outParameters: [ - OutParameter(name: discriminatorName, type: .array(.byte), allocation: .newArray(.byte, size: 1)) - ], - conversion: .toOptionalFromIndirectReturn( - discriminatorName: .combinedName(component: "discriminator$"), - optionalClass: optionalClass, - javaType: javaType, - toValue: .placeholder, - resultName: resultName + } else { + // Otherwise, we return the result as normal, but + // use an indirect return for the discriminator. + return TranslatedResult( + javaType: .class(package: nil, name: returnType), + annotations: parameterAnnotations, + outParameters: [ + OutParameter(name: discriminatorName, type: .array(.byte), allocation: .newArray(.byte, size: 1)) + ], + conversion: .toOptionalFromIndirectReturn( + discriminatorName: .combinedName(component: "discriminator$"), + optionalClass: optionalClass, + javaType: javaType, + toValue: .placeholder, + resultName: resultName + ) ) - ) + } } } diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift index 65eb703de..3b78b6bec 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift @@ -372,25 +372,36 @@ extension JNISwift2JavaGenerator { let nominalTypeName = nominalType.nominalTypeDecl.name if let knownType = nominalType.nominalTypeDecl.knownTypeKind { - guard let javaType = JNIJavaTypeTranslator.translate(knownType: knownType, config: self.config), - javaType.implementsJavaValue - else { - throw JavaTranslationError.unsupportedSwiftType(swiftType) - } + switch knownType { + case .foundationDate, .essentialsDate: + // Handled as wrapped struct + break - return NativeParameter( - parameters: [ - JavaParameter(name: discriminatorName, type: .byte), - JavaParameter(name: valueName, type: javaType), - ], - conversion: .optionalLowering( - .initFromJNI(.placeholder, swiftType: swiftType), - discriminatorName: discriminatorName, - valueName: valueName - ), - indirectConversion: nil, - conversionCheck: nil - ) + case .foundationData, .essentialsData: + // Handled as wrapped struct + break + + default: + guard let javaType = JNIJavaTypeTranslator.translate(knownType: knownType, config: self.config), + javaType.implementsJavaValue + else { + throw JavaTranslationError.unsupportedSwiftType(swiftType) + } + + return NativeParameter( + parameters: [ + JavaParameter(name: discriminatorName, type: .byte), + JavaParameter(name: valueName, type: javaType), + ], + conversion: .optionalLowering( + .initFromJNI(.placeholder, swiftType: swiftType), + discriminatorName: discriminatorName, + valueName: valueName + ), + indirectConversion: nil, + conversionCheck: nil + ) + } } if nominalType.isSwiftJavaWrapper { @@ -438,46 +449,57 @@ extension JNISwift2JavaGenerator { switch swiftType { case .nominal(let nominalType): if let knownType = nominalType.nominalTypeDecl.knownTypeKind { - guard let javaType = JNIJavaTypeTranslator.translate(knownType: knownType, config: self.config), - javaType.implementsJavaValue - else { - throw JavaTranslationError.unsupportedSwiftType(swiftType) - } - - // Check if we can fit the value and a discriminator byte in a primitive. - // so the return JNI value will be (value, discriminator) - if let nextIntergralTypeWithSpaceForByte = javaType.nextIntergralTypeWithSpaceForByte { - return NativeResult( - javaType: nextIntergralTypeWithSpaceForByte.javaType, - conversion: .getJNIValue( - .optionalRaisingWidenIntegerType( - .placeholder, - resultName: resultName, - valueType: javaType, - combinedSwiftType: nextIntergralTypeWithSpaceForByte.swiftType, - valueSizeInBytes: nextIntergralTypeWithSpaceForByte.valueBytes - ) - ), - outParameters: [] - ) - } else { - // Use indirect byte array to store discriminator - - return NativeResult( - javaType: javaType, - conversion: .optionalRaisingIndirectReturn( - .getJNIValue(.placeholder), - returnType: javaType, - discriminatorParameterName: discriminatorName, - placeholderValue: .member( - .constant("\(swiftType)"), - member: "jniPlaceholderValue" - ) - ), - outParameters: [ - JavaParameter(name: discriminatorName, type: .array(.byte)) - ] - ) + switch knownType { + case .foundationDate, .essentialsDate: + // Handled as wrapped struct + break + + case .foundationData, .essentialsData: + // Handled as wrapped struct + break + + default: + guard let javaType = JNIJavaTypeTranslator.translate(knownType: knownType, config: self.config), + javaType.implementsJavaValue + else { + throw JavaTranslationError.unsupportedSwiftType(swiftType) + } + + // Check if we can fit the value and a discriminator byte in a primitive. + // so the return JNI value will be (value, discriminator) + if let nextIntergralTypeWithSpaceForByte = javaType.nextIntergralTypeWithSpaceForByte { + return NativeResult( + javaType: nextIntergralTypeWithSpaceForByte.javaType, + conversion: .getJNIValue( + .optionalRaisingWidenIntegerType( + .placeholder, + resultName: resultName, + valueType: javaType, + combinedSwiftType: nextIntergralTypeWithSpaceForByte.swiftType, + valueSizeInBytes: nextIntergralTypeWithSpaceForByte.valueBytes + ) + ), + outParameters: [] + ) + } else { + // Use indirect byte array to store discriminator + + return NativeResult( + javaType: javaType, + conversion: .optionalRaisingIndirectReturn( + .getJNIValue(.placeholder), + returnType: javaType, + discriminatorParameterName: discriminatorName, + placeholderValue: .member( + .constant("\(swiftType)"), + member: "jniPlaceholderValue" + ) + ), + outParameters: [ + JavaParameter(name: discriminatorName, type: .array(.byte)) + ] + ) + } } } From 54e00bd1c1885e4f101e7247439b7c8e4eeffaf5 Mon Sep 17 00:00:00 2001 From: Mads Odgaard Date: Wed, 18 Feb 2026 12:12:14 +0100 Subject: [PATCH 2/3] format --- .../JNISwift2JavaGenerator+NativeTranslation.swift | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift index 3b78b6bec..ea499c743 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift @@ -383,7 +383,7 @@ extension JNISwift2JavaGenerator { default: guard let javaType = JNIJavaTypeTranslator.translate(knownType: knownType, config: self.config), - javaType.implementsJavaValue + javaType.implementsJavaValue else { throw JavaTranslationError.unsupportedSwiftType(swiftType) } @@ -453,18 +453,18 @@ extension JNISwift2JavaGenerator { case .foundationDate, .essentialsDate: // Handled as wrapped struct break - + case .foundationData, .essentialsData: // Handled as wrapped struct break - + default: guard let javaType = JNIJavaTypeTranslator.translate(knownType: knownType, config: self.config), - javaType.implementsJavaValue + javaType.implementsJavaValue else { throw JavaTranslationError.unsupportedSwiftType(swiftType) } - + // Check if we can fit the value and a discriminator byte in a primitive. // so the return JNI value will be (value, discriminator) if let nextIntergralTypeWithSpaceForByte = javaType.nextIntergralTypeWithSpaceForByte { @@ -483,7 +483,7 @@ extension JNISwift2JavaGenerator { ) } else { // Use indirect byte array to store discriminator - + return NativeResult( javaType: javaType, conversion: .optionalRaisingIndirectReturn( From 7eb3a0d7dfa3fb4854f9810df222afa356722dd4 Mon Sep 17 00:00:00 2001 From: Mads Odgaard Date: Wed, 18 Feb 2026 12:28:49 +0100 Subject: [PATCH 3/3] add logging --- .../JNISwift2JavaGenerator+JavaTranslation.swift | 13 +++++++++---- .../JNISwift2JavaGenerator+NativeTranslation.swift | 3 +++ 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift index 7b47f09f2..43754be1b 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift @@ -23,7 +23,8 @@ extension JNISwift2JavaGenerator { javaPackage: self.javaPackage, javaClassLookupTable: self.javaClassLookupTable, knownTypes: SwiftKnownTypes(symbolTable: lookupContext.symbolTable), - protocolWrappers: self.interfaceProtocolWrappers + protocolWrappers: self.interfaceProtocolWrappers, + logger: self.logger ) } @@ -61,7 +62,8 @@ extension JNISwift2JavaGenerator { javaPackage: self.javaPackage, javaClassLookupTable: self.javaClassLookupTable, knownTypes: SwiftKnownTypes(symbolTable: lookupContext.symbolTable), - protocolWrappers: self.interfaceProtocolWrappers + protocolWrappers: self.interfaceProtocolWrappers, + logger: self.logger ) translated = try translation.translate(enumCase: decl) } catch { @@ -80,6 +82,7 @@ extension JNISwift2JavaGenerator { let javaClassLookupTable: JavaClassLookupTable var knownTypes: SwiftKnownTypes let protocolWrappers: [ImportedNominalType: JavaInterfaceSwiftWrapper] + let logger: Logger func translate(enumCase: ImportedEnumCase) throws -> TranslatedEnumCase { let nativeTranslation = NativeJavaTranslation( @@ -87,7 +90,8 @@ extension JNISwift2JavaGenerator { javaPackage: self.javaPackage, javaClassLookupTable: self.javaClassLookupTable, knownTypes: self.knownTypes, - protocolWrappers: self.protocolWrappers + protocolWrappers: self.protocolWrappers, + logger: self.logger ) let methodName = "" // TODO: Used for closures, replace with better name? @@ -197,7 +201,8 @@ extension JNISwift2JavaGenerator { javaPackage: self.javaPackage, javaClassLookupTable: self.javaClassLookupTable, knownTypes: self.knownTypes, - protocolWrappers: self.protocolWrappers + protocolWrappers: self.protocolWrappers, + logger: self.logger ) // Types with no parent will be outputted inside a "module" class. diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift index ea499c743..18f0b34c7 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift @@ -23,6 +23,7 @@ extension JNISwift2JavaGenerator { let javaClassLookupTable: JavaClassLookupTable var knownTypes: SwiftKnownTypes let protocolWrappers: [ImportedNominalType: JavaInterfaceSwiftWrapper] + let logger: Logger /// Translates a Swift function into the native JNI method signature. func translate( @@ -385,6 +386,7 @@ extension JNISwift2JavaGenerator { guard let javaType = JNIJavaTypeTranslator.translate(knownType: knownType, config: self.config), javaType.implementsJavaValue else { + self.logger.debug("Known type \(knownType) is not supported for optional parameters, skipping.") throw JavaTranslationError.unsupportedSwiftType(swiftType) } @@ -462,6 +464,7 @@ extension JNISwift2JavaGenerator { guard let javaType = JNIJavaTypeTranslator.translate(knownType: knownType, config: self.config), javaType.implementsJavaValue else { + self.logger.debug("Known type \(knownType) is not supported for optional results, skipping.") throw JavaTranslationError.unsupportedSwiftType(swiftType) }