From 31b9712301cda6006b550efc77107240d78cc1b7 Mon Sep 17 00:00:00 2001 From: Mads Odgaard Date: Sun, 3 Aug 2025 16:25:09 +0200 Subject: [PATCH 01/21] import enum cases --- .../Sources/MySwiftLibrary/Vehicle.swift | 18 ++++++++++ .../Sources/MySwiftLibrary/swift-java.config | 1 + Sources/JExtractSwiftLib/ImportedDecls.swift | 23 +++++++++++++ ...t2JavaGenerator+JavaBindingsPrinting.swift | 15 +++++++++ .../JExtractSwiftLib/Swift2JavaVisitor.swift | 23 +++++++++++++ .../SwiftTypes/SwiftEnumCaseParameter.swift | 33 +++++++++++++++++++ 6 files changed, 113 insertions(+) create mode 100644 Samples/JExtractJNISampleApp/Sources/MySwiftLibrary/Vehicle.swift create mode 100644 Sources/JExtractSwiftLib/SwiftTypes/SwiftEnumCaseParameter.swift diff --git a/Samples/JExtractJNISampleApp/Sources/MySwiftLibrary/Vehicle.swift b/Samples/JExtractJNISampleApp/Sources/MySwiftLibrary/Vehicle.swift new file mode 100644 index 00000000..3661e4db --- /dev/null +++ b/Samples/JExtractJNISampleApp/Sources/MySwiftLibrary/Vehicle.swift @@ -0,0 +1,18 @@ +//===----------------------------------------------------------------------===// +// +// 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 enum Vehicle { + case car + case bicycle +} diff --git a/Samples/JExtractJNISampleApp/Sources/MySwiftLibrary/swift-java.config b/Samples/JExtractJNISampleApp/Sources/MySwiftLibrary/swift-java.config index bb637f34..be44c2fd 100644 --- a/Samples/JExtractJNISampleApp/Sources/MySwiftLibrary/swift-java.config +++ b/Samples/JExtractJNISampleApp/Sources/MySwiftLibrary/swift-java.config @@ -1,4 +1,5 @@ { "javaPackage": "com.example.swift", "mode": "jni", + "logLevel": ["debug"] } diff --git a/Sources/JExtractSwiftLib/ImportedDecls.swift b/Sources/JExtractSwiftLib/ImportedDecls.swift index 4c4d9c0f..f832c6fc 100644 --- a/Sources/JExtractSwiftLib/ImportedDecls.swift +++ b/Sources/JExtractSwiftLib/ImportedDecls.swift @@ -32,6 +32,7 @@ package class ImportedNominalType: ImportedDecl { package var initializers: [ImportedFunc] = [] package var methods: [ImportedFunc] = [] package var variables: [ImportedFunc] = [] + package var cases: [ImportedEnumCase] = [] init(swiftNominal: SwiftNominalTypeDeclaration) { self.swiftNominal = swiftNominal @@ -46,6 +47,28 @@ package class ImportedNominalType: ImportedDecl { } } +public final class ImportedEnumCase: ImportedDecl, CustomStringConvertible { + /// The case name + public var name: String + + /// The enum parameters + var parameters: [SwiftEnumCaseParameter] + + init(name: String, parameters: [SwiftEnumCaseParameter]) { + self.name = name + self.parameters = parameters + } + + public var description: String { + """ + ImportedEnumCase { + name: \(name), + parameters: \(parameters) + } + """ + } +} + public final class ImportedFunc: ImportedDecl, CustomStringConvertible { /// Swift module name (e.g. the target name where a type or function was declared) public var module: String diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift index 0daf14de..3aa1b93b 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift @@ -125,6 +125,11 @@ extension JNISwift2JavaGenerator { printer.println() + if decl.swiftNominal.kind == .enum { + printEnumHelpers(&printer, decl) + printer.println() + } + for initializer in decl.initializers { printFunctionDowncallMethods(&printer, initializer) printer.println() @@ -187,6 +192,16 @@ extension JNISwift2JavaGenerator { } } + private func printEnumHelpers(_ printer: inout CodePrinter, _ decl: ImportedNominalType) { + printer.printBraceBlock("public enum Discriminator") { printer in + printer.print( + decl.cases.map { $0.name.uppercased() }.joined(separator: ",\n") + ) + } + + printer.println() + } + private func printFunctionDowncallMethods( _ printer: inout CodePrinter, _ decl: ImportedFunc diff --git a/Sources/JExtractSwiftLib/Swift2JavaVisitor.swift b/Sources/JExtractSwiftLib/Swift2JavaVisitor.swift index 3efddbfc..d148b2aa 100644 --- a/Sources/JExtractSwiftLib/Swift2JavaVisitor.swift +++ b/Sources/JExtractSwiftLib/Swift2JavaVisitor.swift @@ -65,6 +65,8 @@ final class Swift2JavaVisitor { case .subscriptDecl: // TODO: Implement break + case .enumCaseDecl(let node): + self.visit(enumCaseDecl: node, in: parent) default: break @@ -131,6 +133,27 @@ final class Swift2JavaVisitor { } } + func visit(enumCaseDecl node: EnumCaseDeclSyntax, in typeContext: ImportedNominalType?) { + do { + for caseElement in node.elements { + self.log.debug("Import case \(caseElement.name) of enum \(node.qualifiedNameForDebug)") + + let parameters = try caseElement.parameterClause?.parameters.map { + try SwiftEnumCaseParameter($0, lookupContext: translator.lookupContext) + } + + let imported = ImportedEnumCase( + name: caseElement.name.text, + parameters: parameters ?? [] + ) + + typeContext?.cases.append(imported) + } + } catch { + self.log.debug("Failed to import: \(node.qualifiedNameForDebug); \(error)") + } + } + func visit(variableDecl node: VariableDeclSyntax, in typeContext: ImportedNominalType?) { guard node.shouldExtract(config: config, log: log) else { return diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftEnumCaseParameter.swift b/Sources/JExtractSwiftLib/SwiftTypes/SwiftEnumCaseParameter.swift new file mode 100644 index 00000000..d7640584 --- /dev/null +++ b/Sources/JExtractSwiftLib/SwiftTypes/SwiftEnumCaseParameter.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 +// +//===----------------------------------------------------------------------===// + +import SwiftSyntax + +struct SwiftEnumCaseParameter: Equatable { + var name: String? + var type: SwiftType +} + +extension SwiftEnumCaseParameter { + init( + _ node: EnumCaseParameterSyntax, + lookupContext: SwiftTypeLookupContext + ) throws { + + self.init( + name: node.firstName?.text, + type: try SwiftType(node.type, lookupContext: lookupContext) + ) + } +} From 9b53e698a730be924fd1b24331c7210b755bc888 Mon Sep 17 00:00:00 2001 From: Mads Odgaard Date: Sun, 3 Aug 2025 17:58:48 +0200 Subject: [PATCH 02/21] add support for associated values --- .../Sources/MySwiftLibrary/Vehicle.swift | 3 +- .../java/com/example/swift/VehicleTest.java | 47 +++++++++++++++++++ .../Convenience/SwiftSyntax+Extensions.swift | 2 + ...Swift2JavaGenerator+FunctionLowering.swift | 4 ++ ...MSwift2JavaGenerator+JavaTranslation.swift | 2 +- Sources/JExtractSwiftLib/ImportedDecls.swift | 11 ++++- ...t2JavaGenerator+JavaBindingsPrinting.swift | 13 ++++- ...ISwift2JavaGenerator+JavaTranslation.swift | 4 +- ...ift2JavaGenerator+SwiftThunkPrinting.swift | 17 +++++++ .../JExtractSwiftLib/Swift2JavaVisitor.swift | 26 ++++++++-- .../SwiftTypes/SwiftEnumCaseParameter.swift | 3 +- .../SwiftTypes/SwiftFunctionSignature.swift | 19 ++++++++ .../SwiftTypes/SwiftParameter.swift | 10 ++++ 13 files changed, 149 insertions(+), 12 deletions(-) create mode 100644 Samples/JExtractJNISampleApp/src/test/java/com/example/swift/VehicleTest.java diff --git a/Samples/JExtractJNISampleApp/Sources/MySwiftLibrary/Vehicle.swift b/Samples/JExtractJNISampleApp/Sources/MySwiftLibrary/Vehicle.swift index 3661e4db..3b0ee424 100644 --- a/Samples/JExtractJNISampleApp/Sources/MySwiftLibrary/Vehicle.swift +++ b/Samples/JExtractJNISampleApp/Sources/MySwiftLibrary/Vehicle.swift @@ -13,6 +13,7 @@ //===----------------------------------------------------------------------===// public enum Vehicle { - case car case bicycle + case car(String) + case motorbike(String, horsePower: Int64) } diff --git a/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/VehicleTest.java b/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/VehicleTest.java new file mode 100644 index 00000000..b6f28d08 --- /dev/null +++ b/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/VehicleTest.java @@ -0,0 +1,47 @@ +//===----------------------------------------------------------------------===// +// +// 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 org.swift.swiftkit.core.ConfinedSwiftMemorySession; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +public class VehicleTest { + @Test + void bicycle() { + try (var arena = new ConfinedSwiftMemorySession()) { + Vehicle vehicle = Vehicle.bicycle(arena); + assertNotNull(vehicle); + } + } + + @Test + void car() { + try (var arena = new ConfinedSwiftMemorySession()) { + Vehicle vehicle = Vehicle.car("Porsche 911", arena); + assertNotNull(vehicle); + } + } + + @Test + void motorbike() { + try (var arena = new ConfinedSwiftMemorySession()) { + Vehicle vehicle = Vehicle.motorbike("Yamaha", 750, arena); + assertNotNull(vehicle); + } + } +} \ No newline at end of file diff --git a/Sources/JExtractSwiftLib/Convenience/SwiftSyntax+Extensions.swift b/Sources/JExtractSwiftLib/Convenience/SwiftSyntax+Extensions.swift index e71300af..3719d99d 100644 --- a/Sources/JExtractSwiftLib/Convenience/SwiftSyntax+Extensions.swift +++ b/Sources/JExtractSwiftLib/Convenience/SwiftSyntax+Extensions.swift @@ -253,6 +253,8 @@ extension DeclSyntaxProtocol { } )) .triviaSanitizedDescription + case .enumCaseDecl(let node): + node.triviaSanitizedDescription default: fatalError("unimplemented \(self.kind)") } diff --git a/Sources/JExtractSwiftLib/FFM/CDeclLowering/FFMSwift2JavaGenerator+FunctionLowering.swift b/Sources/JExtractSwiftLib/FFM/CDeclLowering/FFMSwift2JavaGenerator+FunctionLowering.swift index 085a6331..0a61708e 100644 --- a/Sources/JExtractSwiftLib/FFM/CDeclLowering/FFMSwift2JavaGenerator+FunctionLowering.swift +++ b/Sources/JExtractSwiftLib/FFM/CDeclLowering/FFMSwift2JavaGenerator+FunctionLowering.swift @@ -835,6 +835,10 @@ extension LoweredFunctionSignature { case .setter: assert(paramExprs.count == 1) resultExpr = "\(callee) = \(paramExprs[0])" + + case .enumCase: + // This should not be called, but let's fatalError. + fatalError("Enum cases are not supported with FFM.") } // Lower the result. diff --git a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift index f8b2e7e8..d622b710 100644 --- a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift +++ b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift @@ -146,7 +146,7 @@ extension FFMSwift2JavaGenerator { let javaName = switch decl.apiKind { case .getter: decl.javaGetterName case .setter: decl.javaSetterName - case .function, .initializer: decl.name + case .function, .initializer, .enumCase: decl.name } // Signature. diff --git a/Sources/JExtractSwiftLib/ImportedDecls.swift b/Sources/JExtractSwiftLib/ImportedDecls.swift index f832c6fc..c8451eb9 100644 --- a/Sources/JExtractSwiftLib/ImportedDecls.swift +++ b/Sources/JExtractSwiftLib/ImportedDecls.swift @@ -22,6 +22,7 @@ package enum SwiftAPIKind { case initializer case getter case setter + case enumCase } /// Describes a Swift nominal type (e.g., a class, struct, enum) that has been @@ -54,16 +55,21 @@ public final class ImportedEnumCase: ImportedDecl, CustomStringConvertible { /// The enum parameters var parameters: [SwiftEnumCaseParameter] - init(name: String, parameters: [SwiftEnumCaseParameter]) { + /// A function that represents the Swift static "initializer" for cases + var caseFunction: ImportedFunc + + init(name: String, parameters: [SwiftEnumCaseParameter], caseFunction: ImportedFunc) { self.name = name self.parameters = parameters + self.caseFunction = caseFunction } public var description: String { """ ImportedEnumCase { name: \(name), - parameters: \(parameters) + parameters: \(parameters), + caseFunction: \(caseFunction) } """ } @@ -136,6 +142,7 @@ public final class ImportedFunc: ImportedDecl, CustomStringConvertible { let prefix = switch self.apiKind { case .getter: "getter:" case .setter: "setter:" + case .enumCase: "case:" case .function, .initializer: "" } diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift index 3aa1b93b..be43c5b6 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift @@ -193,13 +193,24 @@ extension JNISwift2JavaGenerator { } private func printEnumHelpers(_ printer: inout CodePrinter, _ decl: ImportedNominalType) { + // TODO: Move this to seperate file +Enum? + printEnumDiscriminator(&printer, decl) + printer.println() + printEnumStaticInitializers(&printer, decl) + } + + private func printEnumDiscriminator(_ printer: inout CodePrinter, _ decl: ImportedNominalType) { printer.printBraceBlock("public enum Discriminator") { printer in printer.print( decl.cases.map { $0.name.uppercased() }.joined(separator: ",\n") ) } + } - printer.println() + private func printEnumStaticInitializers(_ printer: inout CodePrinter, _ decl: ImportedNominalType) { + for enumCase in decl.cases { + printFunctionDowncallMethods(&printer, enumCase.caseFunction) + } } private func printFunctionDowncallMethods( diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift index 0db77ece..76354562 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift @@ -61,7 +61,7 @@ extension JNISwift2JavaGenerator { let javaName = switch decl.apiKind { case .getter: decl.javaGetterName case .setter: decl.javaSetterName - case .function, .initializer: decl.name + case .function, .initializer, .enumCase: decl.name } // Swift -> Java @@ -137,7 +137,7 @@ extension JNISwift2JavaGenerator { parentName: String ) throws -> TranslatedFunctionSignature { let parameters = try functionSignature.parameters.enumerated().map { idx, param in - let parameterName = param.parameterName ?? "arg\(idx))" + let parameterName = param.parameterName ?? "arg\(idx)" return try translateParameter(swiftType: param.type, parameterName: parameterName, methodName: methodName, parentName: parentName) } diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift index ca580e60..50a30e8d 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift @@ -102,6 +102,11 @@ extension JNISwift2JavaGenerator { printer.println() } + for enumCase in type.cases { + printSwiftFunctionThunk(&printer, enumCase.caseFunction) + printer.println() + } + for method in type.methods { printSwiftFunctionThunk(&printer, method) printer.println() @@ -190,6 +195,18 @@ extension JNISwift2JavaGenerator { .joined(separator: ", ") result = "\(tryClause)\(callee).\(decl.name)(\(downcallArguments))" + case .enumCase: + let downcallArguments = zip( + decl.functionSignature.parameters, + arguments + ).map { originalParam, argument in + let label = originalParam.argumentLabel.map { "\($0): " } ?? "" + return "\(label)\(argument)" + } + + let associatedValues = !downcallArguments.isEmpty ? "(\(downcallArguments.joined(separator: ", ")))" : "" + result = "\(callee).\(decl.name)\(associatedValues)" + case .getter: result = "\(tryClause)\(callee).\(decl.name)" diff --git a/Sources/JExtractSwiftLib/Swift2JavaVisitor.swift b/Sources/JExtractSwiftLib/Swift2JavaVisitor.swift index d148b2aa..399a7147 100644 --- a/Sources/JExtractSwiftLib/Swift2JavaVisitor.swift +++ b/Sources/JExtractSwiftLib/Swift2JavaVisitor.swift @@ -134,6 +134,11 @@ final class Swift2JavaVisitor { } func visit(enumCaseDecl node: EnumCaseDeclSyntax, in typeContext: ImportedNominalType?) { + guard let typeContext else { + self.log.info("Enum case must be within a current type; \(node)") + return + } + do { for caseElement in node.elements { self.log.debug("Import case \(caseElement.name) of enum \(node.qualifiedNameForDebug)") @@ -142,12 +147,27 @@ final class Swift2JavaVisitor { try SwiftEnumCaseParameter($0, lookupContext: translator.lookupContext) } - let imported = ImportedEnumCase( + let signature = try SwiftFunctionSignature( + caseElement, + enclosingType: typeContext.swiftType, + lookupContext: translator.lookupContext + ) + + let caseFunction = ImportedFunc( + module: translator.swiftModuleName, + swiftDecl: node, + name: caseElement.name.text, + apiKind: .enumCase, + functionSignature: signature + ) + + let importedCase = ImportedEnumCase( name: caseElement.name.text, - parameters: parameters ?? [] + parameters: parameters ?? [], + caseFunction: caseFunction ) - typeContext?.cases.append(imported) + typeContext.cases.append(importedCase) } } catch { self.log.debug("Failed to import: \(node.qualifiedNameForDebug); \(error)") diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftEnumCaseParameter.swift b/Sources/JExtractSwiftLib/SwiftTypes/SwiftEnumCaseParameter.swift index d7640584..55682152 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftEnumCaseParameter.swift +++ b/Sources/JExtractSwiftLib/SwiftTypes/SwiftEnumCaseParameter.swift @@ -24,9 +24,8 @@ extension SwiftEnumCaseParameter { _ node: EnumCaseParameterSyntax, lookupContext: SwiftTypeLookupContext ) throws { - self.init( - name: node.firstName?.text, + name: node.firstName?.identifier?.name, type: try SwiftType(node.type, lookupContext: lookupContext) ) } diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftFunctionSignature.swift b/Sources/JExtractSwiftLib/SwiftTypes/SwiftFunctionSignature.swift index 85b96110..3b645c05 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftFunctionSignature.swift +++ b/Sources/JExtractSwiftLib/SwiftTypes/SwiftFunctionSignature.swift @@ -96,6 +96,25 @@ extension SwiftFunctionSignature { ) } + init( + _ node: EnumCaseElementSyntax, + enclosingType: SwiftType, + lookupContext: SwiftTypeLookupContext + ) throws { + let parameters = try node.parameterClause?.parameters.map { param in + try SwiftParameter(param, lookupContext: lookupContext) + } + + self.init( + selfParameter: .initializer(enclosingType), + parameters: parameters ?? [], + result: SwiftResult(convention: .direct, type: enclosingType), + effectSpecifiers: [], + genericParameters: [], + genericRequirements: [] + ) + } + init( _ node: FunctionDeclSyntax, enclosingType: SwiftType?, diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftParameter.swift b/Sources/JExtractSwiftLib/SwiftTypes/SwiftParameter.swift index 75d165e9..63f7d75b 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftParameter.swift +++ b/Sources/JExtractSwiftLib/SwiftTypes/SwiftParameter.swift @@ -57,6 +57,16 @@ enum SwiftParameterConvention: Equatable { case `inout` } +extension SwiftParameter { + init(_ node: EnumCaseParameterSyntax, lookupContext: SwiftTypeLookupContext) throws { + self.convention = .byValue + self.type = try SwiftType(node.type, lookupContext: lookupContext) + self.argumentLabel = nil + self.parameterName = node.firstName?.identifier?.name + self.argumentLabel = node.firstName?.identifier?.name + } +} + extension SwiftParameter { init(_ node: FunctionParameterSyntax, lookupContext: SwiftTypeLookupContext) throws { // Determine the convention. The default is by-value, but there are From 0d0af8bfe235b2974e277ab5b0a2520c8208ea0b Mon Sep 17 00:00:00 2001 From: Mads Odgaard Date: Sun, 3 Aug 2025 21:40:11 +0200 Subject: [PATCH 03/21] add support for failable initializers --- .../Sources/MySwiftLibrary/Vehicle.swift | 35 +++++++++++++++ .../java/com/example/swift/VehicleTest.java | 45 ++++++++++++++++++- ...t2JavaGenerator+JavaBindingsPrinting.swift | 16 +++++++ .../JExtractSwiftLib/Swift2JavaVisitor.swift | 9 ---- .../SwiftTypes/SwiftFunctionSignature.swift | 9 ++-- 5 files changed, 97 insertions(+), 17 deletions(-) diff --git a/Samples/JExtractJNISampleApp/Sources/MySwiftLibrary/Vehicle.swift b/Samples/JExtractJNISampleApp/Sources/MySwiftLibrary/Vehicle.swift index 3b0ee424..c426ed77 100644 --- a/Samples/JExtractJNISampleApp/Sources/MySwiftLibrary/Vehicle.swift +++ b/Samples/JExtractJNISampleApp/Sources/MySwiftLibrary/Vehicle.swift @@ -16,4 +16,39 @@ public enum Vehicle { case bicycle case car(String) case motorbike(String, horsePower: Int64) + + public init?(name: String) { + switch name { + case "bicycle": self = .bicycle + case "car": self = .car("Unknown") + case "motorbike": self = .motorbike("Unknown", horsePower: 0) + default: return nil + } + } + + public var name: String { + switch self { + case .bicycle: "bicycle" + case .car: "car" + case .motorbike: "motorbike" + } + } + + public func isFasterThan(other: Vehicle) -> Bool { + switch (self, other) { + case (.bicycle, .bicycle), (.bicycle, .car), (.bicycle, .motorbike): false + case (.car, .bicycle): true + case (.car, .motorbike), (.car, .car): false + case (.motorbike, .bicycle), (.motorbike, .car): true + case (.motorbike, .motorbike): false + } + } + + public mutating func upgrade() { + switch self { + case .bicycle: self = .car("Unknown") + case .car: self = .motorbike("Unknown", horsePower: 0) + case .motorbike: break + } + } } diff --git a/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/VehicleTest.java b/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/VehicleTest.java index b6f28d08..1e45c7f8 100644 --- a/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/VehicleTest.java +++ b/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/VehicleTest.java @@ -17,8 +17,9 @@ import org.junit.jupiter.api.Test; import org.swift.swiftkit.core.ConfinedSwiftMemorySession; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; public class VehicleTest { @Test @@ -44,4 +45,44 @@ void motorbike() { assertNotNull(vehicle); } } + + @Test + void initName() { + try (var arena = new ConfinedSwiftMemorySession()) { + assertFalse(Vehicle.init("bus", arena).isPresent()); + Optional vehicle = Vehicle.init("car", arena); + assertTrue(vehicle.isPresent()); + assertNotNull(vehicle.get()); + } + } + + @Test + void nameProperty() { + try (var arena = new ConfinedSwiftMemorySession()) { + Vehicle vehicle = Vehicle.bicycle(arena); + assertEquals("bicycle", vehicle.getName()); + } + } + + @Test + void isFasterThan() { + try (var arena = new ConfinedSwiftMemorySession()) { + Vehicle bicycle = Vehicle.bicycle(arena); + Vehicle car = Vehicle.car("Porsche 911", arena); + assertFalse(bicycle.isFasterThan(car)); + assertTrue(car.isFasterThan(bicycle)); + } + } + + @Test + void upgrade() { + try (var arena = new ConfinedSwiftMemorySession()) { + Vehicle vehicle = Vehicle.bicycle(arena); + assertEquals("bicycle", vehicle.getName()); + vehicle.upgrade(); + assertEquals("car", vehicle.getName()); + vehicle.upgrade(); + assertEquals("motorbike", vehicle.getName()); + } + } } \ No newline at end of file diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift index be43c5b6..bad5859a 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift @@ -196,7 +196,11 @@ extension JNISwift2JavaGenerator { // TODO: Move this to seperate file +Enum? printEnumDiscriminator(&printer, decl) printer.println() + printEnumCaseInterface(&printer, decl) + printer.println() printEnumStaticInitializers(&printer, decl) + printer.println() + printEnumCases(&printer, decl) } private func printEnumDiscriminator(_ printer: inout CodePrinter, _ decl: ImportedNominalType) { @@ -207,12 +211,24 @@ extension JNISwift2JavaGenerator { } } + private func printEnumCaseInterface(_ printer: inout CodePrinter, _ decl: ImportedNominalType) { +// printer.print("public sealed interface Case {}") + } + private func printEnumStaticInitializers(_ printer: inout CodePrinter, _ decl: ImportedNominalType) { for enumCase in decl.cases { printFunctionDowncallMethods(&printer, enumCase.caseFunction) } } + private func printEnumCases(_ printer: inout CodePrinter, _ decl: ImportedNominalType) { +// for enumCase in decl.cases { +// printer.printBraceBlock("public static final \(enumCase.name.firstCharacterUppercased) implements Case") { printer in +// +// } +// } + } + private func printFunctionDowncallMethods( _ printer: inout CodePrinter, _ decl: ImportedFunc diff --git a/Sources/JExtractSwiftLib/Swift2JavaVisitor.swift b/Sources/JExtractSwiftLib/Swift2JavaVisitor.swift index 399a7147..ea2543f2 100644 --- a/Sources/JExtractSwiftLib/Swift2JavaVisitor.swift +++ b/Sources/JExtractSwiftLib/Swift2JavaVisitor.swift @@ -276,15 +276,6 @@ extension DeclSyntaxProtocol where Self: WithModifiersSyntax & WithAttributesSyn return false } - if let node = self.as(InitializerDeclSyntax.self) { - let isFailable = node.optionalMark != nil - - if isFailable { - log.warning("Skip import '\(self.qualifiedNameForDebug)': failable initializer") - return false - } - } - return true } } diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftFunctionSignature.swift b/Sources/JExtractSwiftLib/SwiftTypes/SwiftFunctionSignature.swift index 3b645c05..a5b01bee 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftFunctionSignature.swift +++ b/Sources/JExtractSwiftLib/SwiftTypes/SwiftFunctionSignature.swift @@ -71,11 +71,6 @@ extension SwiftFunctionSignature { throw SwiftFunctionTranslationError.missingEnclosingType(node) } - // We do not yet support failable initializers. - if node.optionalMark != nil { - throw SwiftFunctionTranslationError.failableInitializer(node) - } - let (genericParams, genericRequirements) = try Self.translateGenericParameters( parameterClause: node.genericParameterClause, whereClause: node.genericWhereClause, @@ -86,10 +81,12 @@ extension SwiftFunctionSignature { lookupContext: lookupContext ) + let type = node.optionalMark != nil ? .optional(enclosingType) : enclosingType + self.init( selfParameter: .initializer(enclosingType), parameters: parameters, - result: SwiftResult(convention: .direct, type: enclosingType), + result: SwiftResult(convention: .direct, type: type), effectSpecifiers: effectSpecifiers, genericParameters: genericParams, genericRequirements: genericRequirements From ffcab93409f3f062527f0db6eef7529774906254 Mon Sep 17 00:00:00 2001 From: Mads Odgaard Date: Sun, 3 Aug 2025 22:42:26 +0200 Subject: [PATCH 04/21] synthesize `RawRepresentable` --- .../Sources/MySwiftLibrary/Alignment.swift | 18 +++++++++ .../java/com/example/swift/AlignmentTest.java | 40 +++++++++++++++++++ .../JExtractSwiftLib/Swift2JavaVisitor.swift | 34 +++++++++++++++- .../SwiftNominalTypeDeclaration.swift | 8 ++++ .../SwiftTypes/SwiftType.swift | 13 ++++++ 5 files changed, 112 insertions(+), 1 deletion(-) create mode 100644 Samples/JExtractJNISampleApp/Sources/MySwiftLibrary/Alignment.swift create mode 100644 Samples/JExtractJNISampleApp/src/test/java/com/example/swift/AlignmentTest.java diff --git a/Samples/JExtractJNISampleApp/Sources/MySwiftLibrary/Alignment.swift b/Samples/JExtractJNISampleApp/Sources/MySwiftLibrary/Alignment.swift new file mode 100644 index 00000000..760c564b --- /dev/null +++ b/Samples/JExtractJNISampleApp/Sources/MySwiftLibrary/Alignment.swift @@ -0,0 +1,18 @@ +//===----------------------------------------------------------------------===// +// +// 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 enum Alignment: String { + case horizontal + case vertical +} diff --git a/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/AlignmentTest.java b/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/AlignmentTest.java new file mode 100644 index 00000000..85b90546 --- /dev/null +++ b/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/AlignmentTest.java @@ -0,0 +1,40 @@ +//===----------------------------------------------------------------------===// +// +// 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 org.swift.swiftkit.core.ConfinedSwiftMemorySession; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; + +public class AlignmentTest { + @Test + void rawValue() { + try (var arena = new ConfinedSwiftMemorySession()) { + Optional invalid = Alignment.init("invalid", arena); + assertFalse(invalid.isPresent()); + + Optional horizontal = Alignment.init("horizontal", arena); + assertTrue(horizontal.isPresent()); + assertEquals("horizontal", horizontal.get().getRawValue()); + + Optional vertical = Alignment.init("vertical", arena); + assertTrue(vertical.isPresent()); + assertEquals("vertical", vertical.get().getRawValue()); + } + } +} \ No newline at end of file diff --git a/Sources/JExtractSwiftLib/Swift2JavaVisitor.swift b/Sources/JExtractSwiftLib/Swift2JavaVisitor.swift index ea2543f2..ea474a25 100644 --- a/Sources/JExtractSwiftLib/Swift2JavaVisitor.swift +++ b/Sources/JExtractSwiftLib/Swift2JavaVisitor.swift @@ -46,7 +46,7 @@ final class Swift2JavaVisitor { case .structDecl(let node): self.visit(nominalDecl: node, in: parent) case .enumDecl(let node): - self.visit(nominalDecl: node, in: parent) + self.visit(enumDecl: node, in: parent) case .protocolDecl(let node): self.visit(nominalDecl: node, in: parent) case .extensionDecl(let node): @@ -85,6 +85,12 @@ final class Swift2JavaVisitor { } } + func visit(enumDecl node: EnumDeclSyntax, in parent: ImportedNominalType?) { + self.visit(nominalDecl: node, in: parent) + + self.synthesizeRawRepresentableConformance(enumDecl: node, in: parent) + } + func visit(extensionDecl node: ExtensionDeclSyntax, in parent: ImportedNominalType?) { guard parent != nil else { // 'extension' in a nominal type is invalid. Ignore @@ -256,6 +262,32 @@ final class Swift2JavaVisitor { typeContext.initializers.append(imported) } + + private func synthesizeRawRepresentableConformance(enumDecl node: EnumDeclSyntax, in parent: ImportedNominalType?) { + guard let imported = translator.importedNominalType(node, parent: parent) else { + return + } + + if let firstInheritanceType = imported.swiftNominal.firstInheritanceType, + let inheritanceType = try? SwiftType( + firstInheritanceType, + lookupContext: translator.lookupContext + ), + inheritanceType.isRawTypeCompatible + { + if !imported.variables.contains(where: { $0.name == "rawValue" && $0.functionSignature.result.type != inheritanceType }) { + let decl: DeclSyntax = "public var rawValue: \(raw: inheritanceType.description) { get }" + self.visit(decl: decl, in: imported) + } + + imported.variables.first?.signatureString + + if !imported.initializers.contains(where: { $0.functionSignature.parameters.count == 1 && $0.functionSignature.parameters.first?.parameterName == "rawValue" && $0.functionSignature.parameters.first?.type == inheritanceType }) { + let decl: DeclSyntax = "public init?(rawValue: \(raw: inheritanceType))" + self.visit(decl: decl, in: imported) + } + } + } } extension DeclSyntaxProtocol where Self: WithModifiersSyntax & WithAttributesSyntax { diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftNominalTypeDeclaration.swift b/Sources/JExtractSwiftLib/SwiftTypes/SwiftNominalTypeDeclaration.swift index cef4e731..335979a4 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftNominalTypeDeclaration.swift +++ b/Sources/JExtractSwiftLib/SwiftTypes/SwiftNominalTypeDeclaration.swift @@ -85,6 +85,14 @@ package class SwiftNominalTypeDeclaration: SwiftTypeDeclaration { super.init(moduleName: moduleName, name: node.name.text) } + lazy var firstInheritanceType: TypeSyntax? = { + guard let firstInheritanceType = self.syntax?.inheritanceClause?.inheritedTypes.first else { + return nil + } + + return firstInheritanceType.type + }() + /// Returns true if this type conforms to `Sendable` and therefore is "threadsafe". lazy var isSendable: Bool = { // Check if Sendable is in the inheritance list diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftType.swift b/Sources/JExtractSwiftLib/SwiftTypes/SwiftType.swift index 3cc14406..58bb65c3 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftType.swift +++ b/Sources/JExtractSwiftLib/SwiftTypes/SwiftType.swift @@ -103,6 +103,19 @@ enum SwiftType: Equatable { default: false } } + + var isRawTypeCompatible: Bool { + switch self { + case .nominal(let nominal): + switch nominal.nominalTypeDecl.knownTypeKind { + case .int, .uint, .int8, .uint8, .int16, .uint16, .int32, .uint32, .int64, .uint64, .float, .double, .string: + true + default: + false + } + default: false + } + } } extension SwiftType: CustomStringConvertible { From e74299e4579b557ddb23c9baa2040a148636389a Mon Sep 17 00:00:00 2001 From: Mads Odgaard Date: Mon, 4 Aug 2025 08:22:56 +0200 Subject: [PATCH 05/21] rename tests --- .../swift/{AlignmentTest.java => AlignmentEnumTest.java} | 2 +- .../example/swift/{VehicleTest.java => VehicleEnumTest.java} | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename Samples/JExtractJNISampleApp/src/test/java/com/example/swift/{AlignmentTest.java => AlignmentEnumTest.java} (97%) rename Samples/JExtractJNISampleApp/src/test/java/com/example/swift/{VehicleTest.java => VehicleEnumTest.java} (98%) diff --git a/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/AlignmentTest.java b/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/AlignmentEnumTest.java similarity index 97% rename from Samples/JExtractJNISampleApp/src/test/java/com/example/swift/AlignmentTest.java rename to Samples/JExtractJNISampleApp/src/test/java/com/example/swift/AlignmentEnumTest.java index 85b90546..6256bd84 100644 --- a/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/AlignmentTest.java +++ b/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/AlignmentEnumTest.java @@ -21,7 +21,7 @@ import static org.junit.jupiter.api.Assertions.*; -public class AlignmentTest { +public class AlignmentEnumTest { @Test void rawValue() { try (var arena = new ConfinedSwiftMemorySession()) { diff --git a/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/VehicleTest.java b/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/VehicleEnumTest.java similarity index 98% rename from Samples/JExtractJNISampleApp/src/test/java/com/example/swift/VehicleTest.java rename to Samples/JExtractJNISampleApp/src/test/java/com/example/swift/VehicleEnumTest.java index 1e45c7f8..a3f20475 100644 --- a/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/VehicleTest.java +++ b/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/VehicleEnumTest.java @@ -21,7 +21,7 @@ import static org.junit.jupiter.api.Assertions.*; -public class VehicleTest { +public class VehicleEnumTest { @Test void bicycle() { try (var arena = new ConfinedSwiftMemorySession()) { From 5efd8c5aa6637d4bf8da5f2820d7c0a18a4aecc1 Mon Sep 17 00:00:00 2001 From: Mads Odgaard Date: Tue, 5 Aug 2025 21:24:57 +0200 Subject: [PATCH 06/21] add simple support for getting associated values --- .../java/com/example/swift/EnumBenchmark.java | 59 ++++++++ .../com/example/swift/VehicleEnumTest.java | 42 ++++++ Sources/JExtractSwiftLib/ImportedDecls.swift | 25 +++- ...t2JavaGenerator+JavaBindingsPrinting.swift | 57 +++++++- ...ISwift2JavaGenerator+JavaTranslation.swift | 127 ++++++++++++++++-- ...wift2JavaGenerator+NativeTranslation.swift | 17 +++ ...ift2JavaGenerator+SwiftThunkPrinting.swift | 60 ++++++++- .../JNI/JNISwift2JavaGenerator.swift | 1 + .../JExtractSwiftLib/Swift2JavaVisitor.swift | 2 + Sources/JavaTypes/Mangling.swift | 7 +- 10 files changed, 373 insertions(+), 24 deletions(-) create mode 100644 Samples/JExtractJNISampleApp/src/jmh/java/com/example/swift/EnumBenchmark.java diff --git a/Samples/JExtractJNISampleApp/src/jmh/java/com/example/swift/EnumBenchmark.java b/Samples/JExtractJNISampleApp/src/jmh/java/com/example/swift/EnumBenchmark.java new file mode 100644 index 00000000..daf36f38 --- /dev/null +++ b/Samples/JExtractJNISampleApp/src/jmh/java/com/example/swift/EnumBenchmark.java @@ -0,0 +1,59 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 20245Apple 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.openjdk.jmh.annotations.*; +import org.openjdk.jmh.infra.Blackhole; +import org.swift.swiftkit.core.ClosableSwiftArena; +import org.swift.swiftkit.core.ConfinedSwiftMemorySession; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.TimeUnit; + +@BenchmarkMode(Mode.AverageTime) +@Warmup(iterations = 5, time = 200, timeUnit = TimeUnit.MILLISECONDS) +@Measurement(iterations = 10, time = 500, timeUnit = TimeUnit.MILLISECONDS) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@Fork(value = 3, jvmArgsAppend = { "--enable-native-access=ALL-UNNAMED" }) +public class EnumBenchmark { + + @State(Scope.Benchmark) + public static class BenchmarkState { + ClosableSwiftArena arena; + Vehicle vehicle; + + @Setup(Level.Trial) + public void beforeAll() { + arena = new ConfinedSwiftMemorySession(); + vehicle = Vehicle.motorbike("Yamaha", 900, arena); + } + + @TearDown(Level.Trial) + public void afterAll() { + arena.close(); + } + } + + @Benchmark + public Vehicle.Motorbike java_copy(BenchmarkState state, Blackhole bh) { + Vehicle.Motorbike motorbike = state.vehicle.getAsMotorbike().orElseThrow(); + bh.consume(motorbike.arg0()); + bh.consume(motorbike.horsePower()); + + return motorbike; + } +} \ No newline at end of file diff --git a/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/VehicleEnumTest.java b/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/VehicleEnumTest.java index a3f20475..ffe6ec77 100644 --- a/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/VehicleEnumTest.java +++ b/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/VehicleEnumTest.java @@ -85,4 +85,46 @@ void upgrade() { assertEquals("motorbike", vehicle.getName()); } } + + @Test + void getAsBicycle() { + try (var arena = new ConfinedSwiftMemorySession()) { + Vehicle vehicle = Vehicle.bicycle(arena); + Vehicle.Bicycle bicycle = vehicle.getAsBicycle().orElseThrow(); + assertNotNull(bicycle); + } + } + + @Test + void getAsCar() { + try (var arena = new ConfinedSwiftMemorySession()) { + Vehicle vehicle = Vehicle.car("BMW", arena); + Vehicle.Car car = vehicle.getAsCar().orElseThrow(); + assertEquals("BMW", car.arg0()); + } + } + + @Test + void getAsMotorbike() { + try (var arena = new ConfinedSwiftMemorySession()) { + Vehicle vehicle = Vehicle.motorbike("Yamaha", 750, arena); + Vehicle.Motorbike motorbike = vehicle.getAsMotorbike().orElseThrow(); + assertEquals("Yamaha", motorbike.arg0()); + assertEquals(750, motorbike.horsePower()); + } + } + + @Test + void associatedValuesAreCopied() { + try (var arena = new ConfinedSwiftMemorySession()) { + Vehicle vehicle = Vehicle.car("BMW", arena); + Vehicle.Car car = vehicle.getAsCar().orElseThrow(); + assertEquals("BMW", car.arg0()); + vehicle.upgrade(); + Vehicle.Motorbike motorbike = vehicle.getAsMotorbike().orElseThrow(); + assertNotNull(motorbike); + // Motorbike should still remain + assertEquals("BMW", car.arg0()); + } + } } \ No newline at end of file diff --git a/Sources/JExtractSwiftLib/ImportedDecls.swift b/Sources/JExtractSwiftLib/ImportedDecls.swift index c8451eb9..5655af00 100644 --- a/Sources/JExtractSwiftLib/ImportedDecls.swift +++ b/Sources/JExtractSwiftLib/ImportedDecls.swift @@ -55,12 +55,24 @@ public final class ImportedEnumCase: ImportedDecl, CustomStringConvertible { /// The enum parameters var parameters: [SwiftEnumCaseParameter] + var swiftDecl: any DeclSyntaxProtocol + + var enumType: SwiftNominalType + /// A function that represents the Swift static "initializer" for cases var caseFunction: ImportedFunc - init(name: String, parameters: [SwiftEnumCaseParameter], caseFunction: ImportedFunc) { + init( + name: String, + parameters: [SwiftEnumCaseParameter], + swiftDecl: any DeclSyntaxProtocol, + enumType: SwiftNominalType, + caseFunction: ImportedFunc + ) { self.name = name self.parameters = parameters + self.swiftDecl = swiftDecl + self.enumType = enumType self.caseFunction = caseFunction } @@ -69,12 +81,23 @@ public final class ImportedEnumCase: ImportedDecl, CustomStringConvertible { ImportedEnumCase { name: \(name), parameters: \(parameters), + swiftDecl: \(swiftDecl), + enumType: \(enumType), caseFunction: \(caseFunction) } """ } } +extension ImportedEnumCase: Hashable { + public func hash(into hasher: inout Hasher) { + hasher.combine(ObjectIdentifier(self)) + } + public static func == (lhs: ImportedEnumCase, rhs: ImportedEnumCase) -> Bool { + return lhs === rhs + } +} + public final class ImportedFunc: ImportedDecl, CustomStringConvertible { /// Swift module name (e.g. the target name where a type or function was declared) public var module: String diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift index bad5859a..cf0aee23 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift @@ -193,7 +193,6 @@ extension JNISwift2JavaGenerator { } private func printEnumHelpers(_ printer: inout CodePrinter, _ decl: ImportedNominalType) { - // TODO: Move this to seperate file +Enum? printEnumDiscriminator(&printer, decl) printer.println() printEnumCaseInterface(&printer, decl) @@ -212,7 +211,8 @@ extension JNISwift2JavaGenerator { } private func printEnumCaseInterface(_ printer: inout CodePrinter, _ decl: ImportedNominalType) { -// printer.print("public sealed interface Case {}") + printer.print("public sealed interface Case {}") + // TODO: Print `getCase()` method to allow for easy pattern matching. } private func printEnumStaticInitializers(_ printer: inout CodePrinter, _ decl: ImportedNominalType) { @@ -222,11 +222,54 @@ extension JNISwift2JavaGenerator { } private func printEnumCases(_ printer: inout CodePrinter, _ decl: ImportedNominalType) { -// for enumCase in decl.cases { -// printer.printBraceBlock("public static final \(enumCase.name.firstCharacterUppercased) implements Case") { printer in -// -// } -// } + for enumCase in decl.cases { + guard let translatedCase = self.translatedEnumCase(for: enumCase) else { + return + } + + let members = translatedCase.translatedValues.map { + $0.parameter.renderParameter() + } + + let caseName = enumCase.name.firstCharacterUppercased + let hasParameters = !enumCase.parameters.isEmpty + + // Print record + printer.printBraceBlock("public record \(caseName)(\(members.joined(separator: ", "))) implements Case") { printer in + if hasParameters { + let nativeResults = zip(translatedCase.translatedValues, translatedCase.conversions).map { value, conversion in + "\(conversion.native.javaType) \(value.parameter.name)" + } + printer.print(#"@SuppressWarnings("unused")"#) + printer.printBraceBlock("static \(caseName) fromJNI(\(nativeResults.joined(separator: ", ")))") { printer in + let memberValues = zip(translatedCase.translatedValues, translatedCase.conversions).map { (value, conversion) in + let result = conversion.translated.conversion.render(&printer, value.parameter.name) + return result + } + printer.print("return new \(caseName)(\(memberValues.joined(separator: ", ")));") + } + } + } + + // TODO: Optimize when all values can just be passed directly, instead of going through "middle type"? + + // Print method to get enum as case + printer.printBraceBlock("public Optional<\(caseName)> getAs\(caseName)()") { printer in + // TODO: Check that discriminator is OK + if hasParameters { + printer.print( + """ + return Optional.of($getAs\(caseName)(this.$memoryAddress())); + """ + ) + } else { + printer.print("return Optional.of(new \(caseName)());") + } + } + printer.print("private static native \(caseName) $getAs\(caseName)(long self);") + + printer.println() + } } private func printFunctionDowncallMethods( diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift index 76354562..dc71d4a6 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift @@ -41,12 +41,82 @@ extension JNISwift2JavaGenerator { return translated } + func translatedEnumCase( + for decl: ImportedEnumCase + ) -> TranslatedEnumCase? { + if let cached = translatedEnumCases[decl] { + return cached + } + + let translated: TranslatedEnumCase? + do { + let translation = JavaTranslation( + config: config, + swiftModuleName: swiftModuleName, + javaPackage: self.javaPackage, + javaClassLookupTable: self.javaClassLookupTable + ) + translated = try translation.translate(enumCase: decl) + } catch { + self.logger.debug("Failed to translate: '\(decl.swiftDecl.qualifiedNameForDebug)'; \(error)") + translated = nil + } + + translatedEnumCases[decl] = translated + return translated + } + struct JavaTranslation { let config: Configuration let swiftModuleName: String let javaPackage: String let javaClassLookupTable: JavaClassLookupTable + func translate(enumCase: ImportedEnumCase) throws -> TranslatedEnumCase { + let nativeTranslation = NativeJavaTranslation( + config: self.config, + javaPackage: self.javaPackage, + javaClassLookupTable: self.javaClassLookupTable + ) + + let methodName = "" // TODO: Used for closures, replace with better name? + let parentName = "" // TODO: Used for closures, replace with better name? + + let translatedValues = try self.translateParameters( + enumCase.parameters.map { ($0.name, $0.type) }, + methodName: methodName, + parentName: parentName + ) + + let conversions = try enumCase.parameters.map { + let result = SwiftResult(convention: .direct, type: $0.type) + let translatedResult = try self.translate(swiftResult: result) + let nativeResult = try nativeTranslation.translate(swiftResult: result) + return (translatedResult, nativeResult) + } + +// let nativeParameters = try nativeTranslation.translateParameters( +// enumCase.parameters.map { +// SwiftParameter( +// convention: .byValue, +// argumentLabel: $0.name, +// parameterName: $0.name, +// type: $0.type +// ) +// }, +// translatedParameters: translatedParameters, +// methodName: methodName, +// parentName: parentName +// ) + + return TranslatedEnumCase( + name: enumCase.name.firstCharacterUppercased, + enumName: enumCase.enumType.nominalTypeDecl.name, + translatedValues: translatedValues, + conversions: conversions + ) + } + func translate(_ decl: ImportedFunc) throws -> TranslatedFunctionDecl { let nativeTranslation = NativeJavaTranslation( config: self.config, @@ -136,31 +206,47 @@ extension JNISwift2JavaGenerator { methodName: String, parentName: String ) throws -> TranslatedFunctionSignature { - let parameters = try functionSignature.parameters.enumerated().map { idx, param in - let parameterName = param.parameterName ?? "arg\(idx)" + let parameters = try translateParameters( + functionSignature.parameters.map { ($0.parameterName, $0.type )}, + methodName: methodName, + parentName: parentName + ) + + // 'self' + let selfParameter = try self.translateSelfParameter(functionSignature.selfParameter, methodName: methodName, parentName: parentName) + + let resultType = try translate(swiftResult: functionSignature.result) + + return TranslatedFunctionSignature( + selfParameter: selfParameter, + parameters: parameters, + resultType: resultType + ) + } + + func translateParameters( + _ parameters: [(name: String?, type: SwiftType)], + methodName: String, + parentName: String + ) throws -> [TranslatedParameter] { + try parameters.enumerated().map { idx, param in + let parameterName = param.name ?? "arg\(idx)" return try translateParameter(swiftType: param.type, parameterName: parameterName, methodName: methodName, parentName: parentName) } + } + func translateSelfParameter(_ selfParameter: SwiftSelfParameter?, methodName: String, parentName: String) throws -> TranslatedParameter? { // 'self' - let selfParameter: TranslatedParameter? - if case .instance(let swiftSelf) = functionSignature.selfParameter { - selfParameter = try self.translateParameter( + if case .instance(let swiftSelf) = selfParameter { + return try self.translateParameter( swiftType: swiftSelf.type, parameterName: swiftSelf.parameterName ?? "self", methodName: methodName, parentName: parentName ) } else { - selfParameter = nil + return nil } - - let resultType = try translate(swiftResult: functionSignature.result) - - return TranslatedFunctionSignature( - selfParameter: selfParameter, - parameters: parameters, - resultType: resultType - ) } func translateParameter( @@ -475,6 +561,19 @@ extension JNISwift2JavaGenerator { } } + struct TranslatedEnumCase { + /// The corresponding Java case class (CamelCased) + let name: String + + /// The name of the translated enum + let enumName: String + + /// A list of the translated associated values + let translatedValues: [TranslatedParameter] + + let conversions: [(translated: TranslatedResult, native: NativeResult)] + } + struct TranslatedFunctionDecl { /// Java function name let name: String diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift index e7f7efbe..26c1c396 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift @@ -61,6 +61,23 @@ extension JNISwift2JavaGenerator { ) } + func translateParameters( + _ parameters: [SwiftParameter], + translatedParameters: [TranslatedParameter], + methodName: String, + parentName: String + ) throws -> [NativeParameter] { + try zip(translatedParameters, parameters).map { translatedParameter, swiftParameter in + let parameterName = translatedParameter.parameter.name + return try translate( + swiftParameter: swiftParameter, + parameterName: parameterName, + methodName: methodName, + parentName: parentName + ) + } + } + func translate( swiftParameter: SwiftParameter, parameterName: String, diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift index 50a30e8d..019a8aad 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift @@ -103,7 +103,7 @@ extension JNISwift2JavaGenerator { } for enumCase in type.cases { - printSwiftFunctionThunk(&printer, enumCase.caseFunction) + printEnumCase(&printer, enumCase) printer.println() } @@ -120,6 +120,64 @@ extension JNISwift2JavaGenerator { printDestroyFunctionThunk(&printer, type) } + private func printEnumCase(_ printer: inout CodePrinter, _ enumCase: ImportedEnumCase) { + guard let translatedCase = self.translatedEnumCase(for: enumCase) else { + return + } + + // Print static case initializer + printSwiftFunctionThunk(&printer, enumCase.caseFunction) + printer.println() + + // Print getAsCase method + if !enumCase.parameters.isEmpty { + let selfParameter = JavaParameter(name: "self", type: .long) + let resultType = JavaType.class(package: javaPackage, name: "\(translatedCase.enumName).\(translatedCase.name)") + printCDecl( + &printer, + javaMethodName: "$getAs\(translatedCase.name)", + parentName: "\(translatedCase.enumName)", + parameters: [selfParameter], + resultType: resultType + ) { printer in + let selfPointer = self.printSelfJLongToUnsafeMutablePointer( + &printer, + swiftParentName: enumCase.enumType.nominalTypeDecl.name, + selfParameter + ) + let caseNames = enumCase.parameters.enumerated().map { idx, parameter in + parameter.name ?? "_\(idx)" + } + let caseNamesWithLet = caseNames.map { "let \($0)" } + let methodSignature = MethodSignature(resultType: resultType, parameterTypes: translatedCase.conversions.map(\.native.javaType)) + // TODO: Caching of class and static method ID. + printer.print( + """ + guard case .\(enumCase.name)(\(caseNamesWithLet.joined(separator: ", "))) = \(selfPointer).pointee else { + fatalError("Expected enum case '\(enumCase.name)', but was '\\(self$.pointee)'!") + } + let recordClass$ = environment.interface.FindClass(environment, "\(javaPackagePath)/\(translatedCase.enumName)$\(translatedCase.name)")! + let fromJNIID$ = environment.interface.GetStaticMethodID(environment, recordClass$, "fromJNI", "\(methodSignature.mangledName)")! + """ + ) + + let upcallArguments = zip(translatedCase.conversions, caseNames).map { conversion, caseName in + // '0' is treated the same as a null pointer. + let nullConversion = !conversion.native.javaType.isPrimitive ? " ?? 0" : "" + let result = conversion.native.conversion.render(&printer, caseName) + return "\(result)\(nullConversion)" + } + printer.print( + """ + return withVaList([\(upcallArguments.joined(separator: ", "))]) { + return environment.interface.CallStaticObjectMethodV(environment, recordClass$, fromJNIID$, $0) + } + """ + ) + } + } + } + private func printSwiftFunctionThunk( _ printer: inout CodePrinter, _ decl: ImportedFunc diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift index c5f3a5b6..60ec1b8b 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift @@ -39,6 +39,7 @@ package class JNISwift2JavaGenerator: Swift2JavaGenerator { /// Cached Java translation result. 'nil' indicates failed translation. var translatedDecls: [ImportedFunc: TranslatedFunctionDecl] = [:] + var translatedEnumCases: [ImportedEnumCase: TranslatedEnumCase] = [:] /// Because we need to write empty files for SwiftPM, keep track which files we didn't write yet, /// and write an empty file for those. diff --git a/Sources/JExtractSwiftLib/Swift2JavaVisitor.swift b/Sources/JExtractSwiftLib/Swift2JavaVisitor.swift index ea474a25..2516eb56 100644 --- a/Sources/JExtractSwiftLib/Swift2JavaVisitor.swift +++ b/Sources/JExtractSwiftLib/Swift2JavaVisitor.swift @@ -170,6 +170,8 @@ final class Swift2JavaVisitor { let importedCase = ImportedEnumCase( name: caseElement.name.text, parameters: parameters ?? [], + swiftDecl: node, + enumType: SwiftNominalType(nominalTypeDecl: typeContext.swiftNominal), caseFunction: caseFunction ) diff --git a/Sources/JavaTypes/Mangling.swift b/Sources/JavaTypes/Mangling.swift index 74e2e0b8..f0dbd484 100644 --- a/Sources/JavaTypes/Mangling.swift +++ b/Sources/JavaTypes/Mangling.swift @@ -36,7 +36,7 @@ extension JavaType { case .void: "V" case .array(let elementType): "[" + elementType.mangledName case .class(package: let package, name: let name): - "L\(package!).\(name);".replacingPeriodsWithSlashes() + "L\(package!).\(name.replacingPeriodsWithDollars());".replacingPeriodsWithSlashes() } } } @@ -145,4 +145,9 @@ extension StringProtocol { fileprivate func replacingSlashesWithPeriods() -> String { return String(self.map { $0 == "/" ? "." as Character : $0 }) } + + /// Return the string after replacing all of the periods (".") with slashes ("$"). + fileprivate func replacingPeriodsWithDollars() -> String { + return String(self.map { $0 == "." ? "$" as Character : $0 }) + } } From 8985c10e384cf568bc38ee6c8a7187c9ce695e05 Mon Sep 17 00:00:00 2001 From: Mads Odgaard Date: Thu, 7 Aug 2025 07:35:36 +0200 Subject: [PATCH 07/21] add `getCase()` and `getDiscriminator()` --- .../Sources/MySwiftLibrary/Vehicle.swift | 13 +++-- .../java/com/example/swift/EnumBenchmark.java | 1 - .../com/example/swift/VehicleEnumTest.java | 41 ++++++++++++++ ...t2JavaGenerator+JavaBindingsPrinting.swift | 54 ++++++++++++++++--- ...ISwift2JavaGenerator+JavaTranslation.swift | 4 ++ 5 files changed, 102 insertions(+), 11 deletions(-) diff --git a/Samples/JExtractJNISampleApp/Sources/MySwiftLibrary/Vehicle.swift b/Samples/JExtractJNISampleApp/Sources/MySwiftLibrary/Vehicle.swift index c426ed77..8b0f2646 100644 --- a/Samples/JExtractJNISampleApp/Sources/MySwiftLibrary/Vehicle.swift +++ b/Samples/JExtractJNISampleApp/Sources/MySwiftLibrary/Vehicle.swift @@ -16,6 +16,7 @@ public enum Vehicle { case bicycle case car(String) case motorbike(String, horsePower: Int64) + indirect case transformer(front: Vehicle, back: Vehicle) public init?(name: String) { switch name { @@ -31,16 +32,19 @@ public enum Vehicle { case .bicycle: "bicycle" case .car: "car" case .motorbike: "motorbike" + case .transformer: "transformer" } } public func isFasterThan(other: Vehicle) -> Bool { switch (self, other) { - case (.bicycle, .bicycle), (.bicycle, .car), (.bicycle, .motorbike): false + case (.bicycle, .bicycle), (.bicycle, .car), (.bicycle, .motorbike), (.bicycle, .transformer): false case (.car, .bicycle): true - case (.car, .motorbike), (.car, .car): false + case (.car, .motorbike), (.car, .transformer), (.car, .car): false case (.motorbike, .bicycle), (.motorbike, .car): true - case (.motorbike, .motorbike): false + case (.motorbike, .motorbike), (.motorbike, .transformer): false + case (.transformer, .bicycle), (.transformer, .car), (.transformer, .motorbike): true + case (.transformer, .transformer): false } } @@ -48,7 +52,8 @@ public enum Vehicle { switch self { case .bicycle: self = .car("Unknown") case .car: self = .motorbike("Unknown", horsePower: 0) - case .motorbike: break + case .motorbike: self = .transformer(front: .car("BMW"), back: self) + case .transformer: break } } } diff --git a/Samples/JExtractJNISampleApp/src/jmh/java/com/example/swift/EnumBenchmark.java b/Samples/JExtractJNISampleApp/src/jmh/java/com/example/swift/EnumBenchmark.java index daf36f38..ad10d242 100644 --- a/Samples/JExtractJNISampleApp/src/jmh/java/com/example/swift/EnumBenchmark.java +++ b/Samples/JExtractJNISampleApp/src/jmh/java/com/example/swift/EnumBenchmark.java @@ -53,7 +53,6 @@ public Vehicle.Motorbike java_copy(BenchmarkState state, Blackhole bh) { Vehicle.Motorbike motorbike = state.vehicle.getAsMotorbike().orElseThrow(); bh.consume(motorbike.arg0()); bh.consume(motorbike.horsePower()); - return motorbike; } } \ No newline at end of file diff --git a/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/VehicleEnumTest.java b/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/VehicleEnumTest.java index ffe6ec77..0f94f8fa 100644 --- a/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/VehicleEnumTest.java +++ b/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/VehicleEnumTest.java @@ -127,4 +127,45 @@ void associatedValuesAreCopied() { assertEquals("BMW", car.arg0()); } } + + @Test + void getDiscriminator() { + try (var arena = new ConfinedSwiftMemorySession()) { + assertEquals(Vehicle.Discriminator.BICYCLE, Vehicle.bicycle(arena).getDiscriminator()); + assertEquals(Vehicle.Discriminator.CAR, Vehicle.car("BMW", arena).getDiscriminator()); + assertEquals(Vehicle.Discriminator.MOTORBIKE, Vehicle.motorbike("Yamaha", 750, arena).getDiscriminator()); + assertEquals(Vehicle.Discriminator.TRANSFORMER, Vehicle.transformer(Vehicle.bicycle(arena), Vehicle.bicycle(arena), arena).getDiscriminator()); + } + } + + @Test + void getCase() { + try (var arena = new ConfinedSwiftMemorySession()) { + Vehicle vehicle = Vehicle.bicycle(arena); + Vehicle.Case caseElement = vehicle.getCase(arena); + assertInstanceOf(Vehicle.Bicycle.class, caseElement); + } + } + + @Test + void switchGetCase() { + try (var arena = new ConfinedSwiftMemorySession()) { + Vehicle vehicle = Vehicle.car("BMW", arena); + switch (vehicle.getCase(arena)) { + case Vehicle.Bicycle b: + fail("Was bicycle"); + break; + case Vehicle.Car car: + assertEquals("BMW", car.arg0()); + break; + case Vehicle.Motorbike motorbike: + fail("Was motorbike"); + break; + case Vehicle.Transformer transformer: + fail("Was transformer"); + break; + } + } + } + } \ No newline at end of file diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift index cf0aee23..6d18aca1 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift @@ -208,11 +208,35 @@ extension JNISwift2JavaGenerator { decl.cases.map { $0.name.uppercased() }.joined(separator: ",\n") ) } + + // TODO: Consider whether all of these "utility" functions can be printed using our existing printing logic. + printer.printBraceBlock("public Discriminator getDiscriminator()") { printer in + printer.print("return Discriminator.values()[$getDiscriminator(this.$memoryAddress())];") + } + printer.print("private static native int $getDiscriminator(long self);") } private func printEnumCaseInterface(_ printer: inout CodePrinter, _ decl: ImportedNominalType) { printer.print("public sealed interface Case {}") - // TODO: Print `getCase()` method to allow for easy pattern matching. + printer.println() + + let requiresSwiftArena = decl.cases.compactMap { + self.translatedEnumCase(for: $0) + }.contains(where: \.requiresSwiftArena) + + printer.printBraceBlock("public Case getCase(\(requiresSwiftArena ? "SwiftArena swiftArena$" : ""))") { printer in + printer.print("Discriminator discriminator = this.getDiscriminator();") + printer.printBraceBlock("switch (discriminator)") { printer in + for enumCase in decl.cases { + guard let translatedCase = self.translatedEnumCase(for: enumCase) else { + continue + } + let arenaArgument = translatedCase.requiresSwiftArena ? "swiftArena$" : "" + printer.print("case \(enumCase.name.uppercased()): return this.getAs\(enumCase.name.firstCharacterUppercased)(\(arenaArgument)).orElseThrow();") + } + } + printer.print(#"throw new RuntimeException("Unknown discriminator value " + discriminator);"#) + } } private func printEnumStaticInitializers(_ printer: inout CodePrinter, _ decl: ImportedNominalType) { @@ -233,13 +257,17 @@ extension JNISwift2JavaGenerator { let caseName = enumCase.name.firstCharacterUppercased let hasParameters = !enumCase.parameters.isEmpty + let requiresSwiftArena = translatedCase.requiresSwiftArena // Print record printer.printBraceBlock("public record \(caseName)(\(members.joined(separator: ", "))) implements Case") { printer in if hasParameters { - let nativeResults = zip(translatedCase.translatedValues, translatedCase.conversions).map { value, conversion in + var nativeResults = zip(translatedCase.translatedValues, translatedCase.conversions).map { value, conversion in "\(conversion.native.javaType) \(value.parameter.name)" } + if requiresSwiftArena { + nativeResults.append("SwiftArena swiftArena$") + } printer.print(#"@SuppressWarnings("unused")"#) printer.printBraceBlock("static \(caseName) fromJNI(\(nativeResults.joined(separator: ", ")))") { printer in let memberValues = zip(translatedCase.translatedValues, translatedCase.conversions).map { (value, conversion) in @@ -254,19 +282,33 @@ extension JNISwift2JavaGenerator { // TODO: Optimize when all values can just be passed directly, instead of going through "middle type"? // Print method to get enum as case - printer.printBraceBlock("public Optional<\(caseName)> getAs\(caseName)()") { printer in - // TODO: Check that discriminator is OK + printer.printBraceBlock("public Optional<\(caseName)> getAs\(caseName)(\(requiresSwiftArena ? "SwiftArena swiftArena$" : ""))") { printer in + printer.print( + """ + if (this.getDiscriminator() != Discriminator.\(caseName.uppercased())) { + return Optional.empty(); + } + """ + ) if hasParameters { + var arguments = ["this.$memoryAddress()"] + if requiresSwiftArena { + arguments.append("swiftArena$") + } printer.print( """ - return Optional.of($getAs\(caseName)(this.$memoryAddress())); + return Optional.of($getAs\(caseName)(\(arguments.joined(separator: ", ")))); """ ) } else { printer.print("return Optional.of(new \(caseName)());") } } - printer.print("private static native \(caseName) $getAs\(caseName)(long self);") + var nativeParameters = ["long self"] + if requiresSwiftArena { + nativeParameters.append("SwiftArena swiftArena$") + } + printer.print("private static native \(caseName) $getAs\(caseName)(\(nativeParameters.joined(separator: ", ")));") printer.println() } diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift index dc71d4a6..3b6f034d 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift @@ -572,6 +572,10 @@ extension JNISwift2JavaGenerator { let translatedValues: [TranslatedParameter] let conversions: [(translated: TranslatedResult, native: NativeResult)] + + var requiresSwiftArena: Bool { + conversions.contains(where: \.translated.conversion.requiresSwiftArena) + } } struct TranslatedFunctionDecl { From cd6561992486388d7cc43a0b3b550724f7e8a557 Mon Sep 17 00:00:00 2001 From: Mads Odgaard Date: Fri, 8 Aug 2025 17:02:38 +0200 Subject: [PATCH 08/21] print discriminator native --- ...ISwift2JavaGenerator+JavaTranslation.swift | 20 ++---------- ...wift2JavaGenerator+NativeTranslation.swift | 5 +-- ...ift2JavaGenerator+SwiftThunkPrinting.swift | 31 +++++++++++++++++-- 3 files changed, 35 insertions(+), 21 deletions(-) diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift index 3b6f034d..467c306b 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift @@ -88,27 +88,13 @@ extension JNISwift2JavaGenerator { parentName: parentName ) - let conversions = try enumCase.parameters.map { - let result = SwiftResult(convention: .direct, type: $0.type) + let conversions = try enumCase.parameters.enumerated().map { idx, parameter in + let result = SwiftResult(convention: .direct, type: parameter.type) let translatedResult = try self.translate(swiftResult: result) - let nativeResult = try nativeTranslation.translate(swiftResult: result) + let nativeResult = try nativeTranslation.translate(swiftResult: result, resultName: parameter.name ?? "arg\(idx)") return (translatedResult, nativeResult) } -// let nativeParameters = try nativeTranslation.translateParameters( -// enumCase.parameters.map { -// SwiftParameter( -// convention: .byValue, -// argumentLabel: $0.name, -// parameterName: $0.name, -// type: $0.type -// ) -// }, -// translatedParameters: translatedParameters, -// methodName: methodName, -// parentName: parentName -// ) - return TranslatedEnumCase( name: enumCase.name.firstCharacterUppercased, enumName: enumCase.enumType.nominalTypeDecl.name, diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift index 26c1c396..05f61c4c 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift @@ -385,7 +385,8 @@ extension JNISwift2JavaGenerator { } func translate( - swiftResult: SwiftResult + swiftResult: SwiftResult, + resultName: String = "result" ) throws -> NativeResult { switch swiftResult.type { case .nominal(let nominalType): @@ -416,7 +417,7 @@ extension JNISwift2JavaGenerator { return NativeResult( javaType: .long, - conversion: .getJNIValue(.allocateSwiftValue(name: "result", swiftType: swiftResult.type)), + conversion: .getJNIValue(.allocateSwiftValue(name: resultName, swiftType: swiftResult.type)), outParameters: [] ) diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift index 019a8aad..b9f62b14 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift @@ -102,9 +102,14 @@ extension JNISwift2JavaGenerator { printer.println() } - for enumCase in type.cases { - printEnumCase(&printer, enumCase) + if type.swiftNominal.kind == .enum { + printEnumDiscriminator(&printer, type) printer.println() + + for enumCase in type.cases { + printEnumCase(&printer, enumCase) + printer.println() + } } for method in type.methods { @@ -120,6 +125,28 @@ extension JNISwift2JavaGenerator { printDestroyFunctionThunk(&printer, type) } + private func printEnumDiscriminator(_ printer: inout CodePrinter, _ type: ImportedNominalType) { + let selfPointerParam = JavaParameter(name: "selfPointer", type: .long) + printCDecl( + &printer, + javaMethodName: "$getDiscriminator", + parentName: type.swiftNominal.name, + parameters: [selfPointerParam], + resultType: .int + ) { printer in + let selfPointer = self.printSelfJLongToUnsafeMutablePointer( + &printer, + swiftParentName: type.swiftNominal.name, + selfPointerParam + ) + printer.printBraceBlock("switch (\(selfPointer).pointee)") { printer in + for (idx, enumCase) in type.cases.enumerated() { + printer.print("case .\(enumCase.name): return \(idx)") + } + } + } + } + private func printEnumCase(_ printer: inout CodePrinter, _ enumCase: ImportedEnumCase) { guard let translatedCase = self.translatedEnumCase(for: enumCase) else { return From 4529f3e70aa4ba87d1507ace9967400e82461c1e Mon Sep 17 00:00:00 2001 From: Mads Odgaard Date: Fri, 8 Aug 2025 17:28:52 +0200 Subject: [PATCH 09/21] support objects as associated values --- .../com/example/swift/VehicleEnumTest.java | 10 ++++++++ ...t2JavaGenerator+JavaBindingsPrinting.swift | 23 +++++++++---------- ...ift2JavaGenerator+SwiftThunkPrinting.swift | 11 +++++---- 3 files changed, 27 insertions(+), 17 deletions(-) diff --git a/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/VehicleEnumTest.java b/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/VehicleEnumTest.java index 0f94f8fa..a27f1f6b 100644 --- a/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/VehicleEnumTest.java +++ b/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/VehicleEnumTest.java @@ -114,6 +114,16 @@ void getAsMotorbike() { } } + @Test + void getAsTransformer() { + try (var arena = new ConfinedSwiftMemorySession()) { + Vehicle vehicle = Vehicle.transformer(Vehicle.bicycle(arena), Vehicle.car("BMW", arena), arena); + Vehicle.Transformer transformer = vehicle.getAsTransformer(arena).orElseThrow(); + assertTrue(transformer.front().getAsBicycle().isPresent()); + assertEquals("BMW", transformer.back().getAsCar().orElseThrow().arg0()); + } + } + @Test void associatedValuesAreCopied() { try (var arena = new ConfinedSwiftMemorySession()) { diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift index 6d18aca1..542b195b 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift @@ -265,16 +265,17 @@ extension JNISwift2JavaGenerator { var nativeResults = zip(translatedCase.translatedValues, translatedCase.conversions).map { value, conversion in "\(conversion.native.javaType) \(value.parameter.name)" } - if requiresSwiftArena { - nativeResults.append("SwiftArena swiftArena$") - } + printer.print("record $NativeParameters(\(nativeResults.joined(separator: ", "))) {}") + printer.println() + printer.print(#"@SuppressWarnings("unused")"#) - printer.printBraceBlock("static \(caseName) fromJNI(\(nativeResults.joined(separator: ", ")))") { printer in + let swiftArenaParameter = requiresSwiftArena ? ", SwiftArena swiftArena$" : "" + printer.printBraceBlock("\(caseName)($NativeParameters parameters\(swiftArenaParameter))") { printer in let memberValues = zip(translatedCase.translatedValues, translatedCase.conversions).map { (value, conversion) in - let result = conversion.translated.conversion.render(&printer, value.parameter.name) + let result = conversion.translated.conversion.render(&printer, "parameters.\(value.parameter.name)") return result } - printer.print("return new \(caseName)(\(memberValues.joined(separator: ", ")));") + printer.print("this(\(memberValues.joined(separator: ", ")));") } } } @@ -291,24 +292,22 @@ extension JNISwift2JavaGenerator { """ ) if hasParameters { - var arguments = ["this.$memoryAddress()"] + var arguments = ["$getAs\(caseName)(this.$memoryAddress())"] if requiresSwiftArena { arguments.append("swiftArena$") } printer.print( """ - return Optional.of($getAs\(caseName)(\(arguments.joined(separator: ", ")))); + return Optional.of(new \(caseName)(\(arguments.joined(separator: ", ")))); """ ) } else { printer.print("return Optional.of(new \(caseName)());") } } - var nativeParameters = ["long self"] - if requiresSwiftArena { - nativeParameters.append("SwiftArena swiftArena$") + if hasParameters { + printer.print("private static native \(caseName).$NativeParameters $getAs\(caseName)(long self);") } - printer.print("private static native \(caseName) $getAs\(caseName)(\(nativeParameters.joined(separator: ", ")));") printer.println() } diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift index b9f62b14..3b0c51eb 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift @@ -159,7 +159,8 @@ extension JNISwift2JavaGenerator { // Print getAsCase method if !enumCase.parameters.isEmpty { let selfParameter = JavaParameter(name: "self", type: .long) - let resultType = JavaType.class(package: javaPackage, name: "\(translatedCase.enumName).\(translatedCase.name)") + + let resultType = JavaType.class(package: javaPackage, name: "\(translatedCase.enumName).\(translatedCase.name).$NativeParameters") printCDecl( &printer, javaMethodName: "$getAs\(translatedCase.name)", @@ -176,15 +177,15 @@ extension JNISwift2JavaGenerator { parameter.name ?? "_\(idx)" } let caseNamesWithLet = caseNames.map { "let \($0)" } - let methodSignature = MethodSignature(resultType: resultType, parameterTypes: translatedCase.conversions.map(\.native.javaType)) + let methodSignature = MethodSignature(resultType: .void, parameterTypes: translatedCase.conversions.map(\.native.javaType)) // TODO: Caching of class and static method ID. printer.print( """ guard case .\(enumCase.name)(\(caseNamesWithLet.joined(separator: ", "))) = \(selfPointer).pointee else { fatalError("Expected enum case '\(enumCase.name)', but was '\\(self$.pointee)'!") } - let recordClass$ = environment.interface.FindClass(environment, "\(javaPackagePath)/\(translatedCase.enumName)$\(translatedCase.name)")! - let fromJNIID$ = environment.interface.GetStaticMethodID(environment, recordClass$, "fromJNI", "\(methodSignature.mangledName)")! + let class$ = environment.interface.FindClass(environment, "\(javaPackagePath)/\(translatedCase.enumName)$\(translatedCase.name)$$NativeParameters")! + let constructorID$ = environment.interface.GetMethodID(environment, class$, "", "\(methodSignature.mangledName)")! """ ) @@ -197,7 +198,7 @@ extension JNISwift2JavaGenerator { printer.print( """ return withVaList([\(upcallArguments.joined(separator: ", "))]) { - return environment.interface.CallStaticObjectMethodV(environment, recordClass$, fromJNIID$, $0) + return environment.interface.NewObjectV(environment, class$, constructorID$, $0) } """ ) From 2770318eb4212ffa1e68f819acaf4aae777ad93b Mon Sep 17 00:00:00 2001 From: Mads Odgaard Date: Mon, 11 Aug 2025 13:41:52 +0200 Subject: [PATCH 10/21] fix support for optional associated values --- .../Sources/MySwiftLibrary/Vehicle.swift | 14 +- .../com/example/swift/VehicleEnumTest.java | 30 ++- ...t2JavaGenerator+JavaBindingsPrinting.swift | 69 ++----- ...ISwift2JavaGenerator+JavaTranslation.swift | 178 +++++++++++++++--- ...wift2JavaGenerator+NativeTranslation.swift | 14 +- ...ift2JavaGenerator+SwiftThunkPrinting.swift | 115 +++++------ 6 files changed, 262 insertions(+), 158 deletions(-) diff --git a/Samples/JExtractJNISampleApp/Sources/MySwiftLibrary/Vehicle.swift b/Samples/JExtractJNISampleApp/Sources/MySwiftLibrary/Vehicle.swift index 8b0f2646..3c155a12 100644 --- a/Samples/JExtractJNISampleApp/Sources/MySwiftLibrary/Vehicle.swift +++ b/Samples/JExtractJNISampleApp/Sources/MySwiftLibrary/Vehicle.swift @@ -14,15 +14,15 @@ public enum Vehicle { case bicycle - case car(String) - case motorbike(String, horsePower: Int64) + case car(String, trailer: String?) + case motorbike(String, horsePower: Int64, helmets: Int32?) indirect case transformer(front: Vehicle, back: Vehicle) public init?(name: String) { switch name { case "bicycle": self = .bicycle - case "car": self = .car("Unknown") - case "motorbike": self = .motorbike("Unknown", horsePower: 0) + case "car": self = .car("Unknown", trailer: nil) + case "motorbike": self = .motorbike("Unknown", horsePower: 0, helmets: nil) default: return nil } } @@ -50,9 +50,9 @@ public enum Vehicle { public mutating func upgrade() { switch self { - case .bicycle: self = .car("Unknown") - case .car: self = .motorbike("Unknown", horsePower: 0) - case .motorbike: self = .transformer(front: .car("BMW"), back: self) + case .bicycle: self = .car("Unknown", trailer: nil) + case .car: self = .motorbike("Unknown", horsePower: 0, helmets: nil) + case .motorbike: self = .transformer(front: .car("BMW", trailer: nil), back: self) case .transformer: break } } diff --git a/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/VehicleEnumTest.java b/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/VehicleEnumTest.java index a27f1f6b..c418eb91 100644 --- a/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/VehicleEnumTest.java +++ b/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/VehicleEnumTest.java @@ -18,6 +18,7 @@ import org.swift.swiftkit.core.ConfinedSwiftMemorySession; import java.util.Optional; +import java.util.OptionalInt; import static org.junit.jupiter.api.Assertions.*; @@ -33,7 +34,7 @@ void bicycle() { @Test void car() { try (var arena = new ConfinedSwiftMemorySession()) { - Vehicle vehicle = Vehicle.car("Porsche 911", arena); + Vehicle vehicle = Vehicle.car("Porsche 911", Optional.empty(), arena); assertNotNull(vehicle); } } @@ -41,7 +42,7 @@ void car() { @Test void motorbike() { try (var arena = new ConfinedSwiftMemorySession()) { - Vehicle vehicle = Vehicle.motorbike("Yamaha", 750, arena); + Vehicle vehicle = Vehicle.motorbike("Yamaha", 750, OptionalInt.empty(), arena); assertNotNull(vehicle); } } @@ -68,7 +69,7 @@ void nameProperty() { void isFasterThan() { try (var arena = new ConfinedSwiftMemorySession()) { Vehicle bicycle = Vehicle.bicycle(arena); - Vehicle car = Vehicle.car("Porsche 911", arena); + Vehicle car = Vehicle.car("Porsche 911", Optional.empty(), arena); assertFalse(bicycle.isFasterThan(car)); assertTrue(car.isFasterThan(bicycle)); } @@ -98,26 +99,35 @@ void getAsBicycle() { @Test void getAsCar() { try (var arena = new ConfinedSwiftMemorySession()) { - Vehicle vehicle = Vehicle.car("BMW", arena); + Vehicle vehicle = Vehicle.car("BMW", Optional.empty(), arena); Vehicle.Car car = vehicle.getAsCar().orElseThrow(); assertEquals("BMW", car.arg0()); + + vehicle = Vehicle.car("BMW", Optional.of("Long trailer"), arena); + car = vehicle.getAsCar().orElseThrow(); + assertEquals("Long trailer", car.trailer().orElseThrow()); } } @Test void getAsMotorbike() { try (var arena = new ConfinedSwiftMemorySession()) { - Vehicle vehicle = Vehicle.motorbike("Yamaha", 750, arena); + Vehicle vehicle = Vehicle.motorbike("Yamaha", 750, OptionalInt.empty(), arena); Vehicle.Motorbike motorbike = vehicle.getAsMotorbike().orElseThrow(); assertEquals("Yamaha", motorbike.arg0()); assertEquals(750, motorbike.horsePower()); + assertEquals(OptionalInt.empty(), motorbike.helmets()); + + vehicle = Vehicle.motorbike("Yamaha", 750, OptionalInt.of(2), arena); + motorbike = vehicle.getAsMotorbike().orElseThrow(); + assertEquals(OptionalInt.of(2), motorbike.helmets()); } } @Test void getAsTransformer() { try (var arena = new ConfinedSwiftMemorySession()) { - Vehicle vehicle = Vehicle.transformer(Vehicle.bicycle(arena), Vehicle.car("BMW", arena), arena); + Vehicle vehicle = Vehicle.transformer(Vehicle.bicycle(arena), Vehicle.car("BMW", Optional.empty(), arena), arena); Vehicle.Transformer transformer = vehicle.getAsTransformer(arena).orElseThrow(); assertTrue(transformer.front().getAsBicycle().isPresent()); assertEquals("BMW", transformer.back().getAsCar().orElseThrow().arg0()); @@ -127,7 +137,7 @@ void getAsTransformer() { @Test void associatedValuesAreCopied() { try (var arena = new ConfinedSwiftMemorySession()) { - Vehicle vehicle = Vehicle.car("BMW", arena); + Vehicle vehicle = Vehicle.car("BMW", Optional.empty(), arena); Vehicle.Car car = vehicle.getAsCar().orElseThrow(); assertEquals("BMW", car.arg0()); vehicle.upgrade(); @@ -142,8 +152,8 @@ void associatedValuesAreCopied() { void getDiscriminator() { try (var arena = new ConfinedSwiftMemorySession()) { assertEquals(Vehicle.Discriminator.BICYCLE, Vehicle.bicycle(arena).getDiscriminator()); - assertEquals(Vehicle.Discriminator.CAR, Vehicle.car("BMW", arena).getDiscriminator()); - assertEquals(Vehicle.Discriminator.MOTORBIKE, Vehicle.motorbike("Yamaha", 750, arena).getDiscriminator()); + assertEquals(Vehicle.Discriminator.CAR, Vehicle.car("BMW", Optional.empty(), arena).getDiscriminator()); + assertEquals(Vehicle.Discriminator.MOTORBIKE, Vehicle.motorbike("Yamaha", 750, OptionalInt.empty(), arena).getDiscriminator()); assertEquals(Vehicle.Discriminator.TRANSFORMER, Vehicle.transformer(Vehicle.bicycle(arena), Vehicle.bicycle(arena), arena).getDiscriminator()); } } @@ -160,7 +170,7 @@ void getCase() { @Test void switchGetCase() { try (var arena = new ConfinedSwiftMemorySession()) { - Vehicle vehicle = Vehicle.car("BMW", arena); + Vehicle vehicle = Vehicle.car("BMW", Optional.empty(), arena); switch (vehicle.getCase(arena)) { case Vehicle.Bicycle b: fail("Was bicycle"); diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift index 542b195b..4f0dd757 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift @@ -256,59 +256,17 @@ extension JNISwift2JavaGenerator { } let caseName = enumCase.name.firstCharacterUppercased - let hasParameters = !enumCase.parameters.isEmpty - let requiresSwiftArena = translatedCase.requiresSwiftArena // Print record printer.printBraceBlock("public record \(caseName)(\(members.joined(separator: ", "))) implements Case") { printer in - if hasParameters { - var nativeResults = zip(translatedCase.translatedValues, translatedCase.conversions).map { value, conversion in - "\(conversion.native.javaType) \(value.parameter.name)" - } - printer.print("record $NativeParameters(\(nativeResults.joined(separator: ", "))) {}") - printer.println() - - printer.print(#"@SuppressWarnings("unused")"#) - let swiftArenaParameter = requiresSwiftArena ? ", SwiftArena swiftArena$" : "" - printer.printBraceBlock("\(caseName)($NativeParameters parameters\(swiftArenaParameter))") { printer in - let memberValues = zip(translatedCase.translatedValues, translatedCase.conversions).map { (value, conversion) in - let result = conversion.translated.conversion.render(&printer, "parameters.\(value.parameter.name)") - return result - } - printer.print("this(\(memberValues.joined(separator: ", ")));") - } + let nativeParameters = zip(translatedCase.translatedValues, translatedCase.parameterConversions).flatMap { value, conversion in + ["\(conversion.native.javaType) \(value.parameter.name)"] } - } - // TODO: Optimize when all values can just be passed directly, instead of going through "middle type"? - - // Print method to get enum as case - printer.printBraceBlock("public Optional<\(caseName)> getAs\(caseName)(\(requiresSwiftArena ? "SwiftArena swiftArena$" : ""))") { printer in - printer.print( - """ - if (this.getDiscriminator() != Discriminator.\(caseName.uppercased())) { - return Optional.empty(); - } - """ - ) - if hasParameters { - var arguments = ["$getAs\(caseName)(this.$memoryAddress())"] - if requiresSwiftArena { - arguments.append("swiftArena$") - } - printer.print( - """ - return Optional.of(new \(caseName)(\(arguments.joined(separator: ", ")))); - """ - ) - } else { - printer.print("return Optional.of(new \(caseName)());") - } - } - if hasParameters { - printer.print("private static native \(caseName).$NativeParameters $getAs\(caseName)(long self);") + printer.print("record $NativeParameters(\(nativeParameters.joined(separator: ", "))) {}") } + self.printJavaBindingWrapperMethod(&printer, translatedCase.getAsCaseFunction) printer.println() } } @@ -326,6 +284,7 @@ extension JNISwift2JavaGenerator { printJavaBindingWrapperHelperClass(&printer, decl) + printDeclDocumentation(&printer, decl) printJavaBindingWrapperMethod(&printer, decl) } @@ -373,9 +332,12 @@ extension JNISwift2JavaGenerator { guard let translatedDecl = translatedDecl(for: decl) else { fatalError("Decl was not translated, \(decl)") } + printJavaBindingWrapperMethod(&printer, translatedDecl) + } + private func printJavaBindingWrapperMethod(_ printer: inout CodePrinter, _ translatedDecl: TranslatedFunctionDecl) { var modifiers = ["public"] - if decl.isStatic || decl.isInitializer || !decl.hasParent { + if translatedDecl.isStatic { modifiers.append("static") } @@ -385,7 +347,7 @@ extension JNISwift2JavaGenerator { if translatedSignature.requiresSwiftArena { parameters.append("SwiftArena swiftArena$") } - let throwsClause = decl.isThrowing ? " throws Exception" : "" + let throwsClause = translatedDecl.isThrowing ? " throws Exception" : "" var annotationsStr = translatedSignature.annotations.map({ $0.render() }).joined(separator: "\n") if !annotationsStr.isEmpty { annotationsStr += "\n" } @@ -393,18 +355,16 @@ extension JNISwift2JavaGenerator { let modifiersStr = modifiers.joined(separator: " ") let parametersStr = parameters.joined(separator: ", ") - printDeclDocumentation(&printer, decl) printer.printBraceBlock( "\(annotationsStr)\(modifiersStr) \(resultType) \(translatedDecl.name)(\(parametersStr))\(throwsClause)" ) { printer in - printDowncall(&printer, decl) + printDowncall(&printer, translatedDecl) } - printNativeFunction(&printer, decl) + printNativeFunction(&printer, translatedDecl) } - private func printNativeFunction(_ printer: inout CodePrinter, _ decl: ImportedFunc) { - let translatedDecl = translatedDecl(for: decl)! // Will always call with valid decl + private func printNativeFunction(_ printer: inout CodePrinter, _ translatedDecl: TranslatedFunctionDecl) { let nativeSignature = translatedDecl.nativeFunctionSignature let resultType = nativeSignature.result.javaType var parameters = nativeSignature.parameters.flatMap(\.parameters) @@ -422,9 +382,8 @@ extension JNISwift2JavaGenerator { private func printDowncall( _ printer: inout CodePrinter, - _ decl: ImportedFunc + _ translatedDecl: TranslatedFunctionDecl ) { - let translatedDecl = translatedDecl(for: decl)! // We will only call this method if we can translate the decl. let translatedFunctionSignature = translatedDecl.translatedFunctionSignature // Regular parameters. diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift index 467c306b..7b14d48a 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift @@ -89,17 +89,70 @@ extension JNISwift2JavaGenerator { ) let conversions = try enumCase.parameters.enumerated().map { idx, parameter in + let resultName = parameter.name ?? "arg\(idx)" let result = SwiftResult(convention: .direct, type: parameter.type) - let translatedResult = try self.translate(swiftResult: result) - let nativeResult = try nativeTranslation.translate(swiftResult: result, resultName: parameter.name ?? "arg\(idx)") - return (translatedResult, nativeResult) + var translatedResult = try self.translate(swiftResult: result, resultName: resultName) + translatedResult.conversion = .replacingPlaceholder(translatedResult.conversion, placeholder: "$nativeParameters.\(resultName)") + let nativeResult = try nativeTranslation.translate(swiftResult: result, resultName: resultName) + return (translated: translatedResult, native: nativeResult) } + let caseName = enumCase.name.firstCharacterUppercased + let enumName = enumCase.enumType.nominalTypeDecl.name + let nativeParametersType = JavaType.class(package: nil, name: "\(caseName).$NativeParameters") + let getAsCaseName = "getAs\(caseName)" + // If the case has no parameters, we can skip the native call. + let constructRecordConversion = JavaNativeConversionStep.method(.constant("Optional"), function: "of", arguments: [ + .constructJavaClass( + .commaSeparated(conversions.map(\.translated.conversion)), + .class(package: nil,name: caseName) + ) + ]) + let getAsCaseFunction = TranslatedFunctionDecl( + name: getAsCaseName, + isStatic: false, + isThrowing: false, + nativeFunctionName: "$\(getAsCaseName)", + parentName: enumName, + functionTypes: [], + translatedFunctionSignature: TranslatedFunctionSignature( + selfParameter: TranslatedParameter( + parameter: JavaParameter(name: "self", type: .long), + conversion: .aggregate( + [ + .ifStatement(.constant("getDiscriminator() != Discriminator.\(caseName.uppercased())"), thenExp: .constant("return Optional.empty();")), + .valueMemoryAddress(.placeholder) + ] + ) + ), + parameters: [], + resultType: TranslatedResult( + javaType: .class(package: nil, name: "Optional<\(caseName)>"), + outParameters: conversions.flatMap(\.translated.outParameters), + conversion: enumCase.parameters.isEmpty ? constructRecordConversion : .aggregate(variable: ("$nativeParameters", nativeParametersType), [constructRecordConversion]) + ) + ), + nativeFunctionSignature: NativeFunctionSignature( + selfParameter: NativeParameter( + parameters: [JavaParameter(name: "self", type: .long)], + conversion: .extractSwiftValue(.placeholder, swiftType: .nominal(enumCase.enumType), allowNil: false) + ), + parameters: [], + result: NativeResult( + javaType: nativeParametersType, + conversion: .placeholder, + outParameters: conversions.flatMap(\.native.outParameters) + ) + ) + ) + return TranslatedEnumCase( name: enumCase.name.firstCharacterUppercased, enumName: enumCase.enumType.nominalTypeDecl.name, + original: enumCase, translatedValues: translatedValues, - conversions: conversions + parameterConversions: conversions, + getAsCaseFunction: getAsCaseFunction ) } @@ -154,6 +207,8 @@ extension JNISwift2JavaGenerator { return TranslatedFunctionDecl( name: javaName, + isStatic: decl.isStatic || !decl.hasParent || decl.isInitializer, + isThrowing: decl.isThrowing, nativeFunctionName: "$\(javaName)", parentName: parentName, functionTypes: funcTypes, @@ -415,7 +470,7 @@ extension JNISwift2JavaGenerator { } } - func translate(swiftResult: SwiftResult) throws -> TranslatedResult { + func translate(swiftResult: SwiftResult, resultName: String = "result") throws -> TranslatedResult { let swiftType = swiftResult.type // If the result type should cause any annotations on the method, include them here. @@ -429,7 +484,7 @@ extension JNISwift2JavaGenerator { guard let genericArgs = nominalType.genericArguments, genericArgs.count == 1 else { throw JavaTranslationError.unsupportedSwiftType(swiftType) } - return try translateOptionalResult(wrappedType: genericArgs[0]) + return try translateOptionalResult(wrappedType: genericArgs[0], resultName: resultName) default: guard let javaType = JNIJavaTypeTranslator.translate(knownType: knownType, config: self.config) else { @@ -462,7 +517,7 @@ extension JNISwift2JavaGenerator { return TranslatedResult(javaType: .void, outParameters: [], conversion: .placeholder) case .optional(let wrapped): - return try translateOptionalResult(wrappedType: wrapped) + return try translateOptionalResult(wrappedType: wrapped, resultName: resultName) case .metatype, .tuple, .function, .existential, .opaque, .genericParameter: throw JavaTranslationError.unsupportedSwiftType(swiftType) @@ -470,8 +525,11 @@ extension JNISwift2JavaGenerator { } func translateOptionalResult( - wrappedType swiftType: SwiftType + wrappedType swiftType: SwiftType, + resultName: String = "result" ) throws -> TranslatedResult { + let discriminatorName = "\(resultName)$_discriminator$" + let parameterAnnotations: [JavaAnnotation] = getTypeAnnotations(swiftType: swiftType, config: config) switch swiftType { @@ -509,13 +567,14 @@ extension JNISwift2JavaGenerator { javaType: .class(package: nil, name: returnType), annotations: parameterAnnotations, outParameters: [ - OutParameter(name: "result_discriminator$", type: .array(.byte), allocation: .newArray(.byte, size: 1)) + OutParameter(name: discriminatorName, type: .array(.byte), allocation: .newArray(.byte, size: 1)) ], conversion: .toOptionalFromIndirectReturn( - discriminatorName: "result_discriminator$", + discriminatorName: .combinedName(component: "discriminator$"), optionalClass: optionalClass, javaType: javaType, - toValue: .placeholder + toValue: .placeholder, + resultName: resultName ) ) } @@ -531,13 +590,14 @@ extension JNISwift2JavaGenerator { javaType: returnType, annotations: parameterAnnotations, outParameters: [ - OutParameter(name: "result_discriminator$", type: .array(.byte), allocation: .newArray(.byte, size: 1)) + OutParameter(name: discriminatorName, type: .array(.byte), allocation: .newArray(.byte, size: 1)) ], conversion: .toOptionalFromIndirectReturn( - discriminatorName: "result_discriminator$", + discriminatorName: .combinedName(component: "discriminator$"), optionalClass: "Optional", javaType: .long, - toValue: .constructSwiftValue(.placeholder, .class(package: nil, name: nominalTypeName)) + toValue: .constructSwiftValue(.placeholder, .class(package: nil, name: nominalTypeName)), + resultName: resultName ) ) @@ -554,13 +614,20 @@ extension JNISwift2JavaGenerator { /// The name of the translated enum let enumName: String + /// The oringinal enum case. + let original: ImportedEnumCase + /// A list of the translated associated values let translatedValues: [TranslatedParameter] - let conversions: [(translated: TranslatedResult, native: NativeResult)] + /// A list of parameter conversions + let parameterConversions: [(translated: TranslatedResult, native: NativeResult)] + + let getAsCaseFunction: TranslatedFunctionDecl + /// Returns whether the parameters require an arena var requiresSwiftArena: Bool { - conversions.contains(where: \.translated.conversion.requiresSwiftArena) + parameterConversions.contains(where: \.translated.conversion.requiresSwiftArena) } } @@ -568,6 +635,10 @@ extension JNISwift2JavaGenerator { /// Java function name let name: String + let isStatic: Bool + + let isThrowing: Bool + /// The name of the native function let nativeFunctionName: String @@ -621,7 +692,7 @@ extension JNISwift2JavaGenerator { let outParameters: [OutParameter] /// Represents how to convert the Java native result into a user-facing result. - let conversion: JavaNativeConversionStep + var conversion: JavaNativeConversionStep } struct OutParameter { @@ -662,6 +733,9 @@ extension JNISwift2JavaGenerator { case constant(String) + /// `input_component` + case combinedName(component: String) + // Convert the results of the inner steps to a comma separated list. indirect case commaSeparated([JavaNativeConversionStep]) @@ -671,6 +745,9 @@ extension JNISwift2JavaGenerator { /// Call `new \(Type)(\(placeholder), swiftArena$)` indirect case constructSwiftValue(JavaNativeConversionStep, JavaType) + /// Call `new \(Type)(\(placeholder))` + indirect case constructJavaClass(JavaNativeConversionStep, JavaType) + indirect case call(JavaNativeConversionStep, function: String) indirect case method(JavaNativeConversionStep, function: String, arguments: [JavaNativeConversionStep] = []) @@ -686,18 +763,18 @@ extension JNISwift2JavaGenerator { indirect case subscriptOf(JavaNativeConversionStep, arguments: [JavaNativeConversionStep]) static func toOptionalFromIndirectReturn( - discriminatorName: String, + discriminatorName: JavaNativeConversionStep, optionalClass: String, javaType: JavaType, - toValue valueConversion: JavaNativeConversionStep + toValue valueConversion: JavaNativeConversionStep, + resultName: String ) -> JavaNativeConversionStep { .aggregate( - name: "result$", - type: javaType, + variable: (name: "\(resultName)$", type: javaType), [ .ternary( .equals( - .subscriptOf(.constant(discriminatorName), arguments: [.constant("0")]), + .subscriptOf(discriminatorName, arguments: [.constant("0")]), .constant("1") ), thenExp: .method(.constant(optionalClass), function: "of", arguments: [valueConversion]), @@ -708,7 +785,12 @@ extension JNISwift2JavaGenerator { } /// Perform multiple conversions using the same input. - case aggregate(name: String, type: JavaType, [JavaNativeConversionStep]) + case aggregate(variable: (name: String, type: JavaType)? = nil, [JavaNativeConversionStep]) + + indirect case ifStatement(JavaNativeConversionStep, thenExp: JavaNativeConversionStep, elseExp: JavaNativeConversionStep? = nil) + + /// Access a member of the value + indirect case replacingPlaceholder(JavaNativeConversionStep, placeholder: String) /// Returns the conversion string applied to the placeholder. func render(_ printer: inout CodePrinter, _ placeholder: String) -> String { @@ -721,6 +803,9 @@ extension JNISwift2JavaGenerator { case .constant(let value): return value + case .combinedName(let component): + return "\(placeholder)_\(component)" + case .commaSeparated(let list): return list.map({ $0.render(&printer, placeholder)}).joined(separator: ", ") @@ -731,6 +816,10 @@ extension JNISwift2JavaGenerator { let inner = inner.render(&printer, placeholder) return "new \(javaType.className!)(\(inner), swiftArena$)" + case .constructJavaClass(let inner, let javaType): + let inner = inner.render(&printer, placeholder) + return "new \(javaType.className!)(\(inner))" + case .call(let inner, let function): let inner = inner.render(&printer, placeholder) return "\(function)(\(inner))" @@ -778,25 +867,50 @@ extension JNISwift2JavaGenerator { let arguments = arguments.map { $0.render(&printer, placeholder) } return "\(inner)[\(arguments.joined(separator: ", "))]" - case .aggregate(let name, let type, let steps): + case .aggregate(let variable, let steps): precondition(!steps.isEmpty, "Aggregate must contain steps") - printer.print("\(type) \(name) = \(placeholder);") + let toExplode: String + if let variable { + printer.print("\(variable.type) \(variable.name) = \(placeholder);") + toExplode = variable.name + } else { + toExplode = placeholder + } let steps = steps.map { - $0.render(&printer, name) + $0.render(&printer, toExplode) } return steps.last! + + case .ifStatement(let cond, let thenExp, let elseExp): + let cond = cond.render(&printer, placeholder) + printer.printBraceBlock("if (\(cond))") { printer in + printer.print(thenExp.render(&printer, placeholder)) + } + if let elseExp { + printer.printBraceBlock("else") { printer in + printer.print(elseExp.render(&printer, placeholder)) + } + } + + return "" + + case .replacingPlaceholder(let inner, let placeholder): + return inner.render(&printer, placeholder) } } /// Whether the conversion uses SwiftArena. var requiresSwiftArena: Bool { switch self { - case .placeholder, .constant, .isOptionalPresent: + case .placeholder, .constant, .isOptionalPresent, .combinedName: return false case .constructSwiftValue: return true + case .constructJavaClass(let inner, _): + return inner.requiresSwiftArena + case .valueMemoryAddress(let inner): return inner.requiresSwiftArena @@ -818,11 +932,17 @@ extension JNISwift2JavaGenerator { case .subscriptOf(let inner, _): return inner.requiresSwiftArena - case .aggregate(_, _, let steps): + case .aggregate(_, let steps): return steps.contains(where: \.requiresSwiftArena) + case .ifStatement(let cond, let thenExp, let elseExp): + return cond.requiresSwiftArena || thenExp.requiresSwiftArena || (elseExp?.requiresSwiftArena ?? false) + case .call(let inner, _): - return inner.requiresSwiftArena + return inner.requiresSwiftArena + + case .replacingPlaceholder(let inner, _): + return inner.requiresSwiftArena } } } diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift index 05f61c4c..85ae625c 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift @@ -248,8 +248,11 @@ extension JNISwift2JavaGenerator { } func translateOptionalResult( - wrappedType swiftType: SwiftType + wrappedType swiftType: SwiftType, + resultName: String = "result" ) throws -> NativeResult { + let discriminatorName = "\(resultName)_discriminator$" + switch swiftType { case .nominal(let nominalType): if let knownType = nominalType.nominalTypeDecl.knownTypeKind { @@ -275,7 +278,6 @@ extension JNISwift2JavaGenerator { ) } else { // Use indirect byte array to store discriminator - let discriminatorName = "result_discriminator$" return NativeResult( javaType: javaType, @@ -301,8 +303,6 @@ extension JNISwift2JavaGenerator { } // Assume JExtract imported class - let discriminatorName = "result_discriminator$" - return NativeResult( javaType: .long, conversion: .optionalRaisingIndirectReturn( @@ -467,6 +467,9 @@ extension JNISwift2JavaGenerator { case constant(String) + /// `input_component` + case combinedName(component: String) + /// `value.getJNIValue(in:)` indirect case getJNIValue(NativeSwiftConversionStep) @@ -521,6 +524,9 @@ extension JNISwift2JavaGenerator { case .constant(let value): return value + case .combinedName(let component): + return "\(placeholder)_\(component)" + case .getJNIValue(let inner): let inner = inner.render(&printer, placeholder) return "\(inner).getJNIValue(in: environment!)" diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift index 3b0c51eb..fb9f2a6e 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift @@ -157,52 +157,48 @@ extension JNISwift2JavaGenerator { printer.println() // Print getAsCase method - if !enumCase.parameters.isEmpty { - let selfParameter = JavaParameter(name: "self", type: .long) + if !translatedCase.translatedValues.isEmpty { + printEnumGetAsCaseThunk(&printer, translatedCase) + } + } - let resultType = JavaType.class(package: javaPackage, name: "\(translatedCase.enumName).\(translatedCase.name).$NativeParameters") - printCDecl( - &printer, - javaMethodName: "$getAs\(translatedCase.name)", - parentName: "\(translatedCase.enumName)", - parameters: [selfParameter], - resultType: resultType - ) { printer in - let selfPointer = self.printSelfJLongToUnsafeMutablePointer( - &printer, - swiftParentName: enumCase.enumType.nominalTypeDecl.name, - selfParameter - ) - let caseNames = enumCase.parameters.enumerated().map { idx, parameter in - parameter.name ?? "_\(idx)" - } - let caseNamesWithLet = caseNames.map { "let \($0)" } - let methodSignature = MethodSignature(resultType: .void, parameterTypes: translatedCase.conversions.map(\.native.javaType)) - // TODO: Caching of class and static method ID. - printer.print( + private func printEnumGetAsCaseThunk( + _ printer: inout CodePrinter, + _ enumCase: TranslatedEnumCase + ) { + printCDecl( + &printer, + enumCase.getAsCaseFunction + ) { printer in + let selfPointer = enumCase.getAsCaseFunction.nativeFunctionSignature.selfParameter!.conversion.render(&printer, "self") + let caseNames = enumCase.original.parameters.enumerated().map { idx, parameter in + parameter.name ?? "_\(idx)" + } + let caseNamesWithLet = caseNames.map { "let \($0)" } + let methodSignature = MethodSignature(resultType: .void, parameterTypes: enumCase.parameterConversions.map(\.native.javaType)) + // TODO: Caching of class and static method ID. + printer.print( """ - guard case .\(enumCase.name)(\(caseNamesWithLet.joined(separator: ", "))) = \(selfPointer).pointee else { - fatalError("Expected enum case '\(enumCase.name)', but was '\\(self$.pointee)'!") + guard case .\(enumCase.original.name)(\(caseNamesWithLet.joined(separator: ", "))) = \(selfPointer).pointee else { + fatalError("Expected enum case '\(enumCase.original.name)', but was '\\(\(selfPointer).pointee)'!") } - let class$ = environment.interface.FindClass(environment, "\(javaPackagePath)/\(translatedCase.enumName)$\(translatedCase.name)$$NativeParameters")! + let class$ = environment.interface.FindClass(environment, "\(javaPackagePath)/\(enumCase.enumName)$\(enumCase.name)$$NativeParameters")! let constructorID$ = environment.interface.GetMethodID(environment, class$, "", "\(methodSignature.mangledName)")! """ - ) - - let upcallArguments = zip(translatedCase.conversions, caseNames).map { conversion, caseName in - // '0' is treated the same as a null pointer. - let nullConversion = !conversion.native.javaType.isPrimitive ? " ?? 0" : "" - let result = conversion.native.conversion.render(&printer, caseName) - return "\(result)\(nullConversion)" - } - printer.print( - """ - return withVaList([\(upcallArguments.joined(separator: ", "))]) { - return environment.interface.NewObjectV(environment, class$, constructorID$, $0) - } - """ - ) + ) + let upcallArguments = zip(enumCase.parameterConversions, caseNames).map { conversion, caseName in + // '0' is treated the same as a null pointer. + let nullConversion = !conversion.native.javaType.isPrimitive ? " ?? 0" : "" + let result = conversion.native.conversion.render(&printer, caseName) + return "\(result)\(nullConversion)" } + printer.print( + """ + return withVaList([\(upcallArguments.joined(separator: ", "))]) { + return environment.interface.NewObjectV(environment, class$, constructorID$, $0) + } + """ + ) } } @@ -215,21 +211,9 @@ extension JNISwift2JavaGenerator { return } - let nativeSignature = translatedDecl.nativeFunctionSignature - var parameters = nativeSignature.parameters.flatMap(\.parameters) - - if let selfParameter = nativeSignature.selfParameter { - parameters += selfParameter.parameters - } - - parameters += nativeSignature.result.outParameters - printCDecl( &printer, - javaMethodName: translatedDecl.nativeFunctionName, - parentName: translatedDecl.parentName, - parameters: parameters, - resultType: nativeSignature.result.javaType + translatedDecl ) { printer in self.printFunctionDowncall(&printer, decl) } @@ -331,6 +315,31 @@ extension JNISwift2JavaGenerator { } } + private func printCDecl( + _ printer: inout CodePrinter, + _ translatedDecl: TranslatedFunctionDecl, + _ body: (inout CodePrinter) -> Void + ) { + let nativeSignature = translatedDecl.nativeFunctionSignature + var parameters = nativeSignature.parameters.flatMap(\.parameters) + + if let selfParameter = nativeSignature.selfParameter { + parameters += selfParameter.parameters + } + + parameters += nativeSignature.result.outParameters + + printCDecl( + &printer, + javaMethodName: translatedDecl.nativeFunctionName, + parentName: translatedDecl.parentName, + parameters: parameters, + resultType: nativeSignature.result.javaType + ) { printer in + body(&printer) + } + } + private func printCDecl( _ printer: inout CodePrinter, javaMethodName: String, From e492e4523dba655e79fb09304de9570581afe847 Mon Sep 17 00:00:00 2001 From: Mads Odgaard Date: Tue, 12 Aug 2025 11:37:52 +0200 Subject: [PATCH 11/21] add caching --- .../JExtractSwiftPlugin.swift | 7 ++ .../java/com/example/swift/EnumBenchmark.java | 5 +- .../Convenience/String+Extensions.swift | 8 +++ Sources/JExtractSwiftLib/JNI/JNICaching.swift | 19 +++++ ...t2JavaGenerator+JavaBindingsPrinting.swift | 26 ++++++- ...ift2JavaGenerator+SwiftThunkPrinting.swift | 57 ++++++++++++++- .../JNI/JNISwift2JavaGenerator.swift | 1 + Sources/JavaKit/Helpers/_JNICache.swift | 71 +++++++++++++++++++ 8 files changed, 187 insertions(+), 7 deletions(-) create mode 100644 Sources/JExtractSwiftLib/JNI/JNICaching.swift create mode 100644 Sources/JavaKit/Helpers/_JNICache.swift diff --git a/Plugins/JExtractSwiftPlugin/JExtractSwiftPlugin.swift b/Plugins/JExtractSwiftPlugin/JExtractSwiftPlugin.swift index 2778cebe..43214d5e 100644 --- a/Plugins/JExtractSwiftPlugin/JExtractSwiftPlugin.swift +++ b/Plugins/JExtractSwiftPlugin/JExtractSwiftPlugin.swift @@ -114,6 +114,13 @@ struct JExtractSwiftBuildToolPlugin: SwiftJavaPluginProtocol, BuildToolPlugin { outputSwiftDirectory.appending(path: "\(sourceModule.name)Module+SwiftJava.swift") ] + // Append any JNI cache files + if configuration?.mode == .jni { + outputSwiftFiles += [ + outputSwiftDirectory.appending(path: "\(sourceModule.name)+JNICaches.swift") + ] + } + // If the module uses 'Data' type, the thunk file is emitted as if 'Data' is declared // in that module. Declare the thunk file as the output. // FIXME: Make this conditional. diff --git a/Samples/JExtractJNISampleApp/src/jmh/java/com/example/swift/EnumBenchmark.java b/Samples/JExtractJNISampleApp/src/jmh/java/com/example/swift/EnumBenchmark.java index ad10d242..b15dfd78 100644 --- a/Samples/JExtractJNISampleApp/src/jmh/java/com/example/swift/EnumBenchmark.java +++ b/Samples/JExtractJNISampleApp/src/jmh/java/com/example/swift/EnumBenchmark.java @@ -22,6 +22,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Optional; +import java.util.OptionalInt; import java.util.concurrent.TimeUnit; @BenchmarkMode(Mode.AverageTime) @@ -39,7 +40,7 @@ public static class BenchmarkState { @Setup(Level.Trial) public void beforeAll() { arena = new ConfinedSwiftMemorySession(); - vehicle = Vehicle.motorbike("Yamaha", 900, arena); + vehicle = Vehicle.motorbike("Yamaha", 900, OptionalInt.empty(), arena); } @TearDown(Level.Trial) @@ -49,7 +50,7 @@ public void afterAll() { } @Benchmark - public Vehicle.Motorbike java_copy(BenchmarkState state, Blackhole bh) { + public Vehicle.Motorbike getAssociatedValues(BenchmarkState state, Blackhole bh) { Vehicle.Motorbike motorbike = state.vehicle.getAsMotorbike().orElseThrow(); bh.consume(motorbike.arg0()); bh.consume(motorbike.horsePower()); diff --git a/Sources/JExtractSwiftLib/Convenience/String+Extensions.swift b/Sources/JExtractSwiftLib/Convenience/String+Extensions.swift index 0f5cfeac..25c46366 100644 --- a/Sources/JExtractSwiftLib/Convenience/String+Extensions.swift +++ b/Sources/JExtractSwiftLib/Convenience/String+Extensions.swift @@ -24,6 +24,14 @@ extension String { return "\(f.uppercased())\(String(dropFirst()))" } + var firstCharacterLowercased: String { + guard let f = first else { + return self + } + + return "\(f.lowercased())\(String(dropFirst()))" + } + /// Returns whether the string is of the format `isX` var hasJavaBooleanNamingConvention: Bool { guard self.hasPrefix("is"), self.count > 2 else { diff --git a/Sources/JExtractSwiftLib/JNI/JNICaching.swift b/Sources/JExtractSwiftLib/JNI/JNICaching.swift new file mode 100644 index 00000000..6d314891 --- /dev/null +++ b/Sources/JExtractSwiftLib/JNI/JNICaching.swift @@ -0,0 +1,19 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +enum JNICaching { + static func cacheName(for enumCase: ImportedEnumCase) -> String { + "\(enumCase.enumType.nominalTypeDecl.name.firstCharacterLowercased)\(enumCase.name.firstCharacterUppercased)Cache" + } +} diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift index 4f0dd757..b2dde9d3 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift @@ -259,6 +259,17 @@ extension JNISwift2JavaGenerator { // Print record printer.printBraceBlock("public record \(caseName)(\(members.joined(separator: ", "))) implements Case") { printer in + printer.printBraceBlock("static class $JNI") { printer in + printer.print("private static native void $nativeInit();") + } + + // Used to ensure static initializer has been calling to trigger caching. + printer.print("static void $ensureInitialized() {}") + + printer.printBraceBlock("static") { printer in + printer.print("$JNI.$nativeInit();") + } + let nativeParameters = zip(translatedCase.translatedValues, translatedCase.parameterConversions).flatMap { value, conversion in ["\(conversion.native.javaType) \(value.parameter.name)"] } @@ -266,7 +277,13 @@ extension JNISwift2JavaGenerator { printer.print("record $NativeParameters(\(nativeParameters.joined(separator: ", "))) {}") } - self.printJavaBindingWrapperMethod(&printer, translatedCase.getAsCaseFunction) + self.printJavaBindingWrapperMethod( + &printer, + translatedCase.getAsCaseFunction, + prefix: { printer in + printer.print("\(caseName).$ensureInitialized();") + } + ) printer.println() } } @@ -335,7 +352,11 @@ extension JNISwift2JavaGenerator { printJavaBindingWrapperMethod(&printer, translatedDecl) } - private func printJavaBindingWrapperMethod(_ printer: inout CodePrinter, _ translatedDecl: TranslatedFunctionDecl) { + private func printJavaBindingWrapperMethod( + _ printer: inout CodePrinter, + _ translatedDecl: TranslatedFunctionDecl, + prefix: (inout CodePrinter) -> Void = { _ in } + ) { var modifiers = ["public"] if translatedDecl.isStatic { modifiers.append("static") @@ -358,6 +379,7 @@ extension JNISwift2JavaGenerator { printer.printBraceBlock( "\(annotationsStr)\(modifiersStr) \(resultType) \(translatedDecl.name)(\(parametersStr))\(throwsClause)" ) { printer in + prefix(&printer) printDowncall(&printer, translatedDecl) } diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift index fb9f2a6e..bcc06702 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift @@ -56,6 +56,8 @@ extension JNISwift2JavaGenerator { self.expectedOutputSwiftFiles.remove(moduleFilename) } + try self.writeJNICacheSource(&printer) + for (_, ty) in self.analysis.importedTypes.sorted(by: { (lhs, rhs) in lhs.key < rhs.key }) { let fileNameBase = "\(ty.swiftNominal.qualifiedName)+SwiftJava" let filename = "\(fileNameBase).swift" @@ -80,6 +82,34 @@ extension JNISwift2JavaGenerator { } } + private func writeJNICacheSource(_ printer: inout CodePrinter) throws { + printer.print("import JavaKit") + printer.println() + printer.printBraceBlock("enum JNICaches") { printer in + let enumCases = self.analysis.importedTypes.values.filter { $0.swiftNominal.kind == .enum }.flatMap(\.cases) + for enumCase in enumCases { + printer.print("static var \(JNICaching.cacheName(for: enumCase)): _JNICache!") + } + printer.println() + printer.printBraceBlock("func cleanup()") { printer in + for enumCase in enumCases { + printer.print("JNICaches.\(JNICaching.cacheName(for: enumCase)) = nil") + } + } + } + + let fileName = "\(self.swiftModuleName)+JNICaches.swift" + + if let outputFile = try printer.writeContents( + outputDirectory: self.swiftOutputDirectory, + javaPackagePath: nil, + filename: fileName + ) { + print("[swift-java] Generated: \(fileName.bold) (at \(outputFile))") + self.expectedOutputSwiftFiles.remove(fileName) + } + } + private func printGlobalSwiftThunkSources(_ printer: inout CodePrinter) throws { printHeader(&printer) @@ -156,12 +186,32 @@ extension JNISwift2JavaGenerator { printSwiftFunctionThunk(&printer, enumCase.caseFunction) printer.println() + // Print enum case native init + printEnumNativeInit(&printer, translatedCase) + // Print getAsCase method if !translatedCase.translatedValues.isEmpty { printEnumGetAsCaseThunk(&printer, translatedCase) } } + private func printEnumNativeInit(_ printer: inout CodePrinter, _ enumCase: TranslatedEnumCase) { + printCDecl( + &printer, + javaMethodName: "$nativeInit", + parentName: "\(enumCase.original.enumType.nominalTypeDecl.name)$\(enumCase.name)$$JNI", + parameters: [], + resultType: .void + ) { printer in + // Setup caching + let nativeParametersClassName = "\(javaPackagePath)/\(enumCase.enumName)$\(enumCase.name)$$NativeParameters" + let methodSignature = MethodSignature(resultType: .void, parameterTypes: enumCase.parameterConversions.map(\.native.javaType)) + let methods = #"[.init(name: "", signature: "\#(methodSignature.mangledName)")]"# + + printer.print(#"JNICaches.\#(JNICaching.cacheName(for: enumCase.original)) = _JNICache(environment: environment, className: "\#(nativeParametersClassName)", methods: \#(methods))"#) + } + } + private func printEnumGetAsCaseThunk( _ printer: inout CodePrinter, _ enumCase: TranslatedEnumCase @@ -176,14 +226,15 @@ extension JNISwift2JavaGenerator { } let caseNamesWithLet = caseNames.map { "let \($0)" } let methodSignature = MethodSignature(resultType: .void, parameterTypes: enumCase.parameterConversions.map(\.native.javaType)) - // TODO: Caching of class and static method ID. printer.print( """ guard case .\(enumCase.original.name)(\(caseNamesWithLet.joined(separator: ", "))) = \(selfPointer).pointee else { fatalError("Expected enum case '\(enumCase.original.name)', but was '\\(\(selfPointer).pointee)'!") } - let class$ = environment.interface.FindClass(environment, "\(javaPackagePath)/\(enumCase.enumName)$\(enumCase.name)$$NativeParameters")! - let constructorID$ = environment.interface.GetMethodID(environment, class$, "", "\(methodSignature.mangledName)")! + let cache$ = JNICaches.\(JNICaching.cacheName(for: enumCase.original))! + let class$ = cache$.javaClass + let method$ = _JNICache.Method(name: "", signature: "\(methodSignature.mangledName)") + let constructorID$ = cache$[method$] """ ) let upcallArguments = zip(enumCase.parameterConversions, caseNames).map { conversion, caseName in diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift index 60ec1b8b..a93a19a7 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift @@ -72,6 +72,7 @@ package class JNISwift2JavaGenerator: Swift2JavaGenerator { return String(filePathPart.replacing(".swift", with: "+SwiftJava.swift")) }) self.expectedOutputSwiftFiles.insert("\(translator.swiftModuleName)Module+SwiftJava.swift") + self.expectedOutputSwiftFiles.insert("\(translator.swiftModuleName)+JNICaches.swift") // FIXME: Can we avoid this? self.expectedOutputSwiftFiles.insert("Data+SwiftJava.swift") diff --git a/Sources/JavaKit/Helpers/_JNICache.swift b/Sources/JavaKit/Helpers/_JNICache.swift new file mode 100644 index 00000000..998539c4 --- /dev/null +++ b/Sources/JavaKit/Helpers/_JNICache.swift @@ -0,0 +1,71 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +/// A cache used to hold references for JNI method and classes. +/// +/// This type is used internally in by the outputted JExtract wrappers +/// to improve performance of any JNI lookups. +/// +/// - Warning: This type is inherently thread-unsafe. +/// We assume that there exists **at most** one of these +/// per Java `class`, and it must only be initialized and modified in the +/// static initializer of that Java class. +public final class _JNICache: @unchecked Sendable { + public struct Method: Hashable { + public let name: String + public let signature: String + + public init(name: String, signature: String) { + self.name = name + self.signature = signature + } + } + + var _class: jclass? + let environment: JNIEnvironment + let methods: [Method: jmethodID] + + public var javaClass: jclass { + self._class! + } + + public init(environment: UnsafeMutablePointer!, className: String, methods: [Method]) { + guard let clazz = environment.interface.FindClass(environment, className) else { + fatalError("Class \(className) could not be found!") + } + self.environment = environment + self._class = environment.interface.NewGlobalRef(environment, clazz)! + self.methods = methods.reduce(into: [:]) { (result, method) in + if let methodID = environment.interface.GetMethodID(environment, clazz, method.name, method.signature) { + result[method] = methodID + } else { + fatalError("Method \(method.signature) with signature \(method.signature) not found in class \(className)") + } + } + } + + + public subscript(_ method: Method) -> jmethodID? { + methods[method] + } + + func cleanup() { + environment.interface.DeleteGlobalRef(environment, self._class) + } + + deinit { + cleanup() + self._class = nil + } +} From 485ea4c5502d51bd40e17aad5f3a58dcca48d71e Mon Sep 17 00:00:00 2001 From: Mads Odgaard Date: Tue, 12 Aug 2025 13:08:40 +0200 Subject: [PATCH 12/21] cleanup cache on jni unload --- .../src/test/java/com/example/swift/VehicleEnumTest.java | 2 +- .../JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/VehicleEnumTest.java b/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/VehicleEnumTest.java index c418eb91..e542d17d 100644 --- a/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/VehicleEnumTest.java +++ b/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/VehicleEnumTest.java @@ -117,7 +117,7 @@ void getAsMotorbike() { assertEquals("Yamaha", motorbike.arg0()); assertEquals(750, motorbike.horsePower()); assertEquals(OptionalInt.empty(), motorbike.helmets()); - + vehicle = Vehicle.motorbike("Yamaha", 750, OptionalInt.of(2), arena); motorbike = vehicle.getAsMotorbike().orElseThrow(); assertEquals(OptionalInt.of(2), motorbike.helmets()); diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift index bcc06702..c6dc9903 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift @@ -98,6 +98,12 @@ extension JNISwift2JavaGenerator { } } + printer.println() + printer.print(#"@_cdecl("JNI_OnUnload")"#) + printer.printBraceBlock("func JNI_OnUnload(javaVM: UnsafeMutablePointer!, reserved: UnsafeMutableRawPointer!)") { printer in + printer.print("JNICaches.cleanup()") + } + let fileName = "\(self.swiftModuleName)+JNICaches.swift" if let outputFile = try printer.writeContents( From fea7909976b3a3a7dfed33c6b16c6ea7a9f02ba3 Mon Sep 17 00:00:00 2001 From: Mads Odgaard Date: Tue, 12 Aug 2025 13:24:42 +0200 Subject: [PATCH 13/21] fix multiple optionals --- .../Sources/MySwiftLibrary/Vehicle.swift | 6 +++++- .../java/com/example/swift/VehicleEnumTest.java | 15 ++++++++++++++- ...JNISwift2JavaGenerator+JavaTranslation.swift | 17 +++++++++-------- ...ISwift2JavaGenerator+NativeTranslation.swift | 13 +++++++------ ...Swift2JavaGenerator+SwiftThunkPrinting.swift | 2 +- 5 files changed, 36 insertions(+), 17 deletions(-) diff --git a/Samples/JExtractJNISampleApp/Sources/MySwiftLibrary/Vehicle.swift b/Samples/JExtractJNISampleApp/Sources/MySwiftLibrary/Vehicle.swift index 3c155a12..9d9155ad 100644 --- a/Samples/JExtractJNISampleApp/Sources/MySwiftLibrary/Vehicle.swift +++ b/Samples/JExtractJNISampleApp/Sources/MySwiftLibrary/Vehicle.swift @@ -17,12 +17,14 @@ public enum Vehicle { case car(String, trailer: String?) case motorbike(String, horsePower: Int64, helmets: Int32?) indirect case transformer(front: Vehicle, back: Vehicle) + case boat(passengers: Int32?, length: Int16?) public init?(name: String) { switch name { case "bicycle": self = .bicycle case "car": self = .car("Unknown", trailer: nil) case "motorbike": self = .motorbike("Unknown", horsePower: 0, helmets: nil) + case "boat": self = .boat(passengers: nil, length: nil) default: return nil } } @@ -33,6 +35,7 @@ public enum Vehicle { case .car: "car" case .motorbike: "motorbike" case .transformer: "transformer" + case .boat: "boat" } } @@ -45,6 +48,7 @@ public enum Vehicle { case (.motorbike, .motorbike), (.motorbike, .transformer): false case (.transformer, .bicycle), (.transformer, .car), (.transformer, .motorbike): true case (.transformer, .transformer): false + default: false } } @@ -53,7 +57,7 @@ public enum Vehicle { case .bicycle: self = .car("Unknown", trailer: nil) case .car: self = .motorbike("Unknown", horsePower: 0, helmets: nil) case .motorbike: self = .transformer(front: .car("BMW", trailer: nil), back: self) - case .transformer: break + case .transformer, .boat: break } } } diff --git a/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/VehicleEnumTest.java b/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/VehicleEnumTest.java index e542d17d..62517789 100644 --- a/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/VehicleEnumTest.java +++ b/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/VehicleEnumTest.java @@ -117,7 +117,7 @@ void getAsMotorbike() { assertEquals("Yamaha", motorbike.arg0()); assertEquals(750, motorbike.horsePower()); assertEquals(OptionalInt.empty(), motorbike.helmets()); - + vehicle = Vehicle.motorbike("Yamaha", 750, OptionalInt.of(2), arena); motorbike = vehicle.getAsMotorbike().orElseThrow(); assertEquals(OptionalInt.of(2), motorbike.helmets()); @@ -134,6 +134,16 @@ void getAsTransformer() { } } + @Test + void getAsBoat() { + try (var arena = new ConfinedSwiftMemorySession()) { + Vehicle vehicle = Vehicle.boat(OptionalInt.of(10), Optional.of((short) 1), arena); + Vehicle.Boat boat = vehicle.getAsBoat().orElseThrow(); + assertEquals(OptionalInt.of(10), boat.passengers()); + assertEquals(Optional.of((short) 1), boat.length()); + } + } + @Test void associatedValuesAreCopied() { try (var arena = new ConfinedSwiftMemorySession()) { @@ -184,6 +194,9 @@ void switchGetCase() { case Vehicle.Transformer transformer: fail("Was transformer"); break; + case Vehicle.Boat b: + fail("Was boat"); + break; } } } diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift index 7b14d48a..d97f6b2e 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift @@ -555,6 +555,7 @@ extension JNISwift2JavaGenerator { conversion: .combinedValueToOptional( .placeholder, nextIntergralTypeWithSpaceForByte.javaType, + resultName: resultName, valueType: javaType, valueSizeInBytes: nextIntergralTypeWithSpaceForByte.valueBytes, optionalType: optionalClass @@ -754,7 +755,7 @@ extension JNISwift2JavaGenerator { case isOptionalPresent - indirect case combinedValueToOptional(JavaNativeConversionStep, JavaType, valueType: JavaType, valueSizeInBytes: Int, optionalType: String) + indirect case combinedValueToOptional(JavaNativeConversionStep, JavaType, resultName: String, valueType: JavaType, valueSizeInBytes: Int, optionalType: String) indirect case ternary(JavaNativeConversionStep, thenExp: JavaNativeConversionStep, elseExp: JavaNativeConversionStep) @@ -834,22 +835,22 @@ extension JNISwift2JavaGenerator { let argsStr = args.joined(separator: ", ") return "\(inner).\(methodName)(\(argsStr))" - case .combinedValueToOptional(let combined, let combinedType, let valueType, let valueSizeInBytes, let optionalType): + case .combinedValueToOptional(let combined, let combinedType, let resultName, let valueType, let valueSizeInBytes, let optionalType): let combined = combined.render(&printer, placeholder) printer.print( """ - \(combinedType) combined$ = \(combined); - byte discriminator$ = (byte) (combined$ & 0xFF); + \(combinedType) \(resultName)_combined$ = \(combined); + byte \(resultName)_discriminator$ = (byte) (\(resultName)_combined$ & 0xFF); """ ) if valueType == .boolean { - printer.print("boolean value$ = ((byte) (combined$ >> 8)) != 0;") + printer.print("boolean \(resultName)_value$ = ((byte) (\(resultName)_combined$ >> 8)) != 0;") } else { - printer.print("\(valueType) value$ = (\(valueType)) (combined$ >> \(valueSizeInBytes * 8));") + printer.print("\(valueType) \(resultName)_value$ = (\(valueType)) (\(resultName)_combined$ >> \(valueSizeInBytes * 8));") } - return "discriminator$ == 1 ? \(optionalType).of(value$) : \(optionalType).empty()" + return "\(resultName)_discriminator$ == 1 ? \(optionalType).of(\(resultName)_value$) : \(optionalType).empty()" case .ternary(let cond, let thenExp, let elseExp): let cond = cond.render(&printer, placeholder) @@ -920,7 +921,7 @@ extension JNISwift2JavaGenerator { case .method(let inner, _, let args): return inner.requiresSwiftArena || args.contains(where: \.requiresSwiftArena) - case .combinedValueToOptional(let inner, _, _, _, _): + case .combinedValueToOptional(let inner, _, _, _, _, _): return inner.requiresSwiftArena case .ternary(let cond, let thenExp, let elseExp): diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift index 85ae625c..f2e9522b 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift @@ -269,6 +269,7 @@ extension JNISwift2JavaGenerator { conversion: .getJNIValue( .optionalRaisingWidenIntegerType( .placeholder, + resultName: resultName, valueType: javaType, combinedSwiftType: nextIntergralTypeWithSpaceForByte.swiftType, valueSizeInBytes: nextIntergralTypeWithSpaceForByte.valueBytes @@ -396,7 +397,7 @@ extension JNISwift2JavaGenerator { guard let genericArgs = nominalType.genericArguments, genericArgs.count == 1 else { throw JavaTranslationError.unsupportedSwiftType(swiftResult.type) } - return try translateOptionalResult(wrappedType: genericArgs[0]) + return try translateOptionalResult(wrappedType: genericArgs[0], resultName: resultName) default: guard let javaType = JNIJavaTypeTranslator.translate(knownType: knownType, config: self.config), javaType.implementsJavaValue else { @@ -429,7 +430,7 @@ extension JNISwift2JavaGenerator { ) case .optional(let wrapped): - return try translateOptionalResult(wrappedType: wrapped) + return try translateOptionalResult(wrappedType: wrapped, resultName: resultName) case .metatype, .tuple, .function, .existential, .opaque, .genericParameter: throw JavaTranslationError.unsupportedSwiftType(swiftResult.type) @@ -501,7 +502,7 @@ extension JNISwift2JavaGenerator { indirect case optionalChain(NativeSwiftConversionStep) - indirect case optionalRaisingWidenIntegerType(NativeSwiftConversionStep, valueType: JavaType, combinedSwiftType: SwiftKnownTypeDeclKind, valueSizeInBytes: Int) + indirect case optionalRaisingWidenIntegerType(NativeSwiftConversionStep, resultName: String, valueType: JavaType, combinedSwiftType: SwiftKnownTypeDeclKind, valueSizeInBytes: Int) indirect case optionalRaisingIndirectReturn(NativeSwiftConversionStep, returnType: JavaType, discriminatorParameterName: String, placeholderValue: NativeSwiftConversionStep) @@ -630,18 +631,18 @@ extension JNISwift2JavaGenerator { let inner = inner.render(&printer, placeholder) return "\(inner)?" - case .optionalRaisingWidenIntegerType(let inner, let valueType, let combinedSwiftType, let valueSizeInBytes): + case .optionalRaisingWidenIntegerType(let inner, let resultName, 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$ = \(inner).map { + let \(resultName)_value$ = \(inner).map { \(combinedSwiftTypeName)(\(value)) << \(valueSizeInBytes * 8) | \(combinedSwiftTypeName)(1) } ?? 0 """ ) - return "value$" + return "\(resultName)_value$" case .optionalRaisingIndirectReturn(let inner, let returnType, let discriminatorParameterName, let placeholderValue): printer.print("let result$: \(returnType.jniTypeName)") diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift index c6dc9903..b6c99674 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift @@ -91,7 +91,7 @@ extension JNISwift2JavaGenerator { printer.print("static var \(JNICaching.cacheName(for: enumCase)): _JNICache!") } printer.println() - printer.printBraceBlock("func cleanup()") { printer in + printer.printBraceBlock("static func cleanup()") { printer in for enumCase in enumCases { printer.print("JNICaches.\(JNICaching.cacheName(for: enumCase)) = nil") } From 7bd0ef65ade082761d09679f90649c7f1cf05d76 Mon Sep 17 00:00:00 2001 From: Mads Odgaard Date: Tue, 12 Aug 2025 13:32:38 +0200 Subject: [PATCH 14/21] fix tests and decl documentation --- .../com/example/swift/AlignmentEnumTest.java | 3 +- .../com/example/swift/VehicleEnumTest.java | 34 ++++++++++--------- ...t2JavaGenerator+JavaBindingsPrinting.swift | 13 +++++-- 3 files changed, 30 insertions(+), 20 deletions(-) diff --git a/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/AlignmentEnumTest.java b/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/AlignmentEnumTest.java index 6256bd84..6be85c75 100644 --- a/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/AlignmentEnumTest.java +++ b/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/AlignmentEnumTest.java @@ -16,6 +16,7 @@ import org.junit.jupiter.api.Test; import org.swift.swiftkit.core.ConfinedSwiftMemorySession; +import org.swift.swiftkit.core.SwiftArena; import java.util.Optional; @@ -24,7 +25,7 @@ public class AlignmentEnumTest { @Test void rawValue() { - try (var arena = new ConfinedSwiftMemorySession()) { + try (var arena = SwiftArena.ofConfined()) { Optional invalid = Alignment.init("invalid", arena); assertFalse(invalid.isPresent()); diff --git a/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/VehicleEnumTest.java b/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/VehicleEnumTest.java index 62517789..c533603a 100644 --- a/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/VehicleEnumTest.java +++ b/Samples/JExtractJNISampleApp/src/test/java/com/example/swift/VehicleEnumTest.java @@ -16,7 +16,9 @@ import org.junit.jupiter.api.Test; import org.swift.swiftkit.core.ConfinedSwiftMemorySession; +import org.swift.swiftkit.core.SwiftArena; +import java.lang.foreign.Arena; import java.util.Optional; import java.util.OptionalInt; @@ -25,7 +27,7 @@ public class VehicleEnumTest { @Test void bicycle() { - try (var arena = new ConfinedSwiftMemorySession()) { + try (var arena = SwiftArena.ofConfined()) { Vehicle vehicle = Vehicle.bicycle(arena); assertNotNull(vehicle); } @@ -33,7 +35,7 @@ void bicycle() { @Test void car() { - try (var arena = new ConfinedSwiftMemorySession()) { + try (var arena = SwiftArena.ofConfined()) { Vehicle vehicle = Vehicle.car("Porsche 911", Optional.empty(), arena); assertNotNull(vehicle); } @@ -41,7 +43,7 @@ void car() { @Test void motorbike() { - try (var arena = new ConfinedSwiftMemorySession()) { + try (var arena = SwiftArena.ofConfined()) { Vehicle vehicle = Vehicle.motorbike("Yamaha", 750, OptionalInt.empty(), arena); assertNotNull(vehicle); } @@ -49,7 +51,7 @@ void motorbike() { @Test void initName() { - try (var arena = new ConfinedSwiftMemorySession()) { + try (var arena = SwiftArena.ofConfined()) { assertFalse(Vehicle.init("bus", arena).isPresent()); Optional vehicle = Vehicle.init("car", arena); assertTrue(vehicle.isPresent()); @@ -59,7 +61,7 @@ void initName() { @Test void nameProperty() { - try (var arena = new ConfinedSwiftMemorySession()) { + try (var arena = SwiftArena.ofConfined()) { Vehicle vehicle = Vehicle.bicycle(arena); assertEquals("bicycle", vehicle.getName()); } @@ -67,7 +69,7 @@ void nameProperty() { @Test void isFasterThan() { - try (var arena = new ConfinedSwiftMemorySession()) { + try (var arena = SwiftArena.ofConfined()) { Vehicle bicycle = Vehicle.bicycle(arena); Vehicle car = Vehicle.car("Porsche 911", Optional.empty(), arena); assertFalse(bicycle.isFasterThan(car)); @@ -77,7 +79,7 @@ void isFasterThan() { @Test void upgrade() { - try (var arena = new ConfinedSwiftMemorySession()) { + try (var arena = SwiftArena.ofConfined()) { Vehicle vehicle = Vehicle.bicycle(arena); assertEquals("bicycle", vehicle.getName()); vehicle.upgrade(); @@ -89,7 +91,7 @@ void upgrade() { @Test void getAsBicycle() { - try (var arena = new ConfinedSwiftMemorySession()) { + try (var arena = SwiftArena.ofConfined()) { Vehicle vehicle = Vehicle.bicycle(arena); Vehicle.Bicycle bicycle = vehicle.getAsBicycle().orElseThrow(); assertNotNull(bicycle); @@ -98,7 +100,7 @@ void getAsBicycle() { @Test void getAsCar() { - try (var arena = new ConfinedSwiftMemorySession()) { + try (var arena = SwiftArena.ofConfined()) { Vehicle vehicle = Vehicle.car("BMW", Optional.empty(), arena); Vehicle.Car car = vehicle.getAsCar().orElseThrow(); assertEquals("BMW", car.arg0()); @@ -111,7 +113,7 @@ void getAsCar() { @Test void getAsMotorbike() { - try (var arena = new ConfinedSwiftMemorySession()) { + try (var arena = SwiftArena.ofConfined()) { Vehicle vehicle = Vehicle.motorbike("Yamaha", 750, OptionalInt.empty(), arena); Vehicle.Motorbike motorbike = vehicle.getAsMotorbike().orElseThrow(); assertEquals("Yamaha", motorbike.arg0()); @@ -126,7 +128,7 @@ void getAsMotorbike() { @Test void getAsTransformer() { - try (var arena = new ConfinedSwiftMemorySession()) { + try (var arena = SwiftArena.ofConfined()) { Vehicle vehicle = Vehicle.transformer(Vehicle.bicycle(arena), Vehicle.car("BMW", Optional.empty(), arena), arena); Vehicle.Transformer transformer = vehicle.getAsTransformer(arena).orElseThrow(); assertTrue(transformer.front().getAsBicycle().isPresent()); @@ -136,7 +138,7 @@ void getAsTransformer() { @Test void getAsBoat() { - try (var arena = new ConfinedSwiftMemorySession()) { + try (var arena = SwiftArena.ofConfined()) { Vehicle vehicle = Vehicle.boat(OptionalInt.of(10), Optional.of((short) 1), arena); Vehicle.Boat boat = vehicle.getAsBoat().orElseThrow(); assertEquals(OptionalInt.of(10), boat.passengers()); @@ -146,7 +148,7 @@ void getAsBoat() { @Test void associatedValuesAreCopied() { - try (var arena = new ConfinedSwiftMemorySession()) { + try (var arena = SwiftArena.ofConfined()) { Vehicle vehicle = Vehicle.car("BMW", Optional.empty(), arena); Vehicle.Car car = vehicle.getAsCar().orElseThrow(); assertEquals("BMW", car.arg0()); @@ -160,7 +162,7 @@ void associatedValuesAreCopied() { @Test void getDiscriminator() { - try (var arena = new ConfinedSwiftMemorySession()) { + try (var arena = SwiftArena.ofConfined()) { assertEquals(Vehicle.Discriminator.BICYCLE, Vehicle.bicycle(arena).getDiscriminator()); assertEquals(Vehicle.Discriminator.CAR, Vehicle.car("BMW", Optional.empty(), arena).getDiscriminator()); assertEquals(Vehicle.Discriminator.MOTORBIKE, Vehicle.motorbike("Yamaha", 750, OptionalInt.empty(), arena).getDiscriminator()); @@ -170,7 +172,7 @@ void getDiscriminator() { @Test void getCase() { - try (var arena = new ConfinedSwiftMemorySession()) { + try (var arena = SwiftArena.ofConfined()) { Vehicle vehicle = Vehicle.bicycle(arena); Vehicle.Case caseElement = vehicle.getCase(arena); assertInstanceOf(Vehicle.Bicycle.class, caseElement); @@ -179,7 +181,7 @@ void getCase() { @Test void switchGetCase() { - try (var arena = new ConfinedSwiftMemorySession()) { + try (var arena = SwiftArena.ofConfined()) { Vehicle vehicle = Vehicle.car("BMW", Optional.empty(), arena); switch (vehicle.getCase(arena)) { case Vehicle.Bicycle b: diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift index 917464c4..77ce9185 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift @@ -349,12 +349,13 @@ extension JNISwift2JavaGenerator { guard let translatedDecl = translatedDecl(for: decl) else { fatalError("Decl was not translated, \(decl)") } - printJavaBindingWrapperMethod(&printer, translatedDecl) + printJavaBindingWrapperMethod(&printer, translatedDecl, importedFunc: decl) } private func printJavaBindingWrapperMethod( _ printer: inout CodePrinter, _ translatedDecl: TranslatedFunctionDecl, + importedFunc: ImportedFunc? = nil, prefix: (inout CodePrinter) -> Void = { _ in } ) { var modifiers = ["public"] @@ -362,9 +363,10 @@ extension JNISwift2JavaGenerator { modifiers.append("static") } + let translatedSignature = translatedDecl.translatedFunctionSignature let resultType = translatedSignature.resultType.javaType var parameters = translatedDecl.translatedFunctionSignature.parameters.map { $0.parameter.renderParameter() } - let throwsClause = decl.isThrowing ? " throws Exception" : "" + let throwsClause = translatedDecl.isThrowing ? " throws Exception" : "" var annotationsStr = translatedSignature.annotations.map({ $0.render() }).joined(separator: "\n") if !annotationsStr.isEmpty { annotationsStr += "\n" } @@ -373,7 +375,9 @@ extension JNISwift2JavaGenerator { // Print default global arena variation if config.effectiveMemoryManagementMode.requiresGlobalArena && translatedSignature.requiresSwiftArena { - printDeclDocumentation(&printer, decl) + if let importedFunc { + printDeclDocumentation(&printer, importedFunc) + } printer.printBraceBlock( "\(annotationsStr)\(modifiers.joined(separator: " ")) \(resultType) \(translatedDecl.name)(\(parametersStr))\(throwsClause)" ) { printer in @@ -392,6 +396,9 @@ extension JNISwift2JavaGenerator { if translatedSignature.requiresSwiftArena { parameters.append("SwiftArena swiftArena$") } + if let importedFunc { + printDeclDocumentation(&printer, importedFunc) + } printer.printBraceBlock( "\(annotationsStr)\(modifiers.joined(separator: " ")) \(resultType) \(translatedDecl.name)(\(parameters.joined(separator: ", ")))\(throwsClause)" ) { printer in From d75c2011795068812b6eeea5a5febd6b9ba67ffc Mon Sep 17 00:00:00 2001 From: Mads Odgaard Date: Tue, 12 Aug 2025 13:38:44 +0200 Subject: [PATCH 15/21] fix benchmark --- .../src/jmh/java/com/example/swift/EnumBenchmark.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Samples/JExtractJNISampleApp/src/jmh/java/com/example/swift/EnumBenchmark.java b/Samples/JExtractJNISampleApp/src/jmh/java/com/example/swift/EnumBenchmark.java index b15dfd78..d3de624b 100644 --- a/Samples/JExtractJNISampleApp/src/jmh/java/com/example/swift/EnumBenchmark.java +++ b/Samples/JExtractJNISampleApp/src/jmh/java/com/example/swift/EnumBenchmark.java @@ -2,7 +2,7 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 20245Apple 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 @@ -18,6 +18,7 @@ import org.openjdk.jmh.infra.Blackhole; import org.swift.swiftkit.core.ClosableSwiftArena; import org.swift.swiftkit.core.ConfinedSwiftMemorySession; +import org.swift.swiftkit.core.SwiftArena; import java.util.ArrayList; import java.util.List; @@ -39,7 +40,7 @@ public static class BenchmarkState { @Setup(Level.Trial) public void beforeAll() { - arena = new ConfinedSwiftMemorySession(); + arena = SwiftArena.ofConfined(); vehicle = Vehicle.motorbike("Yamaha", 900, OptionalInt.empty(), arena); } From dc2a6a679067f219e2e08832fb3be911ac1a5791 Mon Sep 17 00:00:00 2001 From: Mads Odgaard Date: Tue, 12 Aug 2025 14:19:01 +0200 Subject: [PATCH 16/21] add docs --- .../Documentation.docc/SupportedFeatures.md | 113 +++++++++++++++++- 1 file changed, 112 insertions(+), 1 deletion(-) diff --git a/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md b/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md index 130c333b..71c7816b 100644 --- a/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md +++ b/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md @@ -49,7 +49,8 @@ SwiftJava's `swift-java jextract` tool automates generating Java bindings from S | Initializers: `class`, `struct` | ✅ | ✅ | | Optional Initializers / Throwing Initializers | ❌ | ❌ | | Deinitializers: `class`, `struct` | ✅ | ✅ | -| `enum`, `actor` | ❌ | ❌ | +| `enum` | ❌ | ✅ | +| `actor` | ❌ | ❌ | | Global Swift `func` | ✅ | ✅ | | Class/struct member `func` | ✅ | ✅ | | Throwing functions: `func x() throws` | ❌ | ✅ | @@ -157,3 +158,113 @@ you are expected to add a Guava dependency to your Java project. | `Double` | `double` | > Note: The `wrap-guava` mode is currently only available in FFM mode of jextract. + +#### Enums + +> Note: Enums are currently only supported in JNI mode. + +Swift enums are extracted into a corresponding Java `class`. To support associated values +all cases are also extracted as Java `record`s. + +Consider the following Swift enum: +```swift +public enum Vehicle { + case car(String) + case bicycle(maker: String) +} +``` +You can then instantiate a case of `Vehicle` by using one of the static methods: +```java +try (var arena = SwiftArena.ofConfined()) { + Vehicle vehicle = Vehicle.car("BMW", arena); + Optional car = vehicle.getAsCar(); + assertEquals("BMW", car.orElseThrow().arg0()); +} +``` +As you can see above, to access the associated values of a case you can call one of the +`getAsX` methods that will return an Optional record with the associated values. +```java +try (var arena = SwiftArena.ofConfined()) { + Vehicle vehicle = Vehicle.bycicle("My Brand", arena); + Optional car = vehicle.getAsCar(); + assertFalse(car.isPresent()); + + Optional bicycle = vehicle.getAsBicycle(); + assertEquals("My Brand", bicycle.orElseThrow().maker()); +} +``` + +##### Switching + +If you only need to switch on the case and not access any associated values, +you can use the `getDiscriminator()` method: +```java +Vehicle vehicle = ...; +switch (vehicle.getDiscriminator()) { + case BICYCLE: + System.out.println("I am a bicycle!"); + break + case CAR: + System.out.println("I am a car!"); + break +} +``` +If you also want access to the associated values, you have various options +depending on the Java version you are using. +If you are running Java 21+ you can use [pattern matching for switch](https://openjdk.org/jeps/441): +```java +Vehicle vehicle = ...; +switch (vehicle.getCase()) { + case Vehicle.Bicycle b: + System.out.println("Bicycle maker: " + b.maker()); + break + case Vehicle.Car c: + System.out.println("Car: " + c.arg0()); + break +} +``` +For Java 16+ you can use [pattern matching for instanceof](https://openjdk.org/jeps/394) +```java +Vehicle vehicle = ...; +Vehicle.Case case = vehicle.getCase(); +if (case instanceof Vehicle.Bicycle b) { + System.out.println("Bicycle maker: " + b.maker()); +} else if(case instanceof Vehicle.Car c) { + System.out.println("Car: " + c.arg0()); +} +``` +For any previous Java versions you can resort to casting the `Case` to the expected type: +```java +Vehicle vehicle = ...; +Vehicle.Case case = vehicle.getCase(); +if (case instanceof Vehicle.Bicycle) { + Vehicle.Bicycle b = (Vehicle.Bicycle) case; + System.out.println("Bicycle maker: " + b.maker()); +} else if(case instanceof Vehicle.Car) { + Vehicle.Car c = (Vehicle.Car) case; + System.out.println("Car: " + c.arg0()); +} +``` + +##### RawRepresentable + +JExtract also supports extracting enums that conform to `RawRepresentable` +by giving access to an optional initializer and the `rawValue` variable. +Consider the following example: +```swift +public enum Alignment: String { + case horizontal + case vertical +} +``` +you can then initialize `Alignment` from a `String` and also retrieve back its `rawValue`: +```java +try (var arena = SwiftArena.ofConfined()) { + Optional alignment = Alignment.init("horizontal", arena); + assertEqual(HORIZONTAL, alignment.orElseThrow().getDiscriminator()); + assertEqual("horizontal", alignment.orElseThrow().getRawValue()); +} +``` + + + From 89825ad0beeaf0173399c454729ab30385fe31dd Mon Sep 17 00:00:00 2001 From: Mads Odgaard Date: Tue, 12 Aug 2025 14:22:54 +0200 Subject: [PATCH 17/21] correct docs header --- .../Documentation.docc/SupportedFeatures.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md b/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md index 71c7816b..4c0210ad 100644 --- a/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md +++ b/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md @@ -159,7 +159,7 @@ you are expected to add a Guava dependency to your Java project. > Note: The `wrap-guava` mode is currently only available in FFM mode of jextract. -#### Enums +### Enums > Note: Enums are currently only supported in JNI mode. @@ -194,7 +194,7 @@ try (var arena = SwiftArena.ofConfined()) { } ``` -##### Switching +#### Switching If you only need to switch on the case and not access any associated values, you can use the `getDiscriminator()` method: @@ -246,7 +246,7 @@ if (case instanceof Vehicle.Bicycle) { } ``` -##### RawRepresentable +#### RawRepresentable JExtract also supports extracting enums that conform to `RawRepresentable` by giving access to an optional initializer and the `rawValue` variable. From 7f40d0e75c58be131c1797afd9be25949192a269 Mon Sep 17 00:00:00 2001 From: Mads Odgaard Date: Wed, 13 Aug 2025 17:06:01 +0200 Subject: [PATCH 18/21] PR feedback --- .../JExtractSwiftPlugin.swift | 7 --- .../{Alignment.swift => EnumAlignment.swift} | 0 .../{Vehicle.swift => EnumVehicle.swift} | 0 Sources/JExtractSwiftLib/JNI/JNICaching.swift | 14 ++++- ...t2JavaGenerator+JavaBindingsPrinting.swift | 23 +------ ...ift2JavaGenerator+SwiftThunkPrinting.swift | 60 ++++--------------- .../JNI/JNISwift2JavaGenerator.swift | 1 - ...JNICache.swift => _JNIMethodIDCache.swift} | 20 ++----- .../Documentation.docc/SupportedFeatures.md | 4 +- 9 files changed, 33 insertions(+), 96 deletions(-) rename Samples/JExtractJNISampleApp/Sources/MySwiftLibrary/{Alignment.swift => EnumAlignment.swift} (100%) rename Samples/JExtractJNISampleApp/Sources/MySwiftLibrary/{Vehicle.swift => EnumVehicle.swift} (100%) rename Sources/JavaKit/Helpers/{_JNICache.swift => _JNIMethodIDCache.swift} (77%) diff --git a/Plugins/JExtractSwiftPlugin/JExtractSwiftPlugin.swift b/Plugins/JExtractSwiftPlugin/JExtractSwiftPlugin.swift index 43214d5e..2778cebe 100644 --- a/Plugins/JExtractSwiftPlugin/JExtractSwiftPlugin.swift +++ b/Plugins/JExtractSwiftPlugin/JExtractSwiftPlugin.swift @@ -114,13 +114,6 @@ struct JExtractSwiftBuildToolPlugin: SwiftJavaPluginProtocol, BuildToolPlugin { outputSwiftDirectory.appending(path: "\(sourceModule.name)Module+SwiftJava.swift") ] - // Append any JNI cache files - if configuration?.mode == .jni { - outputSwiftFiles += [ - outputSwiftDirectory.appending(path: "\(sourceModule.name)+JNICaches.swift") - ] - } - // If the module uses 'Data' type, the thunk file is emitted as if 'Data' is declared // in that module. Declare the thunk file as the output. // FIXME: Make this conditional. diff --git a/Samples/JExtractJNISampleApp/Sources/MySwiftLibrary/Alignment.swift b/Samples/JExtractJNISampleApp/Sources/MySwiftLibrary/EnumAlignment.swift similarity index 100% rename from Samples/JExtractJNISampleApp/Sources/MySwiftLibrary/Alignment.swift rename to Samples/JExtractJNISampleApp/Sources/MySwiftLibrary/EnumAlignment.swift diff --git a/Samples/JExtractJNISampleApp/Sources/MySwiftLibrary/Vehicle.swift b/Samples/JExtractJNISampleApp/Sources/MySwiftLibrary/EnumVehicle.swift similarity index 100% rename from Samples/JExtractJNISampleApp/Sources/MySwiftLibrary/Vehicle.swift rename to Samples/JExtractJNISampleApp/Sources/MySwiftLibrary/EnumVehicle.swift diff --git a/Sources/JExtractSwiftLib/JNI/JNICaching.swift b/Sources/JExtractSwiftLib/JNI/JNICaching.swift index 6d314891..cdb13e3f 100644 --- a/Sources/JExtractSwiftLib/JNI/JNICaching.swift +++ b/Sources/JExtractSwiftLib/JNI/JNICaching.swift @@ -13,7 +13,19 @@ //===----------------------------------------------------------------------===// enum JNICaching { - static func cacheName(for enumCase: ImportedEnumCase) -> String { + static func cacheName(for type: ImportedNominalType) -> String { + cacheName(for: type.swiftNominal.name) + } + + static func cacheName(for type: SwiftNominalType) -> String { + cacheName(for: type.nominalTypeDecl.name) + } + + private static func cacheName(for name: String) -> String { + "_JNI_\(name)" + } + + static func cacheMemberName(for enumCase: ImportedEnumCase) -> String { "\(enumCase.enumType.nominalTypeDecl.name.firstCharacterLowercased)\(enumCase.name.firstCharacterUppercased)Cache" } } diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift index 77ce9185..9af53f58 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift @@ -259,17 +259,6 @@ extension JNISwift2JavaGenerator { // Print record printer.printBraceBlock("public record \(caseName)(\(members.joined(separator: ", "))) implements Case") { printer in - printer.printBraceBlock("static class $JNI") { printer in - printer.print("private static native void $nativeInit();") - } - - // Used to ensure static initializer has been calling to trigger caching. - printer.print("static void $ensureInitialized() {}") - - printer.printBraceBlock("static") { printer in - printer.print("$JNI.$nativeInit();") - } - let nativeParameters = zip(translatedCase.translatedValues, translatedCase.parameterConversions).flatMap { value, conversion in ["\(conversion.native.javaType) \(value.parameter.name)"] } @@ -277,13 +266,7 @@ extension JNISwift2JavaGenerator { printer.print("record $NativeParameters(\(nativeParameters.joined(separator: ", "))) {}") } - self.printJavaBindingWrapperMethod( - &printer, - translatedCase.getAsCaseFunction, - prefix: { printer in - printer.print("\(caseName).$ensureInitialized();") - } - ) + self.printJavaBindingWrapperMethod(&printer, translatedCase.getAsCaseFunction) printer.println() } } @@ -355,8 +338,7 @@ extension JNISwift2JavaGenerator { private func printJavaBindingWrapperMethod( _ printer: inout CodePrinter, _ translatedDecl: TranslatedFunctionDecl, - importedFunc: ImportedFunc? = nil, - prefix: (inout CodePrinter) -> Void = { _ in } + importedFunc: ImportedFunc? = nil ) { var modifiers = ["public"] if translatedDecl.isStatic { @@ -402,7 +384,6 @@ extension JNISwift2JavaGenerator { printer.printBraceBlock( "\(annotationsStr)\(modifiers.joined(separator: " ")) \(resultType) \(translatedDecl.name)(\(parameters.joined(separator: ", ")))\(throwsClause)" ) { printer in - prefix(&printer) printDowncall(&printer, translatedDecl) } diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift index b6c99674..6b8c435a 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift @@ -56,8 +56,6 @@ extension JNISwift2JavaGenerator { self.expectedOutputSwiftFiles.remove(moduleFilename) } - try self.writeJNICacheSource(&printer) - for (_, ty) in self.analysis.importedTypes.sorted(by: { (lhs, rhs) in lhs.key < rhs.key }) { let fileNameBase = "\(ty.swiftNominal.qualifiedName)+SwiftJava" let filename = "\(fileNameBase).swift" @@ -82,38 +80,13 @@ extension JNISwift2JavaGenerator { } } - private func writeJNICacheSource(_ printer: inout CodePrinter) throws { - printer.print("import JavaKit") - printer.println() - printer.printBraceBlock("enum JNICaches") { printer in - let enumCases = self.analysis.importedTypes.values.filter { $0.swiftNominal.kind == .enum }.flatMap(\.cases) - for enumCase in enumCases { - printer.print("static var \(JNICaching.cacheName(for: enumCase)): _JNICache!") - } - printer.println() - printer.printBraceBlock("static func cleanup()") { printer in - for enumCase in enumCases { - printer.print("JNICaches.\(JNICaching.cacheName(for: enumCase)) = nil") - } + private func printJNICache(_ printer: inout CodePrinter, _ type: ImportedNominalType) { + printer.printBraceBlock("enum \(JNICaching.cacheName(for: type))") { printer in + for enumCase in type.cases { + guard let translatedCase = translatedEnumCase(for: enumCase) else { continue } + printer.print("static let \(JNICaching.cacheMemberName(for: enumCase)) = \(renderEnumCaseCacheInit(translatedCase))") } } - - printer.println() - printer.print(#"@_cdecl("JNI_OnUnload")"#) - printer.printBraceBlock("func JNI_OnUnload(javaVM: UnsafeMutablePointer!, reserved: UnsafeMutableRawPointer!)") { printer in - printer.print("JNICaches.cleanup()") - } - - let fileName = "\(self.swiftModuleName)+JNICaches.swift" - - if let outputFile = try printer.writeContents( - outputDirectory: self.swiftOutputDirectory, - javaPackagePath: nil, - filename: fileName - ) { - print("[swift-java] Generated: \(fileName.bold) (at \(outputFile))") - self.expectedOutputSwiftFiles.remove(fileName) - } } private func printGlobalSwiftThunkSources(_ printer: inout CodePrinter) throws { @@ -133,6 +106,9 @@ extension JNISwift2JavaGenerator { private func printNominalTypeThunks(_ printer: inout CodePrinter, _ type: ImportedNominalType) throws { printHeader(&printer) + printJNICache(&printer, type) + printer.println() + for initializer in type.initializers { printSwiftFunctionThunk(&printer, initializer) printer.println() @@ -192,30 +168,18 @@ extension JNISwift2JavaGenerator { printSwiftFunctionThunk(&printer, enumCase.caseFunction) printer.println() - // Print enum case native init - printEnumNativeInit(&printer, translatedCase) - // Print getAsCase method if !translatedCase.translatedValues.isEmpty { printEnumGetAsCaseThunk(&printer, translatedCase) } } - private func printEnumNativeInit(_ printer: inout CodePrinter, _ enumCase: TranslatedEnumCase) { - printCDecl( - &printer, - javaMethodName: "$nativeInit", - parentName: "\(enumCase.original.enumType.nominalTypeDecl.name)$\(enumCase.name)$$JNI", - parameters: [], - resultType: .void - ) { printer in - // Setup caching + private func renderEnumCaseCacheInit(_ enumCase: TranslatedEnumCase) -> String { let nativeParametersClassName = "\(javaPackagePath)/\(enumCase.enumName)$\(enumCase.name)$$NativeParameters" let methodSignature = MethodSignature(resultType: .void, parameterTypes: enumCase.parameterConversions.map(\.native.javaType)) let methods = #"[.init(name: "", signature: "\#(methodSignature.mangledName)")]"# - printer.print(#"JNICaches.\#(JNICaching.cacheName(for: enumCase.original)) = _JNICache(environment: environment, className: "\#(nativeParametersClassName)", methods: \#(methods))"#) - } + return #"_JNIMethodIDCache(environment: try! JavaVirtualMachine.shared().environment(), className: "\#(nativeParametersClassName)", methods: \#(methods))"# } private func printEnumGetAsCaseThunk( @@ -237,9 +201,9 @@ extension JNISwift2JavaGenerator { guard case .\(enumCase.original.name)(\(caseNamesWithLet.joined(separator: ", "))) = \(selfPointer).pointee else { fatalError("Expected enum case '\(enumCase.original.name)', but was '\\(\(selfPointer).pointee)'!") } - let cache$ = JNICaches.\(JNICaching.cacheName(for: enumCase.original))! + let cache$ = \(JNICaching.cacheName(for: enumCase.original.enumType)).\(JNICaching.cacheMemberName(for: enumCase.original)) let class$ = cache$.javaClass - let method$ = _JNICache.Method(name: "", signature: "\(methodSignature.mangledName)") + let method$ = _JNIMethodIDCache.Method(name: "", signature: "\(methodSignature.mangledName)") let constructorID$ = cache$[method$] """ ) diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift index a93a19a7..60ec1b8b 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift @@ -72,7 +72,6 @@ package class JNISwift2JavaGenerator: Swift2JavaGenerator { return String(filePathPart.replacing(".swift", with: "+SwiftJava.swift")) }) self.expectedOutputSwiftFiles.insert("\(translator.swiftModuleName)Module+SwiftJava.swift") - self.expectedOutputSwiftFiles.insert("\(translator.swiftModuleName)+JNICaches.swift") // FIXME: Can we avoid this? self.expectedOutputSwiftFiles.insert("Data+SwiftJava.swift") diff --git a/Sources/JavaKit/Helpers/_JNICache.swift b/Sources/JavaKit/Helpers/_JNIMethodIDCache.swift similarity index 77% rename from Sources/JavaKit/Helpers/_JNICache.swift rename to Sources/JavaKit/Helpers/_JNIMethodIDCache.swift index 998539c4..a67d225f 100644 --- a/Sources/JavaKit/Helpers/_JNICache.swift +++ b/Sources/JavaKit/Helpers/_JNIMethodIDCache.swift @@ -16,12 +16,7 @@ /// /// This type is used internally in by the outputted JExtract wrappers /// to improve performance of any JNI lookups. -/// -/// - Warning: This type is inherently thread-unsafe. -/// We assume that there exists **at most** one of these -/// per Java `class`, and it must only be initialized and modified in the -/// static initializer of that Java class. -public final class _JNICache: @unchecked Sendable { +public final class _JNIMethodIDCache: Sendable { public struct Method: Hashable { public let name: String public let signature: String @@ -32,9 +27,8 @@ public final class _JNICache: @unchecked Sendable { } } - var _class: jclass? - let environment: JNIEnvironment - let methods: [Method: jmethodID] + nonisolated(unsafe) let _class: jclass? + nonisolated(unsafe) let methods: [Method: jmethodID] public var javaClass: jclass { self._class! @@ -44,7 +38,6 @@ public final class _JNICache: @unchecked Sendable { guard let clazz = environment.interface.FindClass(environment, className) else { fatalError("Class \(className) could not be found!") } - self.environment = environment self._class = environment.interface.NewGlobalRef(environment, clazz)! self.methods = methods.reduce(into: [:]) { (result, method) in if let methodID = environment.interface.GetMethodID(environment, clazz, method.name, method.signature) { @@ -60,12 +53,7 @@ public final class _JNICache: @unchecked Sendable { methods[method] } - func cleanup() { + public func cleanup(environment: UnsafeMutablePointer!) { environment.interface.DeleteGlobalRef(environment, self._class) } - - deinit { - cleanup() - self._class = nil - } } diff --git a/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md b/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md index 4c0210ad..c5de6c45 100644 --- a/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md +++ b/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md @@ -194,7 +194,7 @@ try (var arena = SwiftArena.ofConfined()) { } ``` -#### Switching +#### Switching and pattern matching If you only need to switch on the case and not access any associated values, you can use the `getDiscriminator()` method: @@ -246,7 +246,7 @@ if (case instanceof Vehicle.Bicycle) { } ``` -#### RawRepresentable +#### RawRepresentable enums JExtract also supports extracting enums that conform to `RawRepresentable` by giving access to an optional initializer and the `rawValue` variable. From 5955c98c04dca1e9c9352742fac7fd204c1a584d Mon Sep 17 00:00:00 2001 From: Mads Odgaard Date: Fri, 15 Aug 2025 07:44:43 +0200 Subject: [PATCH 19/21] rename enum files back --- .../MySwiftLibrary/{EnumAlignment.swift => Alignment.swift} | 0 .../Sources/MySwiftLibrary/{EnumVehicle.swift => Vehicle.swift} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename Samples/JExtractJNISampleApp/Sources/MySwiftLibrary/{EnumAlignment.swift => Alignment.swift} (100%) rename Samples/JExtractJNISampleApp/Sources/MySwiftLibrary/{EnumVehicle.swift => Vehicle.swift} (100%) diff --git a/Samples/JExtractJNISampleApp/Sources/MySwiftLibrary/EnumAlignment.swift b/Samples/JExtractJNISampleApp/Sources/MySwiftLibrary/Alignment.swift similarity index 100% rename from Samples/JExtractJNISampleApp/Sources/MySwiftLibrary/EnumAlignment.swift rename to Samples/JExtractJNISampleApp/Sources/MySwiftLibrary/Alignment.swift diff --git a/Samples/JExtractJNISampleApp/Sources/MySwiftLibrary/EnumVehicle.swift b/Samples/JExtractJNISampleApp/Sources/MySwiftLibrary/Vehicle.swift similarity index 100% rename from Samples/JExtractJNISampleApp/Sources/MySwiftLibrary/EnumVehicle.swift rename to Samples/JExtractJNISampleApp/Sources/MySwiftLibrary/Vehicle.swift From a6ff6e15d54ad2599b0ca1b54ffc6078b365499c Mon Sep 17 00:00:00 2001 From: Mads Odgaard Date: Fri, 15 Aug 2025 07:56:44 +0200 Subject: [PATCH 20/21] fix existing tests --- ...t2JavaGenerator+JavaBindingsPrinting.swift | 1 - .../JNI/JNIOptionalTests.swift | 34 +++++++++---------- 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift index c568ab4c..9351252e 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift @@ -297,7 +297,6 @@ extension JNISwift2JavaGenerator { printJavaBindingWrapperHelperClass(&printer, decl) - printDeclDocumentation(&printer, decl) printJavaBindingWrapperMethod(&printer, decl) } diff --git a/Tests/JExtractSwiftTests/JNI/JNIOptionalTests.swift b/Tests/JExtractSwiftTests/JNI/JNIOptionalTests.swift index 2186e227..548a2eac 100644 --- a/Tests/JExtractSwiftTests/JNI/JNIOptionalTests.swift +++ b/Tests/JExtractSwiftTests/JNI/JNIOptionalTests.swift @@ -47,10 +47,10 @@ struct JNIOptionalTests { * } */ 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(); + long result_combined$ = SwiftModule.$optionalSugar((byte) (arg.isPresent() ? 1 : 0), arg.orElse(0L)); + byte result_discriminator$ = (byte) (result_combined$ & 0xFF); + int result_value$ = (int) (result_combined$ >> 32); + return result_discriminator$ == 1 ? OptionalInt.of(result_value$) : OptionalInt.empty(); } """, """ @@ -72,10 +72,10 @@ struct JNIOptionalTests { """ @_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 { + let result_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!) + return result_value$.getJNIValue(in: environment!) } """ ] @@ -98,9 +98,9 @@ struct JNIOptionalTests { * } */ 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(); + 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(); } """, """ @@ -127,12 +127,12 @@ struct JNIOptionalTests { result$ = innerResult$.getJNIValue(in: environment!) var flag$ = Int8(1) environment.interface.SetByteArrayRegion(environment, result_discriminator$, 0, 1, &flag$) - } // render(_:_:) @ JExtractSwiftLib/JNISwift2JavaGenerator+NativeTranslation.swift:624 + } // render(_:_:) @ JExtractSwiftLib/JNISwift2JavaGenerator+NativeTranslation.swift:649 else { result$ = String.jniPlaceholderValue var flag$ = Int8(0) environment.interface.SetByteArrayRegion(environment, result_discriminator$, 0, 1, &flag$) - } // render(_:_:) @ JExtractSwiftLib/JNISwift2JavaGenerator+NativeTranslation.swift:634 + } // render(_:_:) @ JExtractSwiftLib/JNISwift2JavaGenerator+NativeTranslation.swift:659 return result$ } """ @@ -156,9 +156,9 @@ struct JNIOptionalTests { * } */ 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(MyClass.wrapMemoryAddressUnsafe(result$, swiftArena$)) : Optional.empty(); + 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(MyClass.wrapMemoryAddressUnsafe(result$, swiftArena$)) : Optional.empty(); } """, """ @@ -190,12 +190,12 @@ struct JNIOptionalTests { result$ = _resultBits$.getJNIValue(in: environment!) var flag$ = Int8(1) environment.interface.SetByteArrayRegion(environment, result_discriminator$, 0, 1, &flag$) - } // render(_:_:) @ JExtractSwiftLib/JNISwift2JavaGenerator+NativeTranslation.swift:624 + } // render(_:_:) @ JExtractSwiftLib/JNISwift2JavaGenerator+NativeTranslation.swift:649 else { result$ = 0 var flag$ = Int8(0) environment.interface.SetByteArrayRegion(environment, result_discriminator$, 0, 1, &flag$) - } // render(_:_:) @ JExtractSwiftLib/JNISwift2JavaGenerator+NativeTranslation.swift:634 + } // render(_:_:) @ JExtractSwiftLib/JNISwift2JavaGenerator+NativeTranslation.swift:659 return result$ } """ @@ -243,7 +243,7 @@ struct JNIOptionalTests { 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 + } // render(_:_:) @ JExtractSwiftLib/JNISwift2JavaGenerator+NativeTranslation.swift:691 ) } """ From e32edf35d62421b15dac702926ffe1404d6151ac Mon Sep 17 00:00:00 2001 From: Mads Odgaard Date: Sat, 16 Aug 2025 11:24:46 +0200 Subject: [PATCH 21/21] add source gen tests --- .../JExtractSwiftTests/JNI/JNIEnumTests.swift | 313 ++++++++++++++++++ 1 file changed, 313 insertions(+) create mode 100644 Tests/JExtractSwiftTests/JNI/JNIEnumTests.swift diff --git a/Tests/JExtractSwiftTests/JNI/JNIEnumTests.swift b/Tests/JExtractSwiftTests/JNI/JNIEnumTests.swift new file mode 100644 index 00000000..47765043 --- /dev/null +++ b/Tests/JExtractSwiftTests/JNI/JNIEnumTests.swift @@ -0,0 +1,313 @@ +//===----------------------------------------------------------------------===// +// +// 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 JNIEnumTests { + let source = """ + public enum MyEnum { + case first + case second(String) + case third(x: Int64, y: Int32) + } + """ + + @Test + func generatesJavaClass() throws { + try assertOutput( + input: source, + .jni, .java, + expectedChunks: [ + """ + // Generated by jextract-swift + // Swift module: SwiftModule + + package com.example.swift; + + import org.swift.swiftkit.core.*; + import org.swift.swiftkit.core.util.*; + """, + """ + public final class MyEnum extends JNISwiftInstance { + static final String LIB_NAME = "SwiftModule"; + + @SuppressWarnings("unused") + private static final boolean INITIALIZED_LIBS = initializeLibs(); + static boolean initializeLibs() { + System.loadLibrary(LIB_NAME); + return true; + } + """, + """ + private MyEnum(long selfPointer, SwiftArena swiftArena) { + super(selfPointer, swiftArena); + } + """, + """ + public static MyEnum wrapMemoryAddressUnsafe(long selfPointer, SwiftArena swiftArena) { + return new MyEnum(selfPointer, swiftArena); + } + """, + """ + private static native void $destroy(long selfPointer); + """, + """ + @Override + protected Runnable $createDestroyFunction() { + long self$ = this.$memoryAddress(); + if (CallTraces.TRACE_DOWNCALLS) { + CallTraces.traceDowncall("MyEnum.$createDestroyFunction", + "this", this, + "self", self$); + } + return new Runnable() { + @Override + public void run() { + if (CallTraces.TRACE_DOWNCALLS) { + CallTraces.traceDowncall("MyEnum.$destroy", "self", self$); + } + MyEnum.$destroy(self$); + } + }; + """ + ]) + } + + @Test + func generatesDiscriminator_java() throws { + try assertOutput( + input: source, + .jni, .java, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + public enum Discriminator { + FIRST, + SECOND, + THIRD + } + """, + """ + public Discriminator getDiscriminator() { + return Discriminator.values()[$getDiscriminator(this.$memoryAddress())]; + } + """, + """ + private static native int $getDiscriminator(long self); + """ + ]) + } + + @Test + func generatesDiscriminator_swift() throws { + try assertOutput( + input: source, + .jni, .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + @_cdecl("Java_com_example_swift_MyEnum__00024getDiscriminator__J") + func Java_com_example_swift_MyEnum__00024getDiscriminator__J(environment: UnsafeMutablePointer!, thisClass: jclass, selfPointer: jlong) -> jint { + ... + switch (self$.pointee) { + case .first: return 0 + case .second: return 1 + case .third: return 2 + } + } + """ + ]) + } + + @Test + func generatesCases_java() throws { + try assertOutput( + input: source, + .jni, .java, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + public sealed interface Case {} + """, + """ + public Case getCase() { + Discriminator discriminator = this.getDiscriminator(); + switch (discriminator) { + case FIRST: return this.getAsFirst().orElseThrow(); + case SECOND: return this.getAsSecond().orElseThrow(); + case THIRD: return this.getAsThird().orElseThrow(); + } + throw new RuntimeException("Unknown discriminator value " + discriminator); + } + """, + """ + public record First() implements Case { + record $NativeParameters() {} + } + """, + """ + public record Second(java.lang.String arg0) implements Case { + record $NativeParameters(java.lang.String arg0) {} + } + """, + """ + public record Third(long x, int y) implements Case { + record $NativeParameters(long x, int y) {} + } + """ + ]) + } + + @Test + func generatesCaseInitializers_java() throws { + try assertOutput( + input: source, + .jni, .java, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + public static MyEnum first(SwiftArena swiftArena$) { + return MyEnum.wrapMemoryAddressUnsafe(MyEnum.$first(), swiftArena$); + } + """, + """ + public static MyEnum second(java.lang.String arg0, SwiftArena swiftArena$) { + return MyEnum.wrapMemoryAddressUnsafe(MyEnum.$second(arg0), swiftArena$); + } + """, + """ + public static MyEnum third(long x, int y, SwiftArena swiftArena$) { + return MyEnum.wrapMemoryAddressUnsafe(MyEnum.$third(x, y), swiftArena$); + } + """ + ]) + } + + @Test + func generatesCaseInitializers_swift() throws { + try assertOutput( + input: source, + .jni, .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + @_cdecl("Java_com_example_swift_MyEnum__00024first__") + func Java_com_example_swift_MyEnum__00024first__(environment: UnsafeMutablePointer!, thisClass: jclass) -> jlong { + let result$ = UnsafeMutablePointer.allocate(capacity: 1) + result$.initialize(to: MyEnum.first) + let resultBits$ = Int64(Int(bitPattern: result$)) + return resultBits$.getJNIValue(in: environment!) + } + """, + """ + @_cdecl("Java_com_example_swift_MyEnum__00024second__Ljava_lang_String_2") + func Java_com_example_swift_MyEnum__00024second__Ljava_lang_String_2(environment: UnsafeMutablePointer!, thisClass: jclass, arg0: jstring?) -> jlong { + let result$ = UnsafeMutablePointer.allocate(capacity: 1) + result$.initialize(to: MyEnum.second(String(fromJNI: arg0, in: environment!))) + let resultBits$ = Int64(Int(bitPattern: result$)) + return resultBits$.getJNIValue(in: environment!) + } + """, + """ + @_cdecl("Java_com_example_swift_MyEnum__00024third__JI") + func Java_com_example_swift_MyEnum__00024third__JI(environment: UnsafeMutablePointer!, thisClass: jclass, x: jlong, y: jint) -> jlong { + let result$ = UnsafeMutablePointer.allocate(capacity: 1) + result$.initialize(to: MyEnum.third(x: Int64(fromJNI: x, in: environment!), y: Int32(fromJNI: y, in: environment!))) + let resultBits$ = Int64(Int(bitPattern: result$)) + return resultBits$.getJNIValue(in: environment!) + } + """ + ]) + } + + @Test + func generatesGetAsCase_java() throws { + try assertOutput( + input: source, + .jni, .java, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + public Optional getAsFirst() { + if (getDiscriminator() != Discriminator.FIRST) { + return Optional.empty(); + } + return Optional.of(new First()); + } + """, + """ + public Optional getAsSecond() { + if (getDiscriminator() != Discriminator.SECOND) { + return Optional.empty(); + } + Second.$NativeParameters $nativeParameters = MyEnum.$getAsSecond(this.$memoryAddress()); + return Optional.of(new Second($nativeParameters.arg0)); + } + """, + """ + public Optional getAsThird() { + if (getDiscriminator() != Discriminator.THIRD) { + return Optional.empty(); + } + Third.$NativeParameters $nativeParameters = MyEnum.$getAsThird(this.$memoryAddress()); + return Optional.of(new Third($nativeParameters.x, $nativeParameters.y)); + } + """ + ]) + } + + @Test + func generatesGetAsCase_swift() throws { + try assertOutput( + input: source, + .jni, .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + @_cdecl("Java_com_example_swift_MyEnum__00024getAsSecond__J") + func Java_com_example_swift_MyEnum__00024getAsSecond__J(environment: UnsafeMutablePointer!, thisClass: jclass, self: jlong) -> jobject? { + ... + guard case .second(let _0) = self$.pointee else { + fatalError("Expected enum case 'second', but was '\\(self$.pointee)'!") + } + let cache$ = _JNI_MyEnum.myEnumSecondCache + let class$ = cache$.javaClass + let method$ = _JNIMethodIDCache.Method(name: "", signature: "(Ljava/lang/String;)V") + let constructorID$ = cache$[method$] + return withVaList([_0.getJNIValue(in: environment!) ?? 0]) { + return environment.interface.NewObjectV(environment, class$, constructorID$, $0) + } + } + """, + """ + @_cdecl("Java_com_example_swift_MyEnum__00024getAsThird__J") + func Java_com_example_swift_MyEnum__00024getAsThird__J(environment: UnsafeMutablePointer!, thisClass: jclass, self: jlong) -> jobject? { + ... + guard case .third(let x, let y) = self$.pointee else { + fatalError("Expected enum case 'third', but was '\\(self$.pointee)'!") + } + let cache$ = _JNI_MyEnum.myEnumThirdCache + let class$ = cache$.javaClass + let method$ = _JNIMethodIDCache.Method(name: "", signature: "(JI)V") + let constructorID$ = cache$[method$] + return withVaList([x.getJNIValue(in: environment!), y.getJNIValue(in: environment!)]) { + return environment.interface.NewObjectV(environment, class$, constructorID$, $0) + } + } + """ + ]) + } +}