From 7cba57bb56b9b716dfd7047f8aa83b76cdb5aeab Mon Sep 17 00:00:00 2001 From: Mads Odgaard Date: Mon, 28 Jul 2025 13:57:49 +0200 Subject: [PATCH 01/11] add support for optionals as parameters --- .../Sources/MySwiftLibrary/MySwiftClass.swift | 16 ++ .../com/example/swift/MySwiftClassTest.java | 22 +++ ...t2JavaGenerator+JavaBindingsPrinting.swift | 7 +- ...ISwift2JavaGenerator+JavaTranslation.swift | 138 +++++++++++++-- ...wift2JavaGenerator+NativeTranslation.swift | 165 ++++++++++++++---- ...ift2JavaGenerator+SwiftThunkPrinting.swift | 7 +- 6 files changed, 304 insertions(+), 51 deletions(-) diff --git a/Samples/JExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftClass.swift b/Samples/JExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftClass.swift index 9a78c3c27..357b11d3c 100644 --- a/Samples/JExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftClass.swift +++ b/Samples/JExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftClass.swift @@ -90,4 +90,20 @@ public class MySwiftClass { public func addXWithJavaLong(_ other: JavaLong) -> Int64 { return self.x + other.longValue() } + + public func optionalMethod(input: Optional) -> Int64 { + if let input { + return Int64(input) + } else { + return 0 + } + } + + public func optionalMethodClass(input: MySwiftClass?) -> Bool { + if let input { + return true + } else { + return false + } + } } diff --git a/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/MySwiftClassTest.java b/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/MySwiftClassTest.java index f034b9040..4d6185615 100644 --- a/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/MySwiftClassTest.java +++ b/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/MySwiftClassTest.java @@ -17,6 +17,9 @@ import org.junit.jupiter.api.Test; import org.swift.swiftkit.core.ConfinedSwiftMemorySession; +import java.util.Optional; +import java.util.OptionalInt; + import static org.junit.jupiter.api.Assertions.*; public class MySwiftClassTest { @@ -148,4 +151,23 @@ void addXWithJavaLong() { assertEquals(70, c1.addXWithJavaLong(javaLong)); } } + + @Test + void optionalMethod() { + try (var arena = new ConfinedSwiftMemorySession()) { + MySwiftClass c1 = MySwiftClass.init(20, 10, arena); + assertEquals(0, c1.optionalMethod(OptionalInt.empty())); + assertEquals(50, c1.optionalMethod(OptionalInt.of(50))); + } + } + + @Test + void optionalMethodClass() { + try (var arena = new ConfinedSwiftMemorySession()) { + MySwiftClass c1 = MySwiftClass.init(20, 10, arena); + MySwiftClass c2 = MySwiftClass.init(50, 10, arena); + assertFalse(c1.optionalMethodClass(Optional.empty())); + assertTrue(c1.optionalMethodClass(Optional.of(c2))); + } + } } \ No newline at end of file diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift index e0e5cb701..67356bc64 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift @@ -20,6 +20,7 @@ extension JNISwift2JavaGenerator { static let defaultJavaImports: Array = [ "org.swift.swiftkit.core.*", "org.swift.swiftkit.core.util.*", + "java.util.*" ] } @@ -270,7 +271,11 @@ extension JNISwift2JavaGenerator { if let selfParameter = nativeSignature.selfParameter { parameters.append(selfParameter) } - let renderedParameters = parameters.map { "\($0.javaType) \($0.name)"}.joined(separator: ", ") + let renderedParameters = parameters.flatMap { + $0.parameters.map { javaParameter in + "\(javaParameter.type) \(javaParameter.name)" + } + }.joined(separator: ", ") printer.print("private static native \(resultType) \(translatedDecl.nativeFunctionName)(\(renderedParameters));") } diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift index 57ea15b28..5f6bf8e9a 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift @@ -168,14 +168,26 @@ extension JNISwift2JavaGenerator { let nominalTypeName = nominalType.nominalTypeDecl.name if let knownType = nominalType.nominalTypeDecl.knownTypeKind { - guard let javaType = JNISwift2JavaGenerator.translate(knownType: knownType) else { - throw JavaTranslationError.unsupportedSwiftType(swiftType) + switch knownType { + case .optional: + guard let genericArgs = nominalType.genericArguments, genericArgs.count == 1 else { + throw JavaTranslationError.unsupportedSwiftType(swiftType) + } + return try translateOptionalParameter( + wrappedType: genericArgs[0], + parameterName: parameterName + ) + + default: + guard let javaType = JNISwift2JavaGenerator.translate(knownType: knownType) else { + throw JavaTranslationError.unsupportedSwiftType(swiftType) + } + + return TranslatedParameter( + parameter: JavaParameter(name: parameterName, type: javaType), + conversion: .placeholder + ) } - - return TranslatedParameter( - parameter: JavaParameter(name: parameterName, type: javaType), - conversion: .placeholder - ) } if nominalType.isJavaKitWrapper { @@ -216,7 +228,73 @@ extension JNISwift2JavaGenerator { conversion: .placeholder ) - case .metatype, .optional, .tuple, .existential, .opaque: + case .optional(let wrapped): + return try translateOptionalParameter( + wrappedType: wrapped, + parameterName: parameterName + ) + + case .metatype, .tuple, .existential, .opaque: + throw JavaTranslationError.unsupportedSwiftType(swiftType) + } + } + + func translateOptionalParameter( + wrappedType swiftType: SwiftType, + parameterName: String + ) throws -> TranslatedParameter { + switch swiftType { + case .nominal(let nominalType): + let nominalTypeName = nominalType.nominalTypeDecl.name + + if let knownType = nominalType.nominalTypeDecl.knownTypeKind { + guard let javaType = JNISwift2JavaGenerator.translate(knownType: knownType) else { + throw JavaTranslationError.unsupportedSwiftType(swiftType) + } + + let (translatedClass, orElseValue) = switch javaType { + case .boolean: ("Optional", "false") + case .byte: ("Optional", "0") + case .char: ("Optional", "0") + case .short: ("Optional", "0") + case .int: ("OptionalInt", "0") + case .long: ("OptionalLong", "0") + case .float: ("Optional", "0") + case .double: ("OptionalDouble", "0") + case .javaLangString: ("Optional", #""""#) + default: + throw JavaTranslationError.unsupportedSwiftType(swiftType) + } + + return TranslatedParameter( + parameter: JavaParameter( + name: parameterName, + type: JavaType(className: translatedClass) + ), + conversion: .commaSeparated([ + .isOptionalPresent, + .method(.placeholder, function: "orElse", arguments: [.constant(orElseValue)]) + ]) + ) + } + + guard !nominalType.isJavaKitWrapper else { + throw JavaTranslationError.unsupportedSwiftType(swiftType) + } + + // Assume JExtract imported class + return TranslatedParameter( + parameter: JavaParameter( + name: parameterName, + type: .class(package: nil, name: "Optional<\(nominalTypeName)>") + ), + conversion: .method( + .method(.placeholder, function: "map", arguments: [.constant("\(nominalType)::$memoryAddress")]), + function: "orElse", + arguments: [.constant("0L")] + ) + ) + default: throw JavaTranslationError.unsupportedSwiftType(swiftType) } } @@ -337,12 +415,24 @@ extension JNISwift2JavaGenerator { /// The value being converted case placeholder + case constant(String) + + // The input exploded into components. + case explodedName(component: String) + + // Convert the results of the inner steps to a comma separated list. + indirect case commaSeparated([JavaNativeConversionStep]) + /// `value.$memoryAddress()` indirect case valueMemoryAddress(JavaNativeConversionStep) /// Call `new \(Type)(\(placeholder), swiftArena$)` indirect case constructSwiftValue(JavaNativeConversionStep, JavaType) + indirect case method(JavaNativeConversionStep, function: String, arguments: [JavaNativeConversionStep]) + + case isOptionalPresent + /// Returns the conversion string applied to the placeholder. func render(_ printer: inout CodePrinter, _ placeholder: String) -> String { // NOTE: 'printer' is used if the conversion wants to cause side-effects. @@ -350,21 +440,39 @@ extension JNISwift2JavaGenerator { switch self { case .placeholder: return placeholder - + + case .constant(let value): + return value + + case .explodedName(let component): + return "\(placeholder)_\(component)" + + case .commaSeparated(let list): + return list.map({ $0.render(&printer, placeholder)}).joined(separator: ", ") + case .valueMemoryAddress: return "\(placeholder).$memoryAddress()" - + case .constructSwiftValue(let inner, let javaType): let inner = inner.render(&printer, placeholder) return "new \(javaType.className!)(\(inner), swiftArena$)" - + + case .isOptionalPresent: + return "(byte) (\(placeholder).isPresent() ? 1 : 0)" + + case .method(let inner, let methodName, let arguments): + let inner = inner.render(&printer, placeholder) + let args = arguments.map { $0.render(&printer, placeholder) } + let argsStr = args.joined(separator: ", ") + return "\(inner).\(methodName)(\(argsStr))" + } } /// Whether the conversion uses SwiftArena. var requiresSwiftArena: Bool { switch self { - case .placeholder: + case .placeholder, .constant, .explodedName, .isOptionalPresent: return false case .constructSwiftValue: @@ -372,6 +480,12 @@ extension JNISwift2JavaGenerator { case .valueMemoryAddress(let inner): return inner.requiresSwiftArena + + case .commaSeparated(let list): + return list.contains(where: { $0.requiresSwiftArena }) + + case .method(let inner, _, _): + return inner.requiresSwiftArena } } } diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift index 5ccefadb9..00894d80b 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift @@ -70,15 +70,28 @@ extension JNISwift2JavaGenerator { let nominalTypeName = nominalType.nominalTypeDecl.name if let knownType = nominalType.nominalTypeDecl.knownTypeKind { - guard let javaType = JNISwift2JavaGenerator.translate(knownType: knownType), javaType.implementsJavaValue else { - throw JavaTranslationError.unsupportedSwiftType(swiftParameter.type) + switch knownType { + case .optional: + guard let genericArgs = nominalType.genericArguments, genericArgs.count == 1 else { + throw JavaTranslationError.unsupportedSwiftType(swiftParameter.type) + } + return try translateOptionalParameter( + wrappedType: genericArgs[0], + parameterName: parameterName + ) + + default: + guard let javaType = JNISwift2JavaGenerator.translate(knownType: knownType), javaType.implementsJavaValue else { + throw JavaTranslationError.unsupportedSwiftType(swiftParameter.type) + } + + return NativeParameter( + parameters: [ + JavaParameter(name: parameterName, type: javaType) + ], + conversion: .initFromJNI(.placeholder, swiftType: swiftParameter.type) + ) } - - return NativeParameter( - name: parameterName, - javaType: javaType, - conversion: .initFromJNI(.placeholder, swiftType: swiftParameter.type) - ) } if nominalType.isJavaKitWrapper { @@ -87,23 +100,26 @@ extension JNISwift2JavaGenerator { } return NativeParameter( - name: parameterName, - javaType: javaType, + parameters: [ + JavaParameter(name: parameterName, type: javaType) + ], conversion: .initializeJavaKitWrapper(wrapperName: nominalTypeName) ) } // JExtract classes are passed as the pointer. return NativeParameter( - name: parameterName, - javaType: .long, + parameters: [ + JavaParameter(name: parameterName, type: .long) + ], conversion: .pointee(.extractSwiftValue(.placeholder, swiftType: swiftParameter.type)) ) case .tuple([]): return NativeParameter( - name: parameterName, - javaType: .void, + parameters: [ + JavaParameter(name: parameterName, type: .void) + ], conversion: .placeholder ) @@ -121,19 +137,72 @@ extension JNISwift2JavaGenerator { let result = try translateClosureResult(fn.resultType) return NativeParameter( - name: parameterName, - javaType: .class(package: javaPackage, name: "\(parentName).\(methodName).\(parameterName)"), + parameters: [ + JavaParameter(name: parameterName, type: .class(package: javaPackage, name: "\(parentName).\(methodName).\(parameterName)"),) + ], conversion: .closureLowering( parameters: parameters, result: result ) ) - case .metatype, .optional, .tuple, .existential, .opaque: + case .optional(let wrapped): + return try translateOptionalParameter( + wrappedType: wrapped, + parameterName: parameterName + ) + + case .metatype, .tuple, .existential, .opaque: throw JavaTranslationError.unsupportedSwiftType(swiftParameter.type) } } + func translateOptionalParameter( + wrappedType swiftType: SwiftType, + parameterName: String + ) throws -> NativeParameter { + let descriptorParameter = JavaParameter(name: "\(parameterName)_descriptor", type: .byte) + let valueName = "\(parameterName)_value" + + switch swiftType { + case .nominal(let nominalType): + if let knownType = nominalType.nominalTypeDecl.knownTypeKind { + guard let javaType = JNISwift2JavaGenerator.translate(knownType: knownType) else { + throw JavaTranslationError.unsupportedSwiftType(swiftType) + } + + return NativeParameter( + parameters: [ + descriptorParameter, + JavaParameter(name: valueName, type: javaType) + ], + conversion: .optionalLowering(.getJNIValue(.placeholder)) + ) + } + + guard !nominalType.isJavaKitWrapper else { + throw JavaTranslationError.unsupportedSwiftType(swiftType) + } + + // Assume JExtract wrapped class + return NativeParameter( + parameters: [JavaParameter(name: parameterName, type: .long)], + conversion: .pointee( + .optionalChain( + .extractSwiftValue( + .placeholder, + swiftType: swiftType, + allowNil: true + ) + ) + ) + ) + + default: + throw JavaTranslationError.unsupportedSwiftType(swiftType) + } + } + func translateClosureResult( _ type: SwiftType ) throws -> NativeResult { @@ -178,8 +247,9 @@ extension JNISwift2JavaGenerator { // Only support primitives for now. return NativeParameter( - name: parameterName, - javaType: javaType, + parameters: [ + JavaParameter(name: parameterName, type: javaType) + ], conversion: .getJValue(.placeholder) ) } @@ -238,12 +308,9 @@ extension JNISwift2JavaGenerator { } struct NativeParameter { - let name: String - let javaType: JavaType - - var jniType: JNIType { - javaType.jniType - } + /// One Swift parameter can be lowered to multiple parameters. + /// E.g. 'Optional' as (descriptor, value) pair. + var parameters: [JavaParameter] /// Represents how to convert the JNI parameter to a Swift parameter let conversion: NativeSwiftConversionStep @@ -269,7 +336,11 @@ extension JNISwift2JavaGenerator { indirect case initFromJNI(NativeSwiftConversionStep, swiftType: SwiftType) /// Extracts a swift type at a pointer given by a long. - indirect case extractSwiftValue(NativeSwiftConversionStep, swiftType: SwiftType) + indirect case extractSwiftValue( + NativeSwiftConversionStep, + swiftType: SwiftType, + allowNil: Bool = false + ) /// Allocate memory for a Swift value and outputs the pointer case allocateSwiftValue(name: String, swiftType: SwiftType) @@ -282,6 +353,10 @@ extension JNISwift2JavaGenerator { case initializeJavaKitWrapper(wrapperName: String) + indirect case optionalLowering(NativeSwiftConversionStep) + + indirect case optionalChain(NativeSwiftConversionStep) + /// Returns the conversion string applied to the placeholder. func render(_ printer: inout CodePrinter, _ placeholder: String) -> String { // NOTE: 'printer' is used if the conversion wants to cause side-effects. @@ -302,18 +377,28 @@ extension JNISwift2JavaGenerator { let inner = inner.render(&printer, placeholder) return "\(swiftType)(fromJNI: \(inner), in: environment!)" - case .extractSwiftValue(let inner, let swiftType): + case .extractSwiftValue(let inner, let swiftType, let allowNil): let inner = inner.render(&printer, placeholder) + let pointerName = "\(inner)$" + if !allowNil { + printer.print(#"assert(\#(inner) != 0, "\#(inner) memory address was null")"#) + } printer.print( """ - assert(\(inner) != 0, "\(inner) memory address was null") let \(inner)Bits$ = Int(Int64(fromJNI: \(inner), in: environment!)) - guard let \(inner)$ = UnsafeMutablePointer<\(swiftType)>(bitPattern: \(inner)Bits$) else { - fatalError("\(inner) memory address was null in call to \\(#function)!") - } + let \(pointerName) = UnsafeMutablePointer<\(swiftType)>(bitPattern: \(inner)Bits$) """ ) - return "\(inner)$" + if !allowNil { + printer.print( + """ + guard let \(pointerName) else { + fatalError("\(inner) memory address was null in call to \\(#function)!") + } + """ + ) + } + return pointerName case .allocateSwiftValue(let name, let swiftType): let pointerName = "\(name)$" @@ -336,15 +421,17 @@ extension JNISwift2JavaGenerator { let methodSignature = MethodSignature( resultType: nativeResult.javaType, - parameterTypes: parameters.map(\.javaType) + parameterTypes: parameters.flatMap { $0.parameters.map(\.type) } ) - let closureParameters = !parameters.isEmpty ? "\(parameters.map(\.name).joined(separator: ", ")) in" : "" + let names = parameters.flatMap { $0.parameters.map(\.name) } + let closureParameters = !parameters.isEmpty ? "\(names.joined(separator: ", ")) in" : "" printer.print("{ \(closureParameters)") printer.indent() + // TODO: Add support for types that are lowered to multiple parameters in closures let arguments = parameters.map { - $0.conversion.render(&printer, $0.name) + $0.conversion.render(&printer, $0.parameters.first!.name) } printer.print( @@ -371,6 +458,14 @@ extension JNISwift2JavaGenerator { case .initializeJavaKitWrapper(let wrapperName): return "\(wrapperName)(javaThis: \(placeholder), environment: environment!)" + + case .optionalLowering(let valueConversion): + let value = valueConversion.render(&printer, "\(placeholder)_value") + return "\(placeholder)_descriptor == 1 ? \(value) : nil" + + case .optionalChain(let inner): + let inner = inner.render(&printer, placeholder) + return "\(inner)?" } } } diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift index 35d4dbef1..092ac6773 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift @@ -135,7 +135,7 @@ extension JNISwift2JavaGenerator { &printer, javaMethodName: translatedDecl.nativeFunctionName, parentName: translatedDecl.parentName, - parameters: parameters.map { JavaParameter(name: $0.name, type: $0.javaType) }, + parameters: parameters.flatMap { $0.parameters }, resultType: nativeSignature.result.javaType.jniType ) { printer in self.printFunctionDowncall(&printer, decl) @@ -155,8 +155,9 @@ extension JNISwift2JavaGenerator { // Regular parameters. var arguments = [String]() - for parameter in nativeSignature.parameters { - let lowered = parameter.conversion.render(&printer, parameter.name) + for (idx, parameter) in nativeSignature.parameters.enumerated() { + let javaParameterName = translatedDecl.translatedFunctionSignature.parameters[idx].parameter.name + let lowered = parameter.conversion.render(&printer, javaParameterName) arguments.append(lowered) } From dc619ae75bab6fba1dcf14db3c85d6cf657d34a0 Mon Sep 17 00:00:00 2001 From: Mads Odgaard Date: Tue, 29 Jul 2025 10:32:36 +0200 Subject: [PATCH 02/11] add support for basic primitive types as optional returns --- .../Sources/MySwiftLibrary/MySwiftClass.swift | 36 ++++-- .../com/example/swift/MySwiftClassTest.java | 51 ++++++-- .../Convenience/JavaType+Extensions.swift | 10 ++ ...ISwift2JavaGenerator+JavaTranslation.swift | 118 ++++++++++++++++-- ...wift2JavaGenerator+NativeTranslation.swift | 89 +++++++++++-- 5 files changed, 262 insertions(+), 42 deletions(-) diff --git a/Samples/JExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftClass.swift b/Samples/JExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftClass.swift index 357b11d3c..875fc6f52 100644 --- a/Samples/JExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftClass.swift +++ b/Samples/JExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftClass.swift @@ -91,19 +91,31 @@ public class MySwiftClass { return self.x + other.longValue() } - public func optionalMethod(input: Optional) -> Int64 { - if let input { - return Int64(input) - } else { - return 0 - } + public func optionalBool(input: Optional) -> Bool? { + return input } - public func optionalMethodClass(input: MySwiftClass?) -> Bool { - if let input { - return true - } else { - return false - } + public func optionalByte(input: Optional) -> Int8? { + return input } + + public func optionalChar(input: Optional) -> UInt16? { + return input + } + + public func optionalShort(input: Optional) -> Int16? { + return input + } + + public func optionalInt(input: Optional) -> Int32? { + return input + } + +// public func optionalMethodClass(input: MySwiftClass?) -> > { +// if let input { +// return MySwiftClass(x: input.x * 10, y: input.y * 10) +// } else { +// return nil +// } +// } } diff --git a/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/MySwiftClassTest.java b/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/MySwiftClassTest.java index 4d6185615..6ce6a117c 100644 --- a/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/MySwiftClassTest.java +++ b/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/MySwiftClassTest.java @@ -19,6 +19,7 @@ import java.util.Optional; import java.util.OptionalInt; +import java.util.OptionalLong; import static org.junit.jupiter.api.Assertions.*; @@ -153,21 +154,57 @@ void addXWithJavaLong() { } @Test - void optionalMethod() { + void optionalBool() { try (var arena = new ConfinedSwiftMemorySession()) { MySwiftClass c1 = MySwiftClass.init(20, 10, arena); - assertEquals(0, c1.optionalMethod(OptionalInt.empty())); - assertEquals(50, c1.optionalMethod(OptionalInt.of(50))); + assertEquals(Optional.empty(), c1.optionalBool(Optional.empty())); + assertEquals(Optional.of(true), c1.optionalBool(Optional.of(true))); } } @Test - void optionalMethodClass() { + void optionalByte() { try (var arena = new ConfinedSwiftMemorySession()) { MySwiftClass c1 = MySwiftClass.init(20, 10, arena); - MySwiftClass c2 = MySwiftClass.init(50, 10, arena); - assertFalse(c1.optionalMethodClass(Optional.empty())); - assertTrue(c1.optionalMethodClass(Optional.of(c2))); + assertEquals(Optional.empty(), c1.optionalByte(Optional.empty())); + assertEquals(Optional.of((byte) 1) , c1.optionalByte(Optional.of((byte) 1))); + } + } + + @Test + void optionalChar() { + try (var arena = new ConfinedSwiftMemorySession()) { + MySwiftClass c1 = MySwiftClass.init(20, 10, arena); + assertEquals(Optional.empty(), c1.optionalChar(Optional.empty())); + assertEquals(Optional.of((char) 42), c1.optionalChar(Optional.of((char) 42))); + } + } + + @Test + void optionalShort() { + try (var arena = new ConfinedSwiftMemorySession()) { + MySwiftClass c1 = MySwiftClass.init(20, 10, arena); + assertEquals(Optional.empty(), c1.optionalShort(Optional.empty())); + assertEquals(Optional.of((short) -250), c1.optionalShort(Optional.of((short) -250))); + } + } + + @Test + void optionalInt() { + try (var arena = new ConfinedSwiftMemorySession()) { + MySwiftClass c1 = MySwiftClass.init(20, 10, arena); + assertEquals(OptionalInt.empty(), c1.optionalInt(OptionalInt.empty())); + assertEquals(OptionalInt.of(999), c1.optionalInt(OptionalInt.of(999))); } } + +// @Test +// void optionalMethodClass() { +// try (var arena = new ConfinedSwiftMemorySession()) { +// MySwiftClass c1 = MySwiftClass.init(20, 10, arena); +// MySwiftClass c2 = MySwiftClass.init(50, 10, arena); +// assertFalse(c1.optionalMethodClass(Optional.empty())); +// assertTrue(c1.optionalMethodClass(Optional.of(c2))); +// } +// } } \ No newline at end of file diff --git a/Sources/JExtractSwiftLib/Convenience/JavaType+Extensions.swift b/Sources/JExtractSwiftLib/Convenience/JavaType+Extensions.swift index f9a67419d..ebf1dbbd9 100644 --- a/Sources/JExtractSwiftLib/Convenience/JavaType+Extensions.swift +++ b/Sources/JExtractSwiftLib/Convenience/JavaType+Extensions.swift @@ -36,4 +36,14 @@ extension JavaType { case .void: fatalError("There is no type signature for 'void'") } } + + /// Returns the next integral type with space for self and an additional byte. + var nextIntergralTypeWithSpaceForByte: (java: JavaType, swift: String, valueBytes: Int)? { + switch self { + case .boolean, .byte: (.short, "Int16", 1) + case .char, .short: (.int, "Int32", 2) + case .int: (.long, "Int64", 4) + default: nil + } + } } diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift index 5f6bf8e9a..31911ba8a 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift @@ -254,11 +254,11 @@ extension JNISwift2JavaGenerator { let (translatedClass, orElseValue) = switch javaType { case .boolean: ("Optional", "false") - case .byte: ("Optional", "0") - case .char: ("Optional", "0") - case .short: ("Optional", "0") + case .byte: ("Optional", "(byte) 0") + case .char: ("Optional", "(char) 0") + case .short: ("Optional", "(short) 0") case .int: ("OptionalInt", "0") - case .long: ("OptionalLong", "0") + case .long: ("OptionalLong", "0L") case .float: ("Optional", "0") case .double: ("OptionalDouble", "0") case .javaLangString: ("Optional", #""""#) @@ -302,17 +302,28 @@ extension JNISwift2JavaGenerator { func translate( swiftResult: SwiftResult ) throws -> TranslatedResult { - switch swiftResult.type { + let swiftType = swiftResult.type + + switch swiftType { case .nominal(let nominalType): if let knownType = nominalType.nominalTypeDecl.knownTypeKind { - guard let javaType = JNISwift2JavaGenerator.translate(knownType: knownType) else { - throw JavaTranslationError.unsupportedSwiftType(swiftResult.type) - } + switch knownType { + case .optional: + guard let genericArgs = nominalType.genericArguments, genericArgs.count == 1 else { + throw JavaTranslationError.unsupportedSwiftType(swiftType) + } + return try translateOptionalResult(wrappedType: genericArgs[0]) - return TranslatedResult( - javaType: javaType, - conversion: .placeholder - ) + default: + guard let javaType = JNISwift2JavaGenerator.translate(knownType: knownType) else { + throw JavaTranslationError.unsupportedSwiftType(swiftResult.type) + } + + return TranslatedResult( + javaType: javaType, + conversion: .placeholder + ) + } } if nominalType.isJavaKitWrapper { @@ -329,10 +340,70 @@ extension JNISwift2JavaGenerator { case .tuple([]): return TranslatedResult(javaType: .void, conversion: .placeholder) - case .metatype, .optional, .tuple, .function, .existential, .opaque: + case .optional(let wrapped): + return try translateOptionalResult(wrappedType: wrapped) + + case .metatype, .tuple, .function, .existential, .opaque: throw JavaTranslationError.unsupportedSwiftType(swiftResult.type) } } + + func translateOptionalResult( + wrappedType swiftType: SwiftType + ) throws -> TranslatedResult { + switch swiftType { + case .nominal(let nominalType): + let nominalTypeName = nominalType.nominalTypeDecl.name + + if let knownType = nominalType.nominalTypeDecl.knownTypeKind { + guard let javaType = JNISwift2JavaGenerator.translate(knownType: knownType) else { + throw JavaTranslationError.unsupportedSwiftType(swiftType) + } + + let (returnType, staticCallee) = switch javaType { + case .boolean: ("Optional", "Optional") + case .byte: ("Optional", "Optional") + case .char: ("Optional", "Optional") + case .short: ("Optional", "Optional") + case .int: ("OptionalInt", "OptionalInt") + case .long: ("OptionalLong", "OptionalLong") + case .float: ("Optional", "Optional") + case .double: ("OptionalDouble", "OptionalDouble") + case .javaLangString: ("Optional", "Optional") + default: + 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), + conversion: .combinedValueToOptional( + .placeholder, + nextIntergralTypeWithSpaceForByte.java, + valueType: javaType, + valueSizeInBytes: nextIntergralTypeWithSpaceForByte.valueBytes, + optionalType: staticCallee + ) + ) + } else { + // Otherwise, we are forced to use an array. + fatalError() + } + } + + guard !nominalType.isJavaKitWrapper else { + throw JavaTranslationError.unsupportedSwiftType(swiftType) + } + + // Assume JExtract imported class + throw JavaTranslationError.unsupportedSwiftType(swiftType) + + default: + throw JavaTranslationError.unsupportedSwiftType(swiftType) + } + } } struct TranslatedFunctionDecl { @@ -433,6 +504,8 @@ extension JNISwift2JavaGenerator { case isOptionalPresent + indirect case combinedValueToOptional(JavaNativeConversionStep, JavaType, valueType: JavaType, valueSizeInBytes: Int, optionalType: String) + /// Returns the conversion string applied to the placeholder. func render(_ printer: inout CodePrinter, _ placeholder: String) -> String { // NOTE: 'printer' is used if the conversion wants to cause side-effects. @@ -466,6 +539,22 @@ extension JNISwift2JavaGenerator { let argsStr = args.joined(separator: ", ") return "\(inner).\(methodName)(\(argsStr))" + case .combinedValueToOptional(let combined, let combinedType, let valueType, let valueSizeInBytes, let optionalType): + let combined = combined.render(&printer, placeholder) + printer.print( + """ + \(combinedType) combined$ = \(combined); + byte discriminator$ = (byte) (combined$ & 0xFF); + """ + ) + + if valueType == .boolean { + printer.print("boolean value$ = ((byte) (combined$ >> 8)) != 0;") + } else { + printer.print("\(valueType) value$ = (\(valueType)) (combined$ >> \(valueSizeInBytes * 8));") + } + + return "discriminator$ == 1 ? \(optionalType).of(value$) : \(optionalType).empty()" } } @@ -486,6 +575,9 @@ extension JNISwift2JavaGenerator { case .method(let inner, _, _): return inner.requiresSwiftArena + + case .combinedValueToOptional(let inner, _, _, _, _): + return inner.requiresSwiftArena } } } diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift index 00894d80b..40f482a59 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift @@ -167,7 +167,7 @@ extension JNISwift2JavaGenerator { switch swiftType { case .nominal(let nominalType): if let knownType = nominalType.nominalTypeDecl.knownTypeKind { - guard let javaType = JNISwift2JavaGenerator.translate(knownType: knownType) else { + guard let javaType = JNISwift2JavaGenerator.translate(knownType: knownType), javaType.implementsJavaValue else { throw JavaTranslationError.unsupportedSwiftType(swiftType) } @@ -176,7 +176,7 @@ extension JNISwift2JavaGenerator { descriptorParameter, JavaParameter(name: valueName, type: javaType) ], - conversion: .optionalLowering(.getJNIValue(.placeholder)) + conversion: .optionalLowering(.initFromJNI(.placeholder, swiftType: swiftType)) ) } @@ -203,6 +203,49 @@ extension JNISwift2JavaGenerator { } } + func translateOptionalResult( + wrappedType swiftType: SwiftType + ) throws -> NativeResult { + switch swiftType { + case .nominal(let nominalType): + let nominalTypeName = nominalType.nominalTypeDecl.name + + if let knownType = nominalType.nominalTypeDecl.knownTypeKind { + guard let javaType = JNISwift2JavaGenerator.translate(knownType: knownType) 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.java, + conversion: .getJNIValue( + .optionalRaising( + .placeholder, + valueType: javaType, + combinedSwiftType: nextIntergralTypeWithSpaceForByte.swift, + valueSizeInBytes: nextIntergralTypeWithSpaceForByte.valueBytes + ) + ) + ) + } else { + throw JavaTranslationError.unsupportedSwiftType(swiftType) + } + } + + guard !nominalType.isJavaKitWrapper else { + throw JavaTranslationError.unsupportedSwiftType(swiftType) + } + + // Assume JExtract imported class + throw JavaTranslationError.unsupportedSwiftType(swiftType) + + default: + throw JavaTranslationError.unsupportedSwiftType(swiftType) + } + } + func translateClosureResult( _ type: SwiftType ) throws -> NativeResult { @@ -268,14 +311,23 @@ extension JNISwift2JavaGenerator { switch swiftResult.type { case .nominal(let nominalType): if let knownType = nominalType.nominalTypeDecl.knownTypeKind { - guard let javaType = JNISwift2JavaGenerator.translate(knownType: knownType), javaType.implementsJavaValue else { - throw JavaTranslationError.unsupportedSwiftType(swiftResult.type) - } + switch knownType { + case .optional: + guard let genericArgs = nominalType.genericArguments, genericArgs.count == 1 else { + throw JavaTranslationError.unsupportedSwiftType(swiftResult.type) + } + return try translateOptionalResult(wrappedType: swiftResult.type) - return NativeResult( - javaType: javaType, - conversion: .getJNIValue(.placeholder) - ) + default: + guard let javaType = JNISwift2JavaGenerator.translate(knownType: knownType), javaType.implementsJavaValue else { + throw JavaTranslationError.unsupportedSwiftType(swiftResult.type) + } + + return NativeResult( + javaType: javaType, + conversion: .getJNIValue(.placeholder) + ) + } } if nominalType.isJavaKitWrapper { @@ -293,7 +345,10 @@ extension JNISwift2JavaGenerator { conversion: .placeholder ) - case .metatype, .optional, .tuple, .function, .existential, .opaque: + case .optional(let wrapped): + return try translateOptionalResult(wrappedType: wrapped) + + case .metatype, .tuple, .function, .existential, .opaque: throw JavaTranslationError.unsupportedSwiftType(swiftResult.type) } @@ -357,6 +412,8 @@ extension JNISwift2JavaGenerator { indirect case optionalChain(NativeSwiftConversionStep) + indirect case optionalRaising(NativeSwiftConversionStep, valueType: JavaType, combinedSwiftType: String, valueSizeInBytes: Int) + /// Returns the conversion string applied to the placeholder. func render(_ printer: inout CodePrinter, _ placeholder: String) -> String { // NOTE: 'printer' is used if the conversion wants to cause side-effects. @@ -466,6 +523,18 @@ extension JNISwift2JavaGenerator { case .optionalChain(let inner): let inner = inner.render(&printer, placeholder) return "\(inner)?" + + case .optionalRaising(let inner, let valueType, let combinedSwiftType, let valueSizeInBytes): + let inner = inner.render(&printer, placeholder) + let value = valueType == .boolean ? "$0 ? 1 : 0" : "$0" + printer.print( + """ + let value$: \(combinedSwiftType) = \(inner).map { + \(combinedSwiftType)(\(value)) << \(valueSizeInBytes * 8) | \(combinedSwiftType)(1) + } ?? 0 + """ + ) + return "value$" } } } From 04b2d9edb84567c8f8d2ceaf6b5afe700a9620f7 Mon Sep 17 00:00:00 2001 From: Mads Odgaard Date: Tue, 29 Jul 2025 11:13:38 +0200 Subject: [PATCH 03/11] move tests --- .../Sources/MySwiftLibrary/MySwiftClass.swift | 20 ------- .../Sources/MySwiftLibrary/Optionals.swift | 33 +++++++++++ .../com/example/swift/MySwiftClassTest.java | 45 --------------- .../java/com/example/swift/OptionalsTest.java | 55 +++++++++++++++++++ ...t2JavaGenerator+JavaBindingsPrinting.swift | 1 + 5 files changed, 89 insertions(+), 65 deletions(-) create mode 100644 Samples/JExtractJNISampleApp/Sources/MySwiftLibrary/Optionals.swift create mode 100644 Samples/JExtractJNISampleApp/src/test/java/com/example/swift/OptionalsTest.java diff --git a/Samples/JExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftClass.swift b/Samples/JExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftClass.swift index 875fc6f52..b8d27a206 100644 --- a/Samples/JExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftClass.swift +++ b/Samples/JExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftClass.swift @@ -91,26 +91,6 @@ public class MySwiftClass { return self.x + other.longValue() } - public func optionalBool(input: Optional) -> Bool? { - return input - } - - public func optionalByte(input: Optional) -> Int8? { - return input - } - - public func optionalChar(input: Optional) -> UInt16? { - return input - } - - public func optionalShort(input: Optional) -> Int16? { - return input - } - - public func optionalInt(input: Optional) -> Int32? { - return input - } - // public func optionalMethodClass(input: MySwiftClass?) -> > { // if let input { // return MySwiftClass(x: input.x * 10, y: input.y * 10) diff --git a/Samples/JExtractJNISampleApp/Sources/MySwiftLibrary/Optionals.swift b/Samples/JExtractJNISampleApp/Sources/MySwiftLibrary/Optionals.swift new file mode 100644 index 000000000..ddeb693d4 --- /dev/null +++ b/Samples/JExtractJNISampleApp/Sources/MySwiftLibrary/Optionals.swift @@ -0,0 +1,33 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +public func optionalBool(input: Optional) -> Bool? { + return input +} + +public func optionalByte(input: Optional) -> Int8? { + return input +} + +public func optionalChar(input: Optional) -> UInt16? { + return input +} + +public func optionalShort(input: Optional) -> Int16? { + return input +} + +public func optionalInt(input: Optional) -> Int32? { + return input +} diff --git a/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/MySwiftClassTest.java b/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/MySwiftClassTest.java index 6ce6a117c..31ab37a03 100644 --- a/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/MySwiftClassTest.java +++ b/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/MySwiftClassTest.java @@ -153,51 +153,6 @@ void addXWithJavaLong() { } } - @Test - void optionalBool() { - try (var arena = new ConfinedSwiftMemorySession()) { - MySwiftClass c1 = MySwiftClass.init(20, 10, arena); - assertEquals(Optional.empty(), c1.optionalBool(Optional.empty())); - assertEquals(Optional.of(true), c1.optionalBool(Optional.of(true))); - } - } - - @Test - void optionalByte() { - try (var arena = new ConfinedSwiftMemorySession()) { - MySwiftClass c1 = MySwiftClass.init(20, 10, arena); - assertEquals(Optional.empty(), c1.optionalByte(Optional.empty())); - assertEquals(Optional.of((byte) 1) , c1.optionalByte(Optional.of((byte) 1))); - } - } - - @Test - void optionalChar() { - try (var arena = new ConfinedSwiftMemorySession()) { - MySwiftClass c1 = MySwiftClass.init(20, 10, arena); - assertEquals(Optional.empty(), c1.optionalChar(Optional.empty())); - assertEquals(Optional.of((char) 42), c1.optionalChar(Optional.of((char) 42))); - } - } - - @Test - void optionalShort() { - try (var arena = new ConfinedSwiftMemorySession()) { - MySwiftClass c1 = MySwiftClass.init(20, 10, arena); - assertEquals(Optional.empty(), c1.optionalShort(Optional.empty())); - assertEquals(Optional.of((short) -250), c1.optionalShort(Optional.of((short) -250))); - } - } - - @Test - void optionalInt() { - try (var arena = new ConfinedSwiftMemorySession()) { - MySwiftClass c1 = MySwiftClass.init(20, 10, arena); - assertEquals(OptionalInt.empty(), c1.optionalInt(OptionalInt.empty())); - assertEquals(OptionalInt.of(999), c1.optionalInt(OptionalInt.of(999))); - } - } - // @Test // void optionalMethodClass() { // try (var arena = new ConfinedSwiftMemorySession()) { diff --git a/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/OptionalsTest.java b/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/OptionalsTest.java new file mode 100644 index 000000000..c41c2ef6b --- /dev/null +++ b/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/OptionalsTest.java @@ -0,0 +1,55 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 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 java.util.Optional; +import java.util.OptionalInt; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class OptionalsTest { + @Test + void optionalBool() { + assertEquals(Optional.empty(), MySwiftLibrary.optionalBool(Optional.empty())); + assertEquals(Optional.of(true), MySwiftLibrary.optionalBool(Optional.of(true))); + } + + @Test + void optionalByte() { + assertEquals(Optional.empty(), MySwiftLibrary.optionalByte(Optional.empty())); + assertEquals(Optional.of((byte) 1) , MySwiftLibrary.optionalByte(Optional.of((byte) 1))); + } + + @Test + void optionalChar() { + assertEquals(Optional.empty(), MySwiftLibrary.optionalChar(Optional.empty())); + assertEquals(Optional.of((char) 42), MySwiftLibrary.optionalChar(Optional.of((char) 42))); + } + + @Test + void optionalShort() { + assertEquals(Optional.empty(), MySwiftLibrary.optionalShort(Optional.empty())); + assertEquals(Optional.of((short) -250), MySwiftLibrary.optionalShort(Optional.of((short) -250))); + } + + @Test + void optionalInt() { + assertEquals(OptionalInt.empty(), MySwiftLibrary.optionalInt(OptionalInt.empty())); + assertEquals(OptionalInt.of(999), MySwiftLibrary.optionalInt(OptionalInt.of(999))); + } +} \ No newline at end of file diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift index 67356bc64..d23b84520 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift @@ -63,6 +63,7 @@ extension JNISwift2JavaGenerator { private func printModule(_ printer: inout CodePrinter) { printHeader(&printer) printPackage(&printer) + printImports(&printer) printModuleClass(&printer) { printer in printer.print( From cea8b92ef7e40deac3f3a43eb45e28fd250df821 Mon Sep 17 00:00:00 2001 From: Mads Odgaard Date: Thu, 31 Jul 2025 12:38:37 +0200 Subject: [PATCH 04/11] add support for primitive types that cannot be widened --- .../Sources/MySwiftLibrary/Optionals.swift | 20 ++++ .../java/com/example/swift/OptionalsTest.java | 36 ++++++ ...t2JavaGenerator+JavaBindingsPrinting.swift | 28 ++++- ...ISwift2JavaGenerator+JavaTranslation.swift | 103 +++++++++++++++++- ...wift2JavaGenerator+NativeTranslation.swift | 89 +++++++++++++-- ...ift2JavaGenerator+SwiftThunkPrinting.swift | 14 ++- 6 files changed, 261 insertions(+), 29 deletions(-) diff --git a/Samples/JExtractJNISampleApp/Sources/MySwiftLibrary/Optionals.swift b/Samples/JExtractJNISampleApp/Sources/MySwiftLibrary/Optionals.swift index ddeb693d4..974c30c07 100644 --- a/Samples/JExtractJNISampleApp/Sources/MySwiftLibrary/Optionals.swift +++ b/Samples/JExtractJNISampleApp/Sources/MySwiftLibrary/Optionals.swift @@ -31,3 +31,23 @@ public func optionalShort(input: Optional) -> Int16? { public func optionalInt(input: Optional) -> Int32? { return input } + +public func optionalLong(input: Optional) -> Int64? { + return input +} + +public func optionalFloat(input: Optional) -> Float? { + return input +} + +public func optionalDouble(input: Optional) -> Double? { + return input +} + +public func optionalString(input: Optional) -> String? { + return input +} + +public func optionalClass(input: Optional) -> MySwiftClass? { + return input +} diff --git a/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/OptionalsTest.java b/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/OptionalsTest.java index c41c2ef6b..c143552d5 100644 --- a/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/OptionalsTest.java +++ b/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/OptionalsTest.java @@ -15,9 +15,12 @@ package com.example.swift; import org.junit.jupiter.api.Test; +import org.swift.swiftkit.core.ConfinedSwiftMemorySession; import java.util.Optional; +import java.util.OptionalDouble; import java.util.OptionalInt; +import java.util.OptionalLong; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -52,4 +55,37 @@ void optionalInt() { assertEquals(OptionalInt.empty(), MySwiftLibrary.optionalInt(OptionalInt.empty())); assertEquals(OptionalInt.of(999), MySwiftLibrary.optionalInt(OptionalInt.of(999))); } + + @Test + void optionalLong() { + assertEquals(OptionalLong.empty(), MySwiftLibrary.optionalLong(OptionalLong.empty())); + assertEquals(OptionalLong.of(999), MySwiftLibrary.optionalLong(OptionalLong.of(999))); + } + + @Test + void optionalFloat() { + assertEquals(Optional.empty(), MySwiftLibrary.optionalFloat(Optional.empty())); + assertEquals(Optional.of(3.14f), MySwiftLibrary.optionalFloat(Optional.of(3.14f))); + } + + @Test + void optionalDouble() { + assertEquals(OptionalDouble.empty(), MySwiftLibrary.optionalDouble(OptionalDouble.empty())); + assertEquals(OptionalDouble.of(2.718), MySwiftLibrary.optionalDouble(OptionalDouble.of(2.718))); + } + + @Test + void optionalString() { + assertEquals(Optional.empty(), MySwiftLibrary.optionalString(Optional.empty())); + assertEquals(Optional.of("Hello Swift!"), MySwiftLibrary.optionalString(Optional.of("Hello Swift!"))); + } + + @Test + void optionalClass() { + try (var arena = new ConfinedSwiftMemorySession()) { + MySwiftClass c = MySwiftClass.init(arena); + assertEquals(Optional.empty(), MySwiftLibrary.optionalClass(Optional.empty())); + assertEquals(Optional.of(c), MySwiftLibrary.optionalClass(Optional.of(c))); + } + } } \ No newline at end of file diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift index d23b84520..f7b4d0e39 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift @@ -12,6 +12,7 @@ // //===----------------------------------------------------------------------===// +import JavaTypes // MARK: Defaults @@ -268,14 +269,14 @@ extension JNISwift2JavaGenerator { let translatedDecl = translatedDecl(for: decl)! // Will always call with valid decl let nativeSignature = translatedDecl.nativeFunctionSignature let resultType = nativeSignature.result.javaType - var parameters = nativeSignature.parameters - if let selfParameter = nativeSignature.selfParameter { - parameters.append(selfParameter) + var parameters = nativeSignature.parameters.flatMap(\.parameters) + if let selfParameter = nativeSignature.selfParameter?.parameters { + parameters += selfParameter } - let renderedParameters = parameters.flatMap { - $0.parameters.map { javaParameter in + parameters += nativeSignature.result.outParameters + + let renderedParameters = parameters.map { javaParameter in "\(javaParameter.type) \(javaParameter.name)" - } }.joined(separator: ", ") printer.print("private static native \(resultType) \(translatedDecl.nativeFunctionName)(\(renderedParameters));") @@ -301,6 +302,12 @@ extension JNISwift2JavaGenerator { arguments.append(lowered) } + // Indirect return receivers + for outParameter in translatedFunctionSignature.resultType.outParameters { + printer.print("\(outParameter.type) \(outParameter.name) = \(outParameter.allocation.render());") + arguments.append(outParameter.name) + } + //=== Part 3: Downcall. // TODO: If we always generate a native method and a "public" method, we can actually choose our own thunk names // using the registry? @@ -356,4 +363,13 @@ extension JNISwift2JavaGenerator { ) } } + + private func renderIndirectReturnAllocation(for javaType: JavaType) -> String { + switch javaType { + case .array(let nested): + "new byte[]" + default: + fatalError("renderIndirectReturnAllocation not supported for \(javaType)") + } + } } diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift index 31911ba8a..c88927e89 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift @@ -259,8 +259,8 @@ extension JNISwift2JavaGenerator { case .short: ("Optional", "(short) 0") case .int: ("OptionalInt", "0") case .long: ("OptionalLong", "0L") - case .float: ("Optional", "0") - case .double: ("OptionalDouble", "0") + case .float: ("Optional", "0f") + case .double: ("OptionalDouble", "0.0") case .javaLangString: ("Optional", #""""#) default: throw JavaTranslationError.unsupportedSwiftType(swiftType) @@ -321,6 +321,7 @@ extension JNISwift2JavaGenerator { return TranslatedResult( javaType: javaType, + outParameters: [], conversion: .placeholder ) } @@ -334,11 +335,12 @@ extension JNISwift2JavaGenerator { let javaType = JavaType.class(package: nil, name: nominalType.nominalTypeDecl.name) return TranslatedResult( javaType: javaType, + outParameters: [], conversion: .constructSwiftValue(.placeholder, javaType) ) case .tuple([]): - return TranslatedResult(javaType: .void, conversion: .placeholder) + return TranslatedResult(javaType: .void, outParameters: [], conversion: .placeholder) case .optional(let wrapped): return try translateOptionalResult(wrappedType: wrapped) @@ -379,6 +381,7 @@ extension JNISwift2JavaGenerator { if let nextIntergralTypeWithSpaceForByte = javaType.nextIntergralTypeWithSpaceForByte { return TranslatedResult( javaType: .class(package: nil, name: returnType), + outParameters: [], conversion: .combinedValueToOptional( .placeholder, nextIntergralTypeWithSpaceForByte.java, @@ -388,8 +391,28 @@ extension JNISwift2JavaGenerator { ) ) } else { - // Otherwise, we are forced to use an array. - fatalError() + // Otherwise, we return the result as normal, but + // use an indirect return for the discriminator. + return TranslatedResult( + javaType: .class(package: nil, name: returnType), + outParameters: [ + OutParameter(name: "result_discriminator$", type: .array(.byte), allocation: .newArray(.byte, size: 1)) + ], + conversion: .aggregate( + name: "result$", + type: javaType, + [ + .ternary( + .equals( + .subscriptOf(.constant("result_discriminator$"), arguments: [.constant("0")]), + .constant("1") + ), + thenExp: .method(.constant(staticCallee), function: "of", arguments: [.constant("result$")]), + elseExp: .method(.constant(staticCallee), function: "empty") + ) + ] + ) + ) } } @@ -467,10 +490,33 @@ extension JNISwift2JavaGenerator { struct TranslatedResult { let javaType: JavaType + let outParameters: [OutParameter] + /// Represents how to convert the Java native result into a user-facing result. let conversion: JavaNativeConversionStep } + struct OutParameter { + enum Allocation { + case newArray(JavaType, size: Int) + + func render() -> String { + switch self { + case .newArray(let javaType, let size): + "new \(javaType)[\(size)]" + } + } + } + + let name: String + let type: JavaType + let allocation: Allocation + + var javaParameter: JavaParameter { + JavaParameter(name: self.name, type: self.type) + } + } + /// Represent a Swift closure type in the user facing Java API. /// /// Closures are translated to named functional interfaces in Java. @@ -500,12 +546,21 @@ extension JNISwift2JavaGenerator { /// Call `new \(Type)(\(placeholder), swiftArena$)` indirect case constructSwiftValue(JavaNativeConversionStep, JavaType) - indirect case method(JavaNativeConversionStep, function: String, arguments: [JavaNativeConversionStep]) + indirect case method(JavaNativeConversionStep, function: String, arguments: [JavaNativeConversionStep] = []) case isOptionalPresent indirect case combinedValueToOptional(JavaNativeConversionStep, JavaType, valueType: JavaType, valueSizeInBytes: Int, optionalType: String) + indirect case ternary(JavaNativeConversionStep, thenExp: JavaNativeConversionStep, elseExp: JavaNativeConversionStep) + + indirect case equals(JavaNativeConversionStep, JavaNativeConversionStep) + + indirect case subscriptOf(JavaNativeConversionStep, arguments: [JavaNativeConversionStep]) + + /// Perform multiple conversions using the same input. + case aggregate(name: String, type: JavaType, [JavaNativeConversionStep]) + /// Returns the conversion string applied to the placeholder. func render(_ printer: inout CodePrinter, _ placeholder: String) -> String { // NOTE: 'printer' is used if the conversion wants to cause side-effects. @@ -555,6 +610,30 @@ extension JNISwift2JavaGenerator { } return "discriminator$ == 1 ? \(optionalType).of(value$) : \(optionalType).empty()" + + case .ternary(let cond, let thenExp, let elseExp): + let cond = cond.render(&printer, placeholder) + let thenExp = thenExp.render(&printer, placeholder) + let elseExp = elseExp.render(&printer, placeholder) + return "(\(cond)) ? \(thenExp) : \(elseExp)" + + case .equals(let lhs, let rhs): + let lhs = lhs.render(&printer, placeholder) + let rhs = rhs.render(&printer, placeholder) + return "\(lhs) == \(rhs)" + + case .subscriptOf(let inner, let arguments): + let inner = inner.render(&printer, placeholder) + let arguments = arguments.map { $0.render(&printer, placeholder) } + return "\(inner)[\(arguments.joined(separator: ", "))]" + + case .aggregate(let name, let type, let steps): + precondition(!steps.isEmpty, "Aggregate must contain steps") + printer.print("\(type) \(name) = \(placeholder);") + let steps = steps.map { + $0.render(&printer, name) + } + return steps.last! } } @@ -578,6 +657,18 @@ extension JNISwift2JavaGenerator { case .combinedValueToOptional(let inner, _, _, _, _): return inner.requiresSwiftArena + + case .ternary(let cond, let thenExp, let elseExp): + return cond.requiresSwiftArena || thenExp.requiresSwiftArena || elseExp.requiresSwiftArena + + case .equals(let lhs, let rhs): + return lhs.requiresSwiftArena || rhs.requiresSwiftArena + + case .subscriptOf(let inner, _): + return inner.requiresSwiftArena + + case .aggregate(_, _, let steps): + return steps.contains(where: \.requiresSwiftArena) } } } diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift index 40f482a59..bf99486d4 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift @@ -211,7 +211,7 @@ extension JNISwift2JavaGenerator { let nominalTypeName = nominalType.nominalTypeDecl.name if let knownType = nominalType.nominalTypeDecl.knownTypeKind { - guard let javaType = JNISwift2JavaGenerator.translate(knownType: knownType) else { + guard let javaType = JNISwift2JavaGenerator.translate(knownType: knownType), javaType.implementsJavaValue else { throw JavaTranslationError.unsupportedSwiftType(swiftType) } @@ -221,24 +221,43 @@ extension JNISwift2JavaGenerator { return NativeResult( javaType: nextIntergralTypeWithSpaceForByte.java, conversion: .getJNIValue( - .optionalRaising( + .optionalRaisingWidenIntegerType( .placeholder, valueType: javaType, combinedSwiftType: nextIntergralTypeWithSpaceForByte.swift, valueSizeInBytes: nextIntergralTypeWithSpaceForByte.valueBytes ) - ) + ), + outParameters: [] ) } else { - throw JavaTranslationError.unsupportedSwiftType(swiftType) + // Use indirect byte array to store discriminator + let discriminatorName = "result_discriminator$" + + return NativeResult( + javaType: javaType, + conversion: .optionalRaisingIndirectReturn( + .getJNIValue(.optionalChain(.placeholder)), + discriminatorParameterName: discriminatorName, + placeholderValue: .member( + .constant("\(swiftType)"), + member: "jniPlaceholderValue" + ) + ), + outParameters: [ + JavaParameter(name: discriminatorName, type: .array(.byte)) + ] + ) } } guard !nominalType.isJavaKitWrapper else { + // TODO: Should be the same as above throw JavaTranslationError.unsupportedSwiftType(swiftType) } // Assume JExtract imported class + // TODO: Should be the same as above, just with a long and different conversion? throw JavaTranslationError.unsupportedSwiftType(swiftType) default: @@ -259,7 +278,8 @@ extension JNISwift2JavaGenerator { // Only support primitives for now. return NativeResult( javaType: javaType, - conversion: .initFromJNI(.placeholder, swiftType: type) + conversion: .initFromJNI(.placeholder, swiftType: type), + outParameters: [] ) } @@ -269,7 +289,8 @@ extension JNISwift2JavaGenerator { case .tuple([]): return NativeResult( javaType: .void, - conversion: .placeholder + conversion: .placeholder, + outParameters: [] ) case .function, .metatype, .optional, .tuple, .existential, .opaque: @@ -325,7 +346,8 @@ extension JNISwift2JavaGenerator { return NativeResult( javaType: javaType, - conversion: .getJNIValue(.placeholder) + conversion: .getJNIValue(.placeholder), + outParameters: [] ) } } @@ -336,13 +358,15 @@ extension JNISwift2JavaGenerator { return NativeResult( javaType: .long, - conversion: .getJNIValue(.allocateSwiftValue(name: "result", swiftType: swiftResult.type)) + conversion: .getJNIValue(.allocateSwiftValue(name: "result", swiftType: swiftResult.type)), + outParameters: [] ) case .tuple([]): return NativeResult( javaType: .void, - conversion: .placeholder + conversion: .placeholder, + outParameters: [] ) case .optional(let wrapped): @@ -374,6 +398,9 @@ extension JNISwift2JavaGenerator { struct NativeResult { let javaType: JavaType let conversion: NativeSwiftConversionStep + + /// Out parameters for populating the indirect return values. + var outParameters: [JavaParameter] } /// Describes how to convert values between Java types and Swift through JNI @@ -381,6 +408,8 @@ extension JNISwift2JavaGenerator { /// The value being converted case placeholder + case constant(String) + /// `value.getJNIValue(in:)` indirect case getJNIValue(NativeSwiftConversionStep) @@ -412,7 +441,13 @@ extension JNISwift2JavaGenerator { indirect case optionalChain(NativeSwiftConversionStep) - indirect case optionalRaising(NativeSwiftConversionStep, valueType: JavaType, combinedSwiftType: String, valueSizeInBytes: Int) + indirect case optionalRaisingWidenIntegerType(NativeSwiftConversionStep, valueType: JavaType, combinedSwiftType: String, valueSizeInBytes: Int) + + indirect case optionalRaisingIndirectReturn(NativeSwiftConversionStep, discriminatorParameterName: String, placeholderValue: NativeSwiftConversionStep) + + indirect case method(NativeSwiftConversionStep, function: String, arguments: [(String?, NativeSwiftConversionStep)] = []) + + indirect case member(NativeSwiftConversionStep, member: String) /// Returns the conversion string applied to the placeholder. func render(_ printer: inout CodePrinter, _ placeholder: String) -> String { @@ -422,6 +457,9 @@ extension JNISwift2JavaGenerator { case .placeholder: return placeholder + case .constant(let value): + return value + case .getJNIValue(let inner): let inner = inner.render(&printer, placeholder) return "\(inner).getJNIValue(in: environment!)" @@ -524,7 +562,7 @@ extension JNISwift2JavaGenerator { let inner = inner.render(&printer, placeholder) return "\(inner)?" - case .optionalRaising(let inner, let valueType, let combinedSwiftType, let valueSizeInBytes): + case .optionalRaisingWidenIntegerType(let inner, let valueType, let combinedSwiftType, let valueSizeInBytes): let inner = inner.render(&printer, placeholder) let value = valueType == .boolean ? "$0 ? 1 : 0" : "$0" printer.print( @@ -535,6 +573,35 @@ extension JNISwift2JavaGenerator { """ ) return "value$" + + case .optionalRaisingIndirectReturn(let inner, let discriminatorParameterName, let placeholderValue): + let inner = inner.render(&printer, placeholder) + let placeholderValue = placeholderValue.render(&printer, placeholder) + printer.print( + """ + let result$ = \(inner) + var flag$ = result$ != nil ? jbyte(1) : jbyte(2) + environment.interface.SetByteArrayRegion(environment, \(discriminatorParameterName), 0, 1, &flag$) + """ + ) + return "result$ ?? \(placeholderValue)" + + case .method(let inner, let methodName, let arguments): + let inner = inner.render(&printer, placeholder) + let args = arguments.map { name, value in + let value = value.render(&printer, placeholder) + if let name { + return "\(name): \(value)" + } else { + return value + } + } + let argsStr = args.joined(separator: ", ") + return "\(inner).\(methodName)(\(argsStr))" + + case .member(let inner, let member): + let inner = inner.render(&printer, placeholder) + return "\(inner).\(member)" } } } diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift index 092ac6773..d34b963bb 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift @@ -125,18 +125,20 @@ extension JNISwift2JavaGenerator { } let nativeSignature = translatedDecl.nativeFunctionSignature - var parameters = nativeSignature.parameters + var parameters = nativeSignature.parameters.flatMap(\.parameters) if let selfParameter = nativeSignature.selfParameter { - parameters.append(selfParameter) + parameters += selfParameter.parameters } + parameters += nativeSignature.result.outParameters + printCDecl( &printer, javaMethodName: translatedDecl.nativeFunctionName, parentName: translatedDecl.parentName, - parameters: parameters.flatMap { $0.parameters }, - resultType: nativeSignature.result.javaType.jniType + parameters: parameters, + resultType: nativeSignature.result.javaType ) { printer in self.printFunctionDowncall(&printer, decl) } @@ -231,7 +233,7 @@ extension JNISwift2JavaGenerator { javaMethodName: String, parentName: String, parameters: [JavaParameter], - resultType: JNIType, + resultType: JavaType, _ body: (inout CodePrinter) -> Void ) { let jniSignature = parameters.reduce(into: "") { signature, parameter in @@ -255,7 +257,7 @@ extension JNISwift2JavaGenerator { "environment: UnsafeMutablePointer!", "thisClass: jclass" ] + translatedParameters - let thunkReturnType = resultType != .void ? " -> \(resultType)" : "" + let thunkReturnType = resultType != .void ? " -> \(resultType.jniTypeName)" : "" // TODO: Think about function overloads printer.printBraceBlock( From 56a9096f4dada41fd4f9f4faaafbd6cd6d3ba82e Mon Sep 17 00:00:00 2001 From: Mads Odgaard Date: Thu, 31 Jul 2025 13:25:29 +0200 Subject: [PATCH 05/11] add support for jextract classes --- .../java/com/example/swift/OptionalsTest.java | 6 +- ...ISwift2JavaGenerator+JavaTranslation.swift | 64 +++++++++++++------ ...wift2JavaGenerator+NativeTranslation.swift | 56 +++++++++++----- 3 files changed, 90 insertions(+), 36 deletions(-) diff --git a/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/OptionalsTest.java b/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/OptionalsTest.java index c143552d5..b4f04cb32 100644 --- a/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/OptionalsTest.java +++ b/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/OptionalsTest.java @@ -84,8 +84,10 @@ void optionalString() { void optionalClass() { try (var arena = new ConfinedSwiftMemorySession()) { MySwiftClass c = MySwiftClass.init(arena); - assertEquals(Optional.empty(), MySwiftLibrary.optionalClass(Optional.empty())); - assertEquals(Optional.of(c), MySwiftLibrary.optionalClass(Optional.of(c))); + assertEquals(Optional.empty(), MySwiftLibrary.optionalClass(Optional.empty(), arena)); + Optional optionalClass = MySwiftLibrary.optionalClass(Optional.of(c), arena); + assertTrue(optionalClass.isPresent()); + assertEquals(c.getX(), optionalClass.get().getX()); } } } \ No newline at end of file diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift index c88927e89..1d49a1815 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift @@ -362,7 +362,7 @@ extension JNISwift2JavaGenerator { throw JavaTranslationError.unsupportedSwiftType(swiftType) } - let (returnType, staticCallee) = switch javaType { + let (returnType, optionalClass) = switch javaType { case .boolean: ("Optional", "Optional") case .byte: ("Optional", "Optional") case .char: ("Optional", "Optional") @@ -387,7 +387,7 @@ extension JNISwift2JavaGenerator { nextIntergralTypeWithSpaceForByte.java, valueType: javaType, valueSizeInBytes: nextIntergralTypeWithSpaceForByte.valueBytes, - optionalType: staticCallee + optionalType: optionalClass ) ) } else { @@ -398,19 +398,11 @@ extension JNISwift2JavaGenerator { outParameters: [ OutParameter(name: "result_discriminator$", type: .array(.byte), allocation: .newArray(.byte, size: 1)) ], - conversion: .aggregate( - name: "result$", - type: javaType, - [ - .ternary( - .equals( - .subscriptOf(.constant("result_discriminator$"), arguments: [.constant("0")]), - .constant("1") - ), - thenExp: .method(.constant(staticCallee), function: "of", arguments: [.constant("result$")]), - elseExp: .method(.constant(staticCallee), function: "empty") - ) - ] + conversion: .toOptionalFromIndirectReturn( + discriminatorName: "result_discriminator$", + optionalClass: optionalClass, + javaType: javaType, + toValue: .placeholder ) ) } @@ -420,8 +412,20 @@ extension JNISwift2JavaGenerator { throw JavaTranslationError.unsupportedSwiftType(swiftType) } - // Assume JExtract imported class - throw JavaTranslationError.unsupportedSwiftType(swiftType) + // We assume this is a JExtract class. + let returnType = JavaType.class(package: nil, name: "Optional<\(nominalTypeName)>") + return TranslatedResult( + javaType: returnType, + outParameters: [ + OutParameter(name: "result_discriminator$", type: .array(.byte), allocation: .newArray(.byte, size: 1)) + ], + conversion: .toOptionalFromIndirectReturn( + discriminatorName: "result_discriminator$", + optionalClass: "Optional", + javaType: .long, + toValue: .constructSwiftValue(.placeholder, .class(package: nil, name: nominalTypeName)) + ) + ) default: throw JavaTranslationError.unsupportedSwiftType(swiftType) @@ -558,6 +562,28 @@ extension JNISwift2JavaGenerator { indirect case subscriptOf(JavaNativeConversionStep, arguments: [JavaNativeConversionStep]) + static func toOptionalFromIndirectReturn( + discriminatorName: String, + optionalClass: String, + javaType: JavaType, + toValue valueConversion: JavaNativeConversionStep + ) -> JavaNativeConversionStep { + .aggregate( + name: "result$", + type: javaType, + [ + .ternary( + .equals( + .subscriptOf(.constant(discriminatorName), arguments: [.constant("0")]), + .constant("1") + ), + thenExp: .method(.constant(optionalClass), function: "of", arguments: [valueConversion]), + elseExp: .method(.constant(optionalClass), function: "empty") + ) + ] + ) + } + /// Perform multiple conversions using the same input. case aggregate(name: String, type: JavaType, [JavaNativeConversionStep]) @@ -652,8 +678,8 @@ extension JNISwift2JavaGenerator { case .commaSeparated(let list): return list.contains(where: { $0.requiresSwiftArena }) - case .method(let inner, _, _): - return inner.requiresSwiftArena + case .method(let inner, _, let args): + return inner.requiresSwiftArena || args.contains(where: \.requiresSwiftArena) case .combinedValueToOptional(let inner, _, _, _, _): return inner.requiresSwiftArena diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift index bf99486d4..b8578a833 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift @@ -237,7 +237,8 @@ extension JNISwift2JavaGenerator { return NativeResult( javaType: javaType, conversion: .optionalRaisingIndirectReturn( - .getJNIValue(.optionalChain(.placeholder)), + .getJNIValue(.placeholder), + returnType: javaType, discriminatorParameterName: discriminatorName, placeholderValue: .member( .constant("\(swiftType)"), @@ -257,8 +258,20 @@ extension JNISwift2JavaGenerator { } // Assume JExtract imported class - // TODO: Should be the same as above, just with a long and different conversion? - throw JavaTranslationError.unsupportedSwiftType(swiftType) + let discriminatorName = "result_discriminator$" + + return NativeResult( + javaType: .long, + conversion: .optionalRaisingIndirectReturn( + .getJNIValue(.allocateSwiftValue(name: "_result", swiftType: swiftType)), + returnType: .long, + discriminatorParameterName: discriminatorName, + placeholderValue: .constant("0") + ), + outParameters: [ + JavaParameter(name: discriminatorName, type: .array(.byte)) + ] + ) default: throw JavaTranslationError.unsupportedSwiftType(swiftType) @@ -443,7 +456,7 @@ extension JNISwift2JavaGenerator { indirect case optionalRaisingWidenIntegerType(NativeSwiftConversionStep, valueType: JavaType, combinedSwiftType: String, valueSizeInBytes: Int) - indirect case optionalRaisingIndirectReturn(NativeSwiftConversionStep, discriminatorParameterName: String, placeholderValue: NativeSwiftConversionStep) + indirect case optionalRaisingIndirectReturn(NativeSwiftConversionStep, returnType: JavaType, discriminatorParameterName: String, placeholderValue: NativeSwiftConversionStep) indirect case method(NativeSwiftConversionStep, function: String, arguments: [(String?, NativeSwiftConversionStep)] = []) @@ -574,17 +587,30 @@ extension JNISwift2JavaGenerator { ) return "value$" - case .optionalRaisingIndirectReturn(let inner, let discriminatorParameterName, let placeholderValue): - let inner = inner.render(&printer, placeholder) - let placeholderValue = placeholderValue.render(&printer, placeholder) - printer.print( - """ - let result$ = \(inner) - var flag$ = result$ != nil ? jbyte(1) : jbyte(2) - environment.interface.SetByteArrayRegion(environment, \(discriminatorParameterName), 0, 1, &flag$) - """ - ) - return "result$ ?? \(placeholderValue)" + case .optionalRaisingIndirectReturn(let inner, let returnType, let discriminatorParameterName, let placeholderValue): + printer.print("let result$: \(returnType.jniTypeName)") + printer.printBraceBlock("if let innerResult$ = \(placeholder)") { printer in + let inner = inner.render(&printer, "innerResult$") + printer.print( + """ + result$ = \(inner) + var flag$ = Int8(1) + environment.interface.SetByteArrayRegion(environment, \(discriminatorParameterName), 0, 1, &flag$) + """ + ) + } + printer.printBraceBlock("else") { printer in + let placeholderValue = placeholderValue.render(&printer, placeholder) + printer.print( + """ + result$ = \(placeholderValue) + var flag$ = Int8(0) + environment.interface.SetByteArrayRegion(environment, \(discriminatorParameterName), 0, 1, &flag$) + """ + ) + } + + return "result$" case .method(let inner, let methodName, let arguments): let inner = inner.render(&printer, placeholder) From a6c2fb92695264d9309ed676d9ab66dd86b671ea Mon Sep 17 00:00:00 2001 From: Mads Odgaard Date: Thu, 31 Jul 2025 14:00:13 +0200 Subject: [PATCH 06/11] small cleanups --- .../Sources/MySwiftLibrary/MySwiftClass.swift | 8 -------- .../test/java/com/example/swift/MySwiftClassTest.java | 10 ---------- .../JNISwift2JavaGenerator+JavaBindingsPrinting.swift | 9 --------- 3 files changed, 27 deletions(-) diff --git a/Samples/JExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftClass.swift b/Samples/JExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftClass.swift index b8d27a206..9a78c3c27 100644 --- a/Samples/JExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftClass.swift +++ b/Samples/JExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftClass.swift @@ -90,12 +90,4 @@ public class MySwiftClass { public func addXWithJavaLong(_ other: JavaLong) -> Int64 { return self.x + other.longValue() } - -// public func optionalMethodClass(input: MySwiftClass?) -> > { -// if let input { -// return MySwiftClass(x: input.x * 10, y: input.y * 10) -// } else { -// return nil -// } -// } } diff --git a/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/MySwiftClassTest.java b/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/MySwiftClassTest.java index 31ab37a03..e7de03add 100644 --- a/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/MySwiftClassTest.java +++ b/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/MySwiftClassTest.java @@ -152,14 +152,4 @@ void addXWithJavaLong() { assertEquals(70, c1.addXWithJavaLong(javaLong)); } } - -// @Test -// void optionalMethodClass() { -// try (var arena = new ConfinedSwiftMemorySession()) { -// MySwiftClass c1 = MySwiftClass.init(20, 10, arena); -// MySwiftClass c2 = MySwiftClass.init(50, 10, arena); -// assertFalse(c1.optionalMethodClass(Optional.empty())); -// assertTrue(c1.optionalMethodClass(Optional.of(c2))); -// } -// } } \ No newline at end of file diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift index 15a4eb773..fe6b156d9 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift @@ -372,13 +372,4 @@ extension JNISwift2JavaGenerator { ) } } - - private func renderIndirectReturnAllocation(for javaType: JavaType) -> String { - switch javaType { - case .array(let nested): - "new byte[]" - default: - fatalError("renderIndirectReturnAllocation not supported for \(javaType)") - } - } } From 528cbab646245d0315592a16b1bc46fdf18c5c28 Mon Sep 17 00:00:00 2001 From: Mads Odgaard Date: Thu, 31 Jul 2025 21:33:09 +0200 Subject: [PATCH 07/11] PR feedback --- .../Sources/MySwiftLibrary/Optionals.swift | 12 +++++ .../java/com/example/swift/OptionalsTest.java | 19 +++++++- .../Convenience/JavaType+Extensions.swift | 48 +++++++++++++++++-- ...ISwift2JavaGenerator+JavaTranslation.swift | 38 +++------------ ...wift2JavaGenerator+NativeTranslation.swift | 31 +++++++----- 5 files changed, 98 insertions(+), 50 deletions(-) diff --git a/Samples/JExtractJNISampleApp/Sources/MySwiftLibrary/Optionals.swift b/Samples/JExtractJNISampleApp/Sources/MySwiftLibrary/Optionals.swift index 974c30c07..0a8300b6d 100644 --- a/Samples/JExtractJNISampleApp/Sources/MySwiftLibrary/Optionals.swift +++ b/Samples/JExtractJNISampleApp/Sources/MySwiftLibrary/Optionals.swift @@ -51,3 +51,15 @@ public func optionalString(input: Optional) -> String? { public func optionalClass(input: Optional) -> MySwiftClass? { return input } + +public func multipleOptionals( + input1: Optional, + input2: Optional, + input3: Optional, + input4: Optional, + input5: Optional, + input6: Optional, + input7: Optional +) -> Int64? { + return 1 +} diff --git a/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/OptionalsTest.java b/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/OptionalsTest.java index b4f04cb32..ec9ff3b0f 100644 --- a/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/OptionalsTest.java +++ b/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/OptionalsTest.java @@ -2,7 +2,7 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information @@ -90,4 +90,21 @@ void optionalClass() { assertEquals(c.getX(), optionalClass.get().getX()); } } + + @Test + void multipleOptionals() { + try (var arena = new ConfinedSwiftMemorySession()) { + MySwiftClass c = MySwiftClass.init(arena); + OptionalLong result = MySwiftLibrary.multipleOptionals( + Optional.of((byte) 1), + Optional.of((short) 42), + OptionalInt.of(50), + OptionalLong.of(1000L), + Optional.of("42"), + Optional.of(c), + Optional.of(true) + ); + assertEquals(result, OptionalLong.of(1L)); + } + } } \ No newline at end of file diff --git a/Sources/JExtractSwiftLib/Convenience/JavaType+Extensions.swift b/Sources/JExtractSwiftLib/Convenience/JavaType+Extensions.swift index ebf1dbbd9..c46854ade 100644 --- a/Sources/JExtractSwiftLib/Convenience/JavaType+Extensions.swift +++ b/Sources/JExtractSwiftLib/Convenience/JavaType+Extensions.swift @@ -38,12 +38,52 @@ extension JavaType { } /// Returns the next integral type with space for self and an additional byte. - var nextIntergralTypeWithSpaceForByte: (java: JavaType, swift: String, valueBytes: Int)? { + var nextIntergralTypeWithSpaceForByte: (javaType: JavaType, swiftType: SwiftKnownTypeDeclKind, valueBytes: Int)? { switch self { - case .boolean, .byte: (.short, "Int16", 1) - case .char, .short: (.int, "Int32", 2) - case .int: (.long, "Int64", 4) + case .boolean, .byte: (.short, .int16, 1) + case .char, .short: (.int, .int32, 2) + case .int: (.long, .int64, 4) default: nil } } + + var optionalType: String? { + switch self { + case .boolean: "Optional" + case .byte: "Optional" + case .char: "Optional" + case .short: "Optional" + case .int: "OptionalInt" + case .long: "OptionalLong" + case .float: "Optional" + case .double: "OptionalDouble" + case .javaLangString: "Optional" + default: nil + } + } + + var optionalWrapperType: String? { + switch self { + case .boolean, .byte, .char, .short, .float, .javaLangString: "Optional" + case .int: "OptionalInt" + case .long: "OptionalLong" + case .double: "OptionalDouble" + default: nil + } + } + + var optionalPlaceholderValue: String? { + switch self { + case .boolean: "false" + case .byte: "(byte) 0" + case .char: "(char) 0" + case .short: "(short) 0" + case .int: "0" + case .long: "0L" + case .float: "0f" + case .double: "0.0" + case .array, .class: "null" + case .void: nil + } + } } diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift index ea7c63cbe..ee5b63c0b 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift @@ -289,17 +289,7 @@ extension JNISwift2JavaGenerator { throw JavaTranslationError.unsupportedSwiftType(swiftType) } - let (translatedClass, orElseValue) = switch javaType { - case .boolean: ("Optional", "false") - case .byte: ("Optional", "(byte) 0") - case .char: ("Optional", "(char) 0") - case .short: ("Optional", "(short) 0") - case .int: ("OptionalInt", "0") - case .long: ("OptionalLong", "0L") - case .float: ("Optional", "0f") - case .double: ("OptionalDouble", "0.0") - case .javaLangString: ("Optional", #""""#) - default: + guard let translatedClass = javaType.optionalType, let placeholderValue = javaType.optionalPlaceholderValue else { throw JavaTranslationError.unsupportedSwiftType(swiftType) } @@ -311,7 +301,7 @@ extension JNISwift2JavaGenerator { ), conversion: .commaSeparated([ .isOptionalPresent, - .method(.placeholder, function: "orElse", arguments: [.constant(orElseValue)]) + .method(.placeholder, function: "orElse", arguments: [.constant(placeholderValue)]) ]) ) } @@ -406,22 +396,12 @@ extension JNISwift2JavaGenerator { throw JavaTranslationError.unsupportedSwiftType(swiftType) } - let (returnType, optionalClass) = switch javaType { - case .boolean: ("Optional", "Optional") - case .byte: ("Optional", "Optional") - case .char: ("Optional", "Optional") - case .short: ("Optional", "Optional") - case .int: ("OptionalInt", "OptionalInt") - case .long: ("OptionalLong", "OptionalLong") - case .float: ("Optional", "Optional") - case .double: ("OptionalDouble", "OptionalDouble") - case .javaLangString: ("Optional", "Optional") - default: + 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) + // so the return JNI value will be (value, discriminator) if let nextIntergralTypeWithSpaceForByte = javaType.nextIntergralTypeWithSpaceForByte { return TranslatedResult( javaType: .class(package: nil, name: returnType), @@ -429,7 +409,7 @@ extension JNISwift2JavaGenerator { outParameters: [], conversion: .combinedValueToOptional( .placeholder, - nextIntergralTypeWithSpaceForByte.java, + nextIntergralTypeWithSpaceForByte.javaType, valueType: javaType, valueSizeInBytes: nextIntergralTypeWithSpaceForByte.valueBytes, optionalType: optionalClass @@ -578,9 +558,6 @@ extension JNISwift2JavaGenerator { case constant(String) - // The input exploded into components. - case explodedName(component: String) - // Convert the results of the inner steps to a comma separated list. indirect case commaSeparated([JavaNativeConversionStep]) @@ -640,9 +617,6 @@ extension JNISwift2JavaGenerator { case .constant(let value): return value - case .explodedName(let component): - return "\(placeholder)_\(component)" - case .commaSeparated(let list): return list.map({ $0.render(&printer, placeholder)}).joined(separator: ", ") @@ -713,7 +687,7 @@ extension JNISwift2JavaGenerator { /// Whether the conversion uses SwiftArena. var requiresSwiftArena: Bool { switch self { - case .placeholder, .constant, .explodedName, .isOptionalPresent: + case .placeholder, .constant, .isOptionalPresent: return false case .constructSwiftValue: diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift index f2b4560c1..4cbae8170 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift @@ -165,7 +165,7 @@ extension JNISwift2JavaGenerator { wrappedType swiftType: SwiftType, parameterName: String ) throws -> NativeParameter { - let descriptorParameter = JavaParameter(name: "\(parameterName)_descriptor", type: .byte) + let discriminatorName = "\(parameterName)_discriminator" let valueName = "\(parameterName)_value" switch swiftType { @@ -178,10 +178,14 @@ extension JNISwift2JavaGenerator { return NativeParameter( parameters: [ - descriptorParameter, + JavaParameter(name: discriminatorName, type: .byte), JavaParameter(name: valueName, type: javaType) ], - conversion: .optionalLowering(.initFromJNI(.placeholder, swiftType: swiftType)) + conversion: .optionalLowering( + .initFromJNI(.placeholder, swiftType: swiftType), + discriminatorName: discriminatorName, + valueName: valueName + ) ) } @@ -220,15 +224,15 @@ extension JNISwift2JavaGenerator { } // Check if we can fit the value and a discriminator byte in a primitive. - // so the return JNI value will be (value || discriminator) + // so the return JNI value will be (value, discriminator) if let nextIntergralTypeWithSpaceForByte = javaType.nextIntergralTypeWithSpaceForByte { return NativeResult( - javaType: nextIntergralTypeWithSpaceForByte.java, + javaType: nextIntergralTypeWithSpaceForByte.javaType, conversion: .getJNIValue( .optionalRaisingWidenIntegerType( .placeholder, valueType: javaType, - combinedSwiftType: nextIntergralTypeWithSpaceForByte.swift, + combinedSwiftType: nextIntergralTypeWithSpaceForByte.swiftType, valueSizeInBytes: nextIntergralTypeWithSpaceForByte.valueBytes ) ), @@ -454,11 +458,11 @@ extension JNISwift2JavaGenerator { case initializeJavaKitWrapper(wrapperName: String) - indirect case optionalLowering(NativeSwiftConversionStep) + indirect case optionalLowering(NativeSwiftConversionStep, discriminatorName: String, valueName: String) indirect case optionalChain(NativeSwiftConversionStep) - indirect case optionalRaisingWidenIntegerType(NativeSwiftConversionStep, valueType: JavaType, combinedSwiftType: String, valueSizeInBytes: Int) + indirect case optionalRaisingWidenIntegerType(NativeSwiftConversionStep, valueType: JavaType, combinedSwiftType: SwiftKnownTypeDeclKind, valueSizeInBytes: Int) indirect case optionalRaisingIndirectReturn(NativeSwiftConversionStep, returnType: JavaType, discriminatorParameterName: String, placeholderValue: NativeSwiftConversionStep) @@ -571,9 +575,9 @@ extension JNISwift2JavaGenerator { case .initializeJavaKitWrapper(let wrapperName): return "\(wrapperName)(javaThis: \(placeholder), environment: environment!)" - case .optionalLowering(let valueConversion): - let value = valueConversion.render(&printer, "\(placeholder)_value") - return "\(placeholder)_descriptor == 1 ? \(value) : nil" + case .optionalLowering(let valueConversion, let discriminatorName, let valueName): + let value = valueConversion.render(&printer, valueName) + return "\(discriminatorName) == 1 ? \(value) : nil" case .optionalChain(let inner): let inner = inner.render(&printer, placeholder) @@ -582,10 +586,11 @@ extension JNISwift2JavaGenerator { case .optionalRaisingWidenIntegerType(let inner, let valueType, let combinedSwiftType, let valueSizeInBytes): let inner = inner.render(&printer, placeholder) let value = valueType == .boolean ? "$0 ? 1 : 0" : "$0" + let combinedSwiftTypeName = combinedSwiftType.moduleAndName.name printer.print( """ - let value$: \(combinedSwiftType) = \(inner).map { - \(combinedSwiftType)(\(value)) << \(valueSizeInBytes * 8) | \(combinedSwiftType)(1) + let value$ = \(inner).map { + \(combinedSwiftTypeName)(\(value)) << \(valueSizeInBytes * 8) | \(combinedSwiftTypeName)(1) } ?? 0 """ ) From ed42947a4954029539b1b7eb7273af06a8c3fb0b Mon Sep 17 00:00:00 2001 From: Mads Odgaard Date: Thu, 31 Jul 2025 22:24:46 +0200 Subject: [PATCH 08/11] add support for optional JavaKit wrapped parameters --- .../Sources/MySwiftLibrary/Optionals.swift | 10 ++ .../java/com/example/swift/OptionalsTest.java | 6 ++ .../Convenience/JavaType+Extensions.swift | 25 +++++ ...ISwift2JavaGenerator+JavaTranslation.swift | 19 +++- ...wift2JavaGenerator+NativeTranslation.swift | 57 +++++++++-- ...ift2JavaGenerator+SwiftThunkPrinting.swift | 2 +- Sources/JExtractSwiftLib/JNI/JNIType.swift | 98 ------------------- 7 files changed, 109 insertions(+), 108 deletions(-) delete mode 100644 Sources/JExtractSwiftLib/JNI/JNIType.swift diff --git a/Samples/JExtractJNISampleApp/Sources/MySwiftLibrary/Optionals.swift b/Samples/JExtractJNISampleApp/Sources/MySwiftLibrary/Optionals.swift index 0a8300b6d..673ecb0b2 100644 --- a/Samples/JExtractJNISampleApp/Sources/MySwiftLibrary/Optionals.swift +++ b/Samples/JExtractJNISampleApp/Sources/MySwiftLibrary/Optionals.swift @@ -12,6 +12,8 @@ // //===----------------------------------------------------------------------===// +import JavaKit + public func optionalBool(input: Optional) -> Bool? { return input } @@ -52,6 +54,14 @@ public func optionalClass(input: Optional) -> MySwiftClass? { return input } +public func optionalJavaKitLong(input: Optional) -> Int64? { + if let input { + return input.longValue() + } else { + return nil + } +} + public func multipleOptionals( input1: Optional, input2: Optional, diff --git a/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/OptionalsTest.java b/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/OptionalsTest.java index ec9ff3b0f..f7262ad4d 100644 --- a/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/OptionalsTest.java +++ b/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/OptionalsTest.java @@ -91,6 +91,12 @@ void optionalClass() { } } + @Test + void optionalJavaKitLong() { + assertEquals(OptionalLong.empty(), MySwiftLibrary.optionalJavaKitLong(Optional.empty())); + assertEquals(OptionalLong.of(99L), MySwiftLibrary.optionalJavaKitLong(Optional.of(99L))); + } + @Test void multipleOptionals() { try (var arena = new ConfinedSwiftMemorySession()) { diff --git a/Sources/JExtractSwiftLib/Convenience/JavaType+Extensions.swift b/Sources/JExtractSwiftLib/Convenience/JavaType+Extensions.swift index c46854ade..cb849e795 100644 --- a/Sources/JExtractSwiftLib/Convenience/JavaType+Extensions.swift +++ b/Sources/JExtractSwiftLib/Convenience/JavaType+Extensions.swift @@ -86,4 +86,29 @@ extension JavaType { case .void: nil } } + + var jniCallMethodAName: String { + switch self { + case .boolean: "CallBooleanMethodA" + case .byte: "CallByteMethodA" + case .char: "CallCharMethodA" + case .short: "CallShortMethodA" + case .int: "CallIntMethodA" + case .long: "CallLongMethodA" + case .float: "CallFloatMethodA" + case .double: "CallDoubleMethodA" + case .void: "CallVoidMethodA" + default: "CallObjectMethodA" + } + } + + /// Returns whether this type returns `JavaValue` from JavaKit + var implementsJavaValue: Bool { + return switch self { + case .boolean, .byte, .char, .short, .int, .long, .float, .double, .void, .javaLangString: + true + default: + false + } + } } diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift index ee5b63c0b..0db77ece9 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift @@ -306,8 +306,23 @@ extension JNISwift2JavaGenerator { ) } - guard !nominalType.isJavaKitWrapper else { - throw JavaTranslationError.unsupportedSwiftType(swiftType) + if nominalType.isJavaKitWrapper { + guard let javaType = nominalTypeName.parseJavaClassFromJavaKitName(in: self.javaClassLookupTable) else { + throw JavaTranslationError.wrappedJavaClassTranslationNotProvided(swiftType) + } + + return TranslatedParameter( + parameter: JavaParameter( + name: parameterName, + type: .class(package: nil, name: "Optional<\(javaType)>"), + annotations: parameterAnnotations + ), + conversion: .method( + .placeholder, + function: "orElse", + arguments: [.constant("null")] + ) + ) } // Assume JExtract imported class diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift index 4cbae8170..6251aaebb 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift @@ -107,7 +107,14 @@ extension JNISwift2JavaGenerator { parameters: [ JavaParameter(name: parameterName, type: javaType) ], - conversion: .initializeJavaKitWrapper(wrapperName: nominalTypeName) + conversion: .initializeJavaKitWrapper( + .unwrapOptional( + .placeholder, + name: parameterName, + fatalErrorMessage: "\(parameterName) was null in call to \\(#function), but Swift requires non-optional!" + ), + wrapperName: nominalTypeName + ) ) } @@ -170,6 +177,8 @@ extension JNISwift2JavaGenerator { switch swiftType { case .nominal(let nominalType): + let nominalTypeName = nominalType.nominalTypeDecl.name + if let knownType = nominalType.nominalTypeDecl.knownTypeKind { guard let javaType = JNIJavaTypeTranslator.translate(knownType: knownType, config: self.config), javaType.implementsJavaValue else { @@ -189,8 +198,17 @@ extension JNISwift2JavaGenerator { ) } - guard !nominalType.isJavaKitWrapper else { - throw JavaTranslationError.unsupportedSwiftType(swiftType) + if nominalType.isJavaKitWrapper { + guard let javaType = nominalTypeName.parseJavaClassFromJavaKitName(in: self.javaClassLookupTable) else { + throw JavaTranslationError.wrappedJavaClassTranslationNotProvided(swiftType) + } + + return NativeParameter( + parameters: [ + JavaParameter(name: parameterName, type: javaType) + ], + conversion: .optionalMap(.initializeJavaKitWrapper(.placeholder, wrapperName: nominalTypeName)) + ) } // Assume JExtract wrapped class @@ -456,7 +474,7 @@ extension JNISwift2JavaGenerator { indirect case closureLowering(parameters: [NativeParameter], result: NativeResult) - case initializeJavaKitWrapper(wrapperName: String) + indirect case initializeJavaKitWrapper(NativeSwiftConversionStep, wrapperName: String) indirect case optionalLowering(NativeSwiftConversionStep, discriminatorName: String, valueName: String) @@ -470,6 +488,10 @@ extension JNISwift2JavaGenerator { indirect case member(NativeSwiftConversionStep, member: String) + indirect case optionalMap(NativeSwiftConversionStep) + + indirect case unwrapOptional(NativeSwiftConversionStep, name: String, fatalErrorMessage: String) + /// Returns the conversion string applied to the placeholder. func render(_ printer: inout CodePrinter, _ placeholder: String) -> String { // NOTE: 'printer' is used if the conversion wants to cause side-effects. @@ -558,7 +580,7 @@ extension JNISwift2JavaGenerator { """ ) - let upcall = "environment!.interface.\(nativeResult.javaType.jniType.callMethodAName)(environment, \(placeholder), methodID$, arguments$)" + let upcall = "environment!.interface.\(nativeResult.javaType.jniCallMethodAName)(environment, \(placeholder), methodID$, arguments$)" let result = nativeResult.conversion.render(&printer, upcall) if nativeResult.javaType.isVoid { @@ -572,8 +594,9 @@ extension JNISwift2JavaGenerator { return printer.finalize() - case .initializeJavaKitWrapper(let wrapperName): - return "\(wrapperName)(javaThis: \(placeholder), environment: environment!)" + case .initializeJavaKitWrapper(let inner, let wrapperName): + let inner = inner.render(&printer, placeholder) + return "\(wrapperName)(javaThis: \(inner), environment: environment!)" case .optionalLowering(let valueConversion, let discriminatorName, let valueName): let value = valueConversion.render(&printer, valueName) @@ -637,6 +660,26 @@ extension JNISwift2JavaGenerator { case .member(let inner, let member): let inner = inner.render(&printer, placeholder) return "\(inner).\(member)" + + case .optionalMap(let inner): + var printer = CodePrinter() + printer.printBraceBlock("\(placeholder).map") { printer in + let inner = inner.render(&printer, "$0") + printer.print("return \(inner)") + } + return printer.finalize() + + case .unwrapOptional(let inner, let name, let fatalErrorMessage): + let unwrappedName = "\(name)_unwrapped$" + let inner = inner.render(&printer, placeholder) + printer.print( + """ + guard let \(unwrappedName) = \(inner) else { + fatalError("\(fatalErrorMessage)") + } + """ + ) + return unwrappedName } } } diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift index 6554847d3..ca580e602 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift @@ -249,7 +249,7 @@ extension JNISwift2JavaGenerator { + jniSignature.escapedJNIIdentifier let translatedParameters = parameters.map { - "\($0.name): \($0.type.jniType)" + "\($0.name): \($0.type.jniTypeName)" } let thunkParameters = diff --git a/Sources/JExtractSwiftLib/JNI/JNIType.swift b/Sources/JExtractSwiftLib/JNI/JNIType.swift deleted file mode 100644 index cdedb0a17..000000000 --- a/Sources/JExtractSwiftLib/JNI/JNIType.swift +++ /dev/null @@ -1,98 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// 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 -// -//===----------------------------------------------------------------------===// - -import JavaTypes - -/// Represents types that are able to be passed over a JNI boundary. -/// -/// - SeeAlso: https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/types.html -enum JNIType { - case jboolean - case jfloat - case jdouble - case jbyte - case jchar - case jshort - case jint - case jlong - case void - case jstring - case jclass - case jthrowable - case jobject - case jbooleanArray - case jbyteArray - case jcharArray - case jshortArray - case jintArray - case jlongArray - case jfloatArray - case jdoubleArray - case jobjectArray - - var callMethodAName: String { - switch self { - case .jboolean: "CallBooleanMethodA" - case .jbyte: "CallByteMethodA" - case .jchar: "CallCharMethodA" - case .jshort: "CallShortMethodA" - case .jint: "CallIntMethodA" - case .jlong: "CallLongMethodA" - case .jfloat: "CallFloatMethodA" - case .jdouble: "CallDoubleMethodA" - case .void: "CallVoidMethodA" - case .jobject, .jstring, .jclass, .jthrowable: "CallObjectMethodA" - case .jbooleanArray, .jbyteArray, .jcharArray, .jshortArray, .jintArray, .jlongArray, .jfloatArray, .jdoubleArray, .jobjectArray: "CallObjectMethodA" - } - } -} - -extension JavaType { - var jniType: JNIType { - switch self { - case .boolean: .jboolean - case .byte: .jbyte - case .char: .jchar - case .short: .jshort - case .int: .jint - case .long: .jlong - case .float: .jfloat - case .double: .jdouble - case .void: .void - case .array(.boolean): .jbooleanArray - case .array(.byte): .jbyteArray - case .array(.char): .jcharArray - case .array(.short): .jshortArray - case .array(.int): .jintArray - case .array(.long): .jlongArray - case .array(.float): .jfloatArray - case .array(.double): .jdoubleArray - case .array: .jobjectArray - case .javaLangString: .jstring - case .javaLangClass: .jclass - case .javaLangThrowable: .jthrowable - case .class: .jobject - } - } - - /// Returns whether this type returns `JavaValue` from JavaKit - var implementsJavaValue: Bool { - return switch self { - case .boolean, .byte, .char, .short, .int, .long, .float, .double, .void, .javaLangString: - true - default: - false - } - } -} From 760476c765202be6e71cc685daace53747e319fa Mon Sep 17 00:00:00 2001 From: Mads Odgaard Date: Fri, 1 Aug 2025 08:10:44 +0200 Subject: [PATCH 09/11] update supported features --- .../Documentation.docc/SupportedFeatures.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md b/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md index b3d02276f..130c333bc 100644 --- a/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md +++ b/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md @@ -63,7 +63,8 @@ SwiftJava's `swift-java jextract` tool automates generating Java bindings from S | `Foundation.Data`, `any Foundation.DataProtocol` | ✅ | ❌ | | Tuples: `(Int, String)`, `(A, B, C)` | ❌ | ❌ | | Protocols: `protocol`, existential parameters `any Collection` | ❌ | ❌ | -| Optional types: `Int?`, `AnyObject?` | ❌ | ❌ | +| Optional parameters: `func f(i: Int?, class: MyClass?)` | ✅ | ✅ | +| Optional return types: `func f() -> Int?`, `func g() -> MyClass?` | ❌ | ✅ | | Primitive types: `Bool`, `Int`, `Int8`, `Int16`, `Int32`, `Int64`, `Float`, `Double` | ✅ | ✅ | | Parameters: JavaKit wrapped types `JavaLong`, `JavaInteger` | ❌ | ✅ | | Return values: JavaKit wrapped types `JavaLong`, `JavaInteger` | ❌ | ❌ | From e08732772ef6863a397114aa726f0e4adf2feaa8 Mon Sep 17 00:00:00 2001 From: Mads Odgaard Date: Fri, 1 Aug 2025 16:26:35 +0200 Subject: [PATCH 10/11] fix tests --- .../JNI/JNIClassTests.swift | 12 ++++--- .../JNI/JNIClosureTests.swift | 4 +-- .../JNI/JNIJavaKitTests.swift | 10 ++++-- .../JNI/JNIModuleTests.swift | 3 +- .../JNI/JNIStructTests.swift | 3 +- .../JNI/JNIVariablesTests.swift | 33 ++++++++++++------- 6 files changed, 44 insertions(+), 21 deletions(-) diff --git a/Tests/JExtractSwiftTests/JNI/JNIClassTests.swift b/Tests/JExtractSwiftTests/JNI/JNIClassTests.swift index 483d53f5a..5b015f894 100644 --- a/Tests/JExtractSwiftTests/JNI/JNIClassTests.swift +++ b/Tests/JExtractSwiftTests/JNI/JNIClassTests.swift @@ -283,7 +283,8 @@ struct JNIClassTests { func Java_com_example_swift_MyClass__00024doSomething__JJ(environment: UnsafeMutablePointer!, thisClass: jclass, x: jlong, self: jlong) { assert(self != 0, "self memory address was null") let selfBits$ = Int(Int64(fromJNI: self, in: environment!)) - guard let self$ = UnsafeMutablePointer(bitPattern: selfBits$) else { + let self$ = UnsafeMutablePointer(bitPattern: selfBits$) + guard let self$ else { fatalError("self memory address was null in call to \\(#function)!") } self$.pointee.doSomething(x: Int64(fromJNI: x, in: environment!)) @@ -331,7 +332,8 @@ struct JNIClassTests { func Java_com_example_swift_MyClass__00024copy__J(environment: UnsafeMutablePointer!, thisClass: jclass, self: jlong) -> jlong { assert(self != 0, "self memory address was null") let selfBits$ = Int(Int64(fromJNI: self, in: environment!)) - guard let self$ = UnsafeMutablePointer(bitPattern: selfBits$) else { + let self$ = UnsafeMutablePointer(bitPattern: selfBits$) + guard let self$ else { fatalError("self memory address was null in call to \\(#function)!") } let result$ = UnsafeMutablePointer.allocate(capacity: 1) @@ -382,12 +384,14 @@ struct JNIClassTests { func Java_com_example_swift_MyClass__00024isEqual__JJ(environment: UnsafeMutablePointer!, thisClass: jclass, other: jlong, self: jlong) -> jboolean { assert(other != 0, "other memory address was null") let otherBits$ = Int(Int64(fromJNI: other, in: environment!)) - guard let other$ = UnsafeMutablePointer(bitPattern: otherBits$) else { + let other$ = UnsafeMutablePointer(bitPattern: otherBits$) + guard let other$ else { fatalError("other memory address was null in call to \\(#function)!") } assert(self != 0, "self memory address was null") let selfBits$ = Int(Int64(fromJNI: self, in: environment!)) - guard let self$ = UnsafeMutablePointer(bitPattern: selfBits$) else { + let self$ = UnsafeMutablePointer(bitPattern: selfBits$) + guard let self$ else { fatalError("self memory address was null in call to \\(#function)!") } return self$.pointee.isEqual(to: other$.pointee).getJNIValue(in: environment!) diff --git a/Tests/JExtractSwiftTests/JNI/JNIClosureTests.swift b/Tests/JExtractSwiftTests/JNI/JNIClosureTests.swift index 47d7e35da..9a388da18 100644 --- a/Tests/JExtractSwiftTests/JNI/JNIClosureTests.swift +++ b/Tests/JExtractSwiftTests/JNI/JNIClosureTests.swift @@ -61,7 +61,7 @@ struct JNIClosureTests { expectedChunks: [ """ @_cdecl("Java_com_example_swift_SwiftModule__00024emptyClosure__Lcom_example_swift_SwiftModule_00024emptyClosure_00024closure_2") - func Java_com_example_swift_SwiftModule__00024emptyClosure__Lcom_example_swift_SwiftModule_00024emptyClosure_00024closure_2(environment: UnsafeMutablePointer!, thisClass: jclass, closure: jobject) { + func Java_com_example_swift_SwiftModule__00024emptyClosure__Lcom_example_swift_SwiftModule_00024emptyClosure_00024closure_2(environment: UnsafeMutablePointer!, thisClass: jclass, closure: jobject?) { SwiftModule.emptyClosure(closure: { let class$ = environment!.interface.GetObjectClass(environment, closure) let methodID$ = environment!.interface.GetMethodID(environment, class$, "apply", "()V")! @@ -113,7 +113,7 @@ struct JNIClosureTests { expectedChunks: [ """ @_cdecl("Java_com_example_swift_SwiftModule__00024closureWithArgumentsAndReturn__Lcom_example_swift_SwiftModule_00024closureWithArgumentsAndReturn_00024closure_2") - func Java_com_example_swift_SwiftModule__00024closureWithArgumentsAndReturn__Lcom_example_swift_SwiftModule_00024closureWithArgumentsAndReturn_00024closure_2(environment: UnsafeMutablePointer!, thisClass: jclass, closure: jobject) { + func Java_com_example_swift_SwiftModule__00024closureWithArgumentsAndReturn__Lcom_example_swift_SwiftModule_00024closureWithArgumentsAndReturn_00024closure_2(environment: UnsafeMutablePointer!, thisClass: jclass, closure: jobject?) { SwiftModule.closureWithArgumentsAndReturn(closure: { _0, _1 in let class$ = environment!.interface.GetObjectClass(environment, closure) let methodID$ = environment!.interface.GetMethodID(environment, class$, "apply", "(JZ)J")! diff --git a/Tests/JExtractSwiftTests/JNI/JNIJavaKitTests.swift b/Tests/JExtractSwiftTests/JNI/JNIJavaKitTests.swift index 0283780af..1f19c8f99 100644 --- a/Tests/JExtractSwiftTests/JNI/JNIJavaKitTests.swift +++ b/Tests/JExtractSwiftTests/JNI/JNIJavaKitTests.swift @@ -64,8 +64,14 @@ struct JNIJavaKitTests { expectedChunks: [ """ @_cdecl("Java_com_example_swift_SwiftModule__00024function__Ljava_lang_Long_2Ljava_lang_Integer_2J") - func Java_com_example_swift_SwiftModule__00024function__Ljava_lang_Long_2Ljava_lang_Integer_2J(environment: UnsafeMutablePointer!, thisClass: jclass, javaLong: jobject, javaInteger: jobject, int: jlong) { - SwiftModule.function(javaLong: JavaLong(javaThis: javaLong, environment: environment!), javaInteger: JavaInteger(javaThis: javaInteger, environment: environment!), int: Int64(fromJNI: int, in: environment!)) + func Java_com_example_swift_SwiftModule__00024function__Ljava_lang_Long_2Ljava_lang_Integer_2J(environment: UnsafeMutablePointer!, thisClass: jclass, javaLong: jobject?, javaInteger: jobject?, int: jlong) { + guard let javaLong_unwrapped$ = javaLong else { + fatalError("javaLong was null in call to \\(#function), but Swift requires non-optional!") + } + guard let javaInteger_unwrapped$ = javaInteger else { + fatalError("javaInteger was null in call to \\(#function), but Swift requires non-optional!") + } + SwiftModule.function(javaLong: JavaLong(javaThis: javaLong_unwrapped$, environment: environment!), javaInteger: JavaInteger(javaThis: javaInteger_unwrapped$, environment: environment!), int: Int64(fromJNI: int, in: environment!)) } """ ] diff --git a/Tests/JExtractSwiftTests/JNI/JNIModuleTests.swift b/Tests/JExtractSwiftTests/JNI/JNIModuleTests.swift index be9cf0ce6..198276ba6 100644 --- a/Tests/JExtractSwiftTests/JNI/JNIModuleTests.swift +++ b/Tests/JExtractSwiftTests/JNI/JNIModuleTests.swift @@ -45,6 +45,7 @@ struct JNIModuleTests { import org.swift.swiftkit.core.*; import org.swift.swiftkit.core.util.*; + import java.util.*; import org.swift.swiftkit.core.annotations.*; public final class SwiftModule { @@ -176,7 +177,7 @@ struct JNIModuleTests { expectedChunks: [ """ @_cdecl("Java_com_example_swift_SwiftModule__00024copy__Ljava_lang_String_2") - func Java_com_example_swift_SwiftModule__00024copy__Ljava_lang_String_2(environment: UnsafeMutablePointer!, thisClass: jclass, string: jstring) -> jstring { + func Java_com_example_swift_SwiftModule__00024copy__Ljava_lang_String_2(environment: UnsafeMutablePointer!, thisClass: jclass, string: jstring?) -> jstring? { return SwiftModule.copy(String(fromJNI: string, in: environment!)).getJNIValue(in: environment!) } """, diff --git a/Tests/JExtractSwiftTests/JNI/JNIStructTests.swift b/Tests/JExtractSwiftTests/JNI/JNIStructTests.swift index 09a8626d3..01a2e3c0f 100644 --- a/Tests/JExtractSwiftTests/JNI/JNIStructTests.swift +++ b/Tests/JExtractSwiftTests/JNI/JNIStructTests.swift @@ -205,7 +205,8 @@ struct JNIStructTests { func Java_com_example_swift_MyStruct__00024doSomething__JJ(environment: UnsafeMutablePointer!, thisClass: jclass, x: jlong, self: jlong) { assert(self != 0, "self memory address was null") let selfBits$ = Int(Int64(fromJNI: self, in: environment!)) - guard let self$ = UnsafeMutablePointer(bitPattern: selfBits$) else { + let self$ = UnsafeMutablePointer(bitPattern: selfBits$) + guard let self$ else { fatalError("self memory address was null in call to \\(#function)!") } self$.pointee.doSomething(x: Int64(fromJNI: x, in: environment!)) diff --git a/Tests/JExtractSwiftTests/JNI/JNIVariablesTests.swift b/Tests/JExtractSwiftTests/JNI/JNIVariablesTests.swift index 9d2fcb227..933e4f088 100644 --- a/Tests/JExtractSwiftTests/JNI/JNIVariablesTests.swift +++ b/Tests/JExtractSwiftTests/JNI/JNIVariablesTests.swift @@ -71,7 +71,8 @@ struct JNIVariablesTests { func Java_com_example_swift_MyClass__00024getConstant__J(environment: UnsafeMutablePointer!, thisClass: jclass, self: jlong) -> jlong { assert(self != 0, "self memory address was null") let selfBits$ = Int(Int64(fromJNI: self, in: environment!)) - guard let self$ = UnsafeMutablePointer(bitPattern: selfBits$) else { + let self$ = UnsafeMutablePointer(bitPattern: selfBits$) + guard let self$ else { fatalError("self memory address was null in call to \\(#function)!") } return self$.pointee.constant.getJNIValue(in: environment!) @@ -134,7 +135,8 @@ struct JNIVariablesTests { func Java_com_example_swift_MyClass__00024getMutable__J(environment: UnsafeMutablePointer!, thisClass: jclass, self: jlong) -> jlong { assert(self != 0, "self memory address was null") let selfBits$ = Int(Int64(fromJNI: self, in: environment!)) - guard let self$ = UnsafeMutablePointer(bitPattern: selfBits$) else { + let self$ = UnsafeMutablePointer(bitPattern: selfBits$) + guard let self$ else { fatalError("self memory address was null in call to \\(#function)!") } return self$.pointee.mutable.getJNIValue(in: environment!) @@ -145,7 +147,8 @@ struct JNIVariablesTests { func Java_com_example_swift_MyClass__00024setMutable__JJ(environment: UnsafeMutablePointer!, thisClass: jclass, newValue: jlong, self: jlong) { assert(self != 0, "self memory address was null") let selfBits$ = Int(Int64(fromJNI: self, in: environment!)) - guard let self$ = UnsafeMutablePointer(bitPattern: selfBits$) else { + let self$ = UnsafeMutablePointer(bitPattern: selfBits$) + guard let self$ else { fatalError("self memory address was null in call to \\(#function)!") } self$.pointee.mutable = Int64(fromJNI: newValue, in: environment!) @@ -194,7 +197,8 @@ struct JNIVariablesTests { func Java_com_example_swift_MyClass__00024getComputed__J(environment: UnsafeMutablePointer!, thisClass: jclass, self: jlong) -> jlong { assert(self != 0, "self memory address was null") let selfBits$ = Int(Int64(fromJNI: self, in: environment!)) - guard let self$ = UnsafeMutablePointer(bitPattern: selfBits$) else { + let self$ = UnsafeMutablePointer(bitPattern: selfBits$) + guard let self$ else { fatalError("self memory address was null in call to \\(#function)!") } return self$.pointee.computed.getJNIValue(in: environment!) @@ -243,7 +247,8 @@ struct JNIVariablesTests { func Java_com_example_swift_MyClass__00024getComputedThrowing__J(environment: UnsafeMutablePointer!, thisClass: jclass, self: jlong) -> jlong { assert(self != 0, "self memory address was null") let selfBits$ = Int(Int64(fromJNI: self, in: environment!)) - guard let self$ = UnsafeMutablePointer(bitPattern: selfBits$) else { + let self$ = UnsafeMutablePointer(bitPattern: selfBits$) + guard let self$ else { fatalError("self memory address was null in call to \\(#function)!") } do { @@ -311,7 +316,8 @@ struct JNIVariablesTests { func Java_com_example_swift_MyClass__00024getGetterAndSetter__J(environment: UnsafeMutablePointer!, thisClass: jclass, self: jlong) -> jlong { assert(self != 0, "self memory address was null") let selfBits$ = Int(Int64(fromJNI: self, in: environment!)) - guard let self$ = UnsafeMutablePointer(bitPattern: selfBits$) else { + let self$ = UnsafeMutablePointer(bitPattern: selfBits$) + guard let self$ else { fatalError("self memory address was null in call to \\(#function)!") } return self$.pointee.getterAndSetter.getJNIValue(in: environment!) @@ -322,7 +328,8 @@ struct JNIVariablesTests { func Java_com_example_swift_MyClass__00024setGetterAndSetter__JJ(environment: UnsafeMutablePointer!, thisClass: jclass, newValue: jlong, self: jlong) { assert(self != 0, "self memory address was null") let selfBits$ = Int(Int64(fromJNI: self, in: environment!)) - guard let self$ = UnsafeMutablePointer(bitPattern: selfBits$) else { + let self$ = UnsafeMutablePointer(bitPattern: selfBits$) + guard let self$ else { fatalError("self memory address was null in call to \\(#function)!") } self$.pointee.getterAndSetter = Int64(fromJNI: newValue, in: environment!) @@ -385,7 +392,8 @@ struct JNIVariablesTests { func Java_com_example_swift_MyClass__00024isSomeBoolean__J(environment: UnsafeMutablePointer!, thisClass: jclass, self: jlong) -> jboolean { assert(self != 0, "self memory address was null") let selfBits$ = Int(Int64(fromJNI: self, in: environment!)) - guard let self$ = UnsafeMutablePointer(bitPattern: selfBits$) else { + let self$ = UnsafeMutablePointer(bitPattern: selfBits$) + guard let self$ else { fatalError("self memory address was null in call to \\(#function)!") } return self$.pointee.someBoolean.getJNIValue(in: environment!) @@ -396,7 +404,8 @@ struct JNIVariablesTests { func Java_com_example_swift_MyClass__00024setSomeBoolean__ZJ(environment: UnsafeMutablePointer!, thisClass: jclass, newValue: jboolean, self: jlong) { assert(self != 0, "self memory address was null") let selfBits$ = Int(Int64(fromJNI: self, in: environment!)) - guard let self$ = UnsafeMutablePointer(bitPattern: selfBits$) else { + let self$ = UnsafeMutablePointer(bitPattern: selfBits$) + guard let self$ else { fatalError("self memory address was null in call to \\(#function)!") } self$.pointee.someBoolean = Bool(fromJNI: newValue, in: environment!) @@ -459,7 +468,8 @@ struct JNIVariablesTests { func Java_com_example_swift_MyClass__00024isBoolean__J(environment: UnsafeMutablePointer!, thisClass: jclass, self: jlong) -> jboolean { assert(self != 0, "self memory address was null") let selfBits$ = Int(Int64(fromJNI: self, in: environment!)) - guard let self$ = UnsafeMutablePointer(bitPattern: selfBits$) else { + let self$ = UnsafeMutablePointer(bitPattern: selfBits$) + guard let self$ else { fatalError("self memory address was null in call to \\(#function)!") } return self$.pointee.isBoolean.getJNIValue(in: environment!) @@ -470,7 +480,8 @@ struct JNIVariablesTests { func Java_com_example_swift_MyClass__00024setBoolean__ZJ(environment: UnsafeMutablePointer!, thisClass: jclass, newValue: jboolean, self: jlong) { assert(self != 0, "self memory address was null") let selfBits$ = Int(Int64(fromJNI: self, in: environment!)) - guard let self$ = UnsafeMutablePointer(bitPattern: selfBits$) else { + let self$ = UnsafeMutablePointer(bitPattern: selfBits$) + guard let self$ else { fatalError("self memory address was null in call to \\(#function)!") } self$.pointee.isBoolean = Bool(fromJNI: newValue, in: environment!) From df418b0eae587639867f30999a17323995961a9b Mon Sep 17 00:00:00 2001 From: Mads Odgaard Date: Fri, 1 Aug 2025 16:46:08 +0200 Subject: [PATCH 11/11] add codegen tests --- ...wift2JavaGenerator+NativeTranslation.swift | 2 +- .../JNI/JNIOptionalTests.swift | 253 ++++++++++++++++++ 2 files changed, 254 insertions(+), 1 deletion(-) create mode 100644 Tests/JExtractSwiftTests/JNI/JNIOptionalTests.swift diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift index 6251aaebb..e7f7efbeb 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift @@ -378,7 +378,7 @@ extension JNISwift2JavaGenerator { guard let genericArgs = nominalType.genericArguments, genericArgs.count == 1 else { throw JavaTranslationError.unsupportedSwiftType(swiftResult.type) } - return try translateOptionalResult(wrappedType: swiftResult.type) + return try translateOptionalResult(wrappedType: genericArgs[0]) default: guard let javaType = JNIJavaTypeTranslator.translate(knownType: knownType, config: self.config), javaType.implementsJavaValue else { diff --git a/Tests/JExtractSwiftTests/JNI/JNIOptionalTests.swift b/Tests/JExtractSwiftTests/JNI/JNIOptionalTests.swift new file mode 100644 index 000000000..cd04660b0 --- /dev/null +++ b/Tests/JExtractSwiftTests/JNI/JNIOptionalTests.swift @@ -0,0 +1,253 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +import JExtractSwiftLib +import Testing + +@Suite +struct JNIOptionalTests { + let source = + """ + class MyClass { } + + public func optionalSugar(_ arg: Int64?) -> Int32? + public func optionalExplicit(_ arg: Optional) -> Optional + public func optionalClass(_ arg: MyClass?) -> MyClass? + public func optionalJavaKitClass(_ arg: JavaLong?) + """ + + let classLookupTable = [ + "JavaLong": "java.lang.Long", + ] + + @Test + func optionalSugar_javaBindings() throws { + try assertOutput( + input: source, + .jni, + .java, + javaClassLookupTable: classLookupTable, + expectedChunks: [ + """ + /** + * Downcall to Swift: + * {@snippet lang=swift : + * public func optionalSugar(_ arg: Int64?) -> Int32? + * } + */ + public static OptionalInt optionalSugar(OptionalLong arg) { + long combined$ = SwiftModule.$optionalSugar((byte) (arg.isPresent() ? 1 : 0), arg.orElse(0L)); + byte discriminator$ = (byte) (combined$ & 0xFF); + int value$ = (int) (combined$ >> 32); + return discriminator$ == 1 ? OptionalInt.of(value$) : OptionalInt.empty(); + } + """, + """ + private static native long $optionalSugar(byte arg_discriminator, long arg_value); + """ + ] + ) + } + + @Test + func optionalSugar_swiftThunks() throws { + try assertOutput( + input: source, + .jni, + .swift, + detectChunkByInitialLines: 1, + javaClassLookupTable: classLookupTable, + expectedChunks: [ + """ + @_cdecl("Java_com_example_swift_SwiftModule__00024optionalSugar__BJ") + func Java_com_example_swift_SwiftModule__00024optionalSugar__BJ(environment: UnsafeMutablePointer!, thisClass: jclass, arg_discriminator: jbyte, arg_value: jlong) -> jlong { + let value$ = SwiftModule.optionalSugar(arg_discriminator == 1 ? Int64(fromJNI: arg_value, in: environment!) : nil).map { + Int64($0) << 32 | Int64(1) + } ?? 0 + return value$.getJNIValue(in: environment!) + } + """ + ] + ) + } + + @Test + func optionalExplicit_javaBindings() throws { + try assertOutput( + input: source, + .jni, + .java, + javaClassLookupTable: classLookupTable, + expectedChunks: [ + """ + /** + * Downcall to Swift: + * {@snippet lang=swift : + * public func optionalExplicit(_ arg: Optional) -> Optional + * } + */ + public static Optional optionalExplicit(Optional arg) { + byte[] result_discriminator$ = new byte[1]; + java.lang.String result$ = SwiftModule.$optionalExplicit((byte) (arg.isPresent() ? 1 : 0), arg.orElse(null), result_discriminator$); + return (result_discriminator$[0] == 1) ? Optional.of(result$) : Optional.empty(); + } + """, + """ + private static native java.lang.String $optionalExplicit(byte arg_discriminator, java.lang.String arg_value, byte[] result_discriminator$); + """ + ] + ) + } + + @Test + func optionalExplicit_swiftThunks() throws { + try assertOutput( + input: source, + .jni, + .swift, + detectChunkByInitialLines: 1, + javaClassLookupTable: classLookupTable, + expectedChunks: [ + """ + @_cdecl("Java_com_example_swift_SwiftModule__00024optionalExplicit__BLjava_lang_String_2_3B") + func Java_com_example_swift_SwiftModule__00024optionalExplicit__BLjava_lang_String_2_3B(environment: UnsafeMutablePointer!, thisClass: jclass, arg_discriminator: jbyte, arg_value: jstring?, result_discriminator$: jbyteArray?) -> jstring? { + let result$: jstring? + if let innerResult$ = SwiftModule.optionalExplicit(arg_discriminator == 1 ? String(fromJNI: arg_value, in: environment!) : nil) { + result$ = innerResult$.getJNIValue(in: environment!) + var flag$ = Int8(1) + environment.interface.SetByteArrayRegion(environment, result_discriminator$, 0, 1, &flag$) + } // render(_:_:) @ JExtractSwiftLib/JNISwift2JavaGenerator+NativeTranslation.swift:624 + else { + result$ = String.jniPlaceholderValue + var flag$ = Int8(0) + environment.interface.SetByteArrayRegion(environment, result_discriminator$, 0, 1, &flag$) + } // render(_:_:) @ JExtractSwiftLib/JNISwift2JavaGenerator+NativeTranslation.swift:634 + return result$ + } + """ + ] + ) + } + + @Test + func optionalClass_javaBindings() throws { + try assertOutput( + input: source, + .jni, + .java, + javaClassLookupTable: classLookupTable, + expectedChunks: [ + """ + /** + * Downcall to Swift: + * {@snippet lang=swift : + * public func optionalClass(_ arg: MyClass?) -> MyClass? + * } + */ + public static Optional optionalClass(Optional arg, SwiftArena swiftArena$) { + byte[] result_discriminator$ = new byte[1]; + long result$ = SwiftModule.$optionalClass(arg.map(MyClass::$memoryAddress).orElse(0L), result_discriminator$); + return (result_discriminator$[0] == 1) ? Optional.of(new MyClass(result$, swiftArena$)) : Optional.empty(); + } + """, + """ + private static native long $optionalClass(long arg, byte[] result_discriminator$); + """ + ] + ) + } + + @Test + func optionalClass_swiftThunks() throws { + try assertOutput( + input: source, + .jni, + .swift, + detectChunkByInitialLines: 1, + javaClassLookupTable: classLookupTable, + expectedChunks: [ + """ + @_cdecl("Java_com_example_swift_SwiftModule__00024optionalClass__J_3B") + func Java_com_example_swift_SwiftModule__00024optionalClass__J_3B(environment: UnsafeMutablePointer!, thisClass: jclass, arg: jlong, result_discriminator$: jbyteArray?) -> jlong { + let argBits$ = Int(Int64(fromJNI: arg, in: environment!)) + let arg$ = UnsafeMutablePointer(bitPattern: argBits$) + let result$: jlong + if let innerResult$ = SwiftModule.optionalClass(arg$?.pointee) { + let _result$ = UnsafeMutablePointer.allocate(capacity: 1) + _result$.initialize(to: innerResult$) + let _resultBits$ = Int64(Int(bitPattern: _result$)) + result$ = _resultBits$.getJNIValue(in: environment!) + var flag$ = Int8(1) + environment.interface.SetByteArrayRegion(environment, result_discriminator$, 0, 1, &flag$) + } // render(_:_:) @ JExtractSwiftLib/JNISwift2JavaGenerator+NativeTranslation.swift:624 + else { + result$ = 0 + var flag$ = Int8(0) + environment.interface.SetByteArrayRegion(environment, result_discriminator$, 0, 1, &flag$) + } // render(_:_:) @ JExtractSwiftLib/JNISwift2JavaGenerator+NativeTranslation.swift:634 + return result$ + } + """ + ] + ) + } + + @Test + func optionalJavaKitClass_javaBindings() throws { + try assertOutput( + input: source, + .jni, + .java, + javaClassLookupTable: classLookupTable, + expectedChunks: [ + """ + /** + * Downcall to Swift: + * {@snippet lang=swift : + * public func optionalJavaKitClass(_ arg: JavaLong?) + * } + */ + public static void optionalJavaKitClass(Optional arg) { + SwiftModule.$optionalJavaKitClass(arg.orElse(null)); + } + """, + """ + private static native void $optionalJavaKitClass(java.lang.Long arg); + """ + ] + ) + } + + @Test + func optionalJavaKitClass_swiftThunks() throws { + try assertOutput( + input: source, + .jni, + .swift, + detectChunkByInitialLines: 1, + javaClassLookupTable: classLookupTable, + expectedChunks: [ + """ + @_cdecl("Java_com_example_swift_SwiftModule__00024optionalJavaKitClass__Ljava_lang_Long_2") + func Java_com_example_swift_SwiftModule__00024optionalJavaKitClass__Ljava_lang_Long_2(environment: UnsafeMutablePointer!, thisClass: jclass, arg: jobject?) { + SwiftModule.optionalJavaKitClass(arg.map { + return JavaLong(javaThis: $0, environment: environment!) + } // render(_:_:) @ JExtractSwiftLib/JNISwift2JavaGenerator+NativeTranslation.swift:666 + ) + } + """ + ] + ) + } +}