From 3fa3d449ec60bb524c34ed461973319ea7b10074 Mon Sep 17 00:00:00 2001 From: Mads Odgaard Date: Mon, 1 Dec 2025 08:24:05 +0100 Subject: [PATCH 01/14] Fix `allowGlobalAutomatic` mode for interfaces (#466) --- ...t2JavaGenerator+JavaBindingsPrinting.swift | 13 +++++++- .../MemoryManagementModeTests.swift | 30 +++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift index 07d23dc0f..1e18cbe8f 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift @@ -424,7 +424,11 @@ extension JNISwift2JavaGenerator { ) } - private func printJavaBindingWrapperMethod(_ printer: inout CodePrinter, _ decl: ImportedFunc, signaturesOnly: Bool) { + private func printJavaBindingWrapperMethod( + _ printer: inout CodePrinter, + _ decl: ImportedFunc, + signaturesOnly: Bool + ) { guard let translatedDecl = translatedDecl(for: decl) else { fatalError("Decl was not translated, \(decl)") } @@ -470,6 +474,13 @@ extension JNISwift2JavaGenerator { if let importedFunc { printDeclDocumentation(&printer, importedFunc) } + var modifiers = modifiers + + // If we are a protocol, we emit this as default method + if importedFunc?.parentType?.asNominalTypeDeclaration?.kind == .protocol { + modifiers.insert("default", at: 1) + } + printer.printBraceBlock("\(annotationsStr)\(modifiers.joined(separator: " ")) \(resultType) \(translatedDecl.name)(\(parametersStr))\(throwsClause)") { printer in let globalArenaName = "SwiftMemoryManagement.GLOBAL_SWIFT_JAVA_ARENA" let arguments = translatedDecl.translatedFunctionSignature.parameters.map(\.parameter.name) + [globalArenaName] diff --git a/Tests/JExtractSwiftTests/MemoryManagementModeTests.swift b/Tests/JExtractSwiftTests/MemoryManagementModeTests.swift index 6017c9875..2228aad88 100644 --- a/Tests/JExtractSwiftTests/MemoryManagementModeTests.swift +++ b/Tests/JExtractSwiftTests/MemoryManagementModeTests.swift @@ -74,4 +74,34 @@ struct MemoryManagementModeTests { ] ) } + + @Test + func allowGlobalAutomatic_protocol() throws { + var config = Configuration() + config.memoryManagementMode = .allowGlobalAutomatic + + try assertOutput( + input: + """ + public class MyClass {} + + public protocol MyProtocol { + public func f() -> MyClass + } + """, + config: config, + .jni, .java, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + public default MyClass f() { + return f(SwiftMemoryManagement.GLOBAL_SWIFT_JAVA_ARENA); + } + """, + """ + public MyClass f(SwiftArena swiftArena$); + """ + ] + ) + } } From 6eb4705a483a1455563bf2d4f098c7c2e5e5cdf2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Bukowiecki?= Date: Mon, 1 Dec 2025 08:24:38 +0100 Subject: [PATCH 02/14] Improve readme section of Java installation from SDKMan . (#461) Co-authored-by: pelekon <13712101+pelekon@users.noreply.github.com> --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index abca3b9dd..7494366e2 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,12 @@ Alternatively, you can use a JDK manager like [sdkman](https://sdkman.io/install $ export JAVA_HOME="$(sdk home java current)" ``` +E.g sdkman install command: + +```bash +sdk install java 25.0.1-amzn +``` + ## Self-publish supporting Java libraries Swift-java relies on supporting libraries that are under active development and not yet published to Maven Central. To use the project, you'll need to self-publish these libraries locally so your Java project can depend on them. From 8d6365c29072f339849e8e06a2dbd10a65b96d51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Bukowiecki?= Date: Mon, 1 Dec 2025 08:26:30 +0100 Subject: [PATCH 03/14] jextract (ffm, jni): Subscripts support (#459) Co-authored-by: pelekon <13712101+pelekon@users.noreply.github.com> --- .../MySwiftLibrary/MySwiftStruct.swift | 22 ++ .../swift/swiftkitffm/MySwiftStructTest.java | 22 ++ .../MySwiftLibrary/MySwiftStruct.swift | 22 ++ .../com/example/swift/MySwiftStructTest.java | 22 ++ ...Swift2JavaGenerator+FunctionLowering.swift | 19 +- ...MSwift2JavaGenerator+JavaTranslation.swift | 4 +- Sources/JExtractSwiftLib/ImportedDecls.swift | 4 + ...ISwift2JavaGenerator+JavaTranslation.swift | 4 +- ...ift2JavaGenerator+SwiftThunkPrinting.swift | 13 ++ .../JExtractSwiftLib/Swift2JavaVisitor.swift | 151 +++++++++---- .../SwiftTypes/SwiftFunctionSignature.swift | 149 +++++++++--- .../JExtractSwiftLib/ThunkNameRegistry.swift | 4 +- .../Documentation.docc/SupportedFeatures.md | 2 +- .../FFM/FFMSubscriptsTests.swift | 212 ++++++++++++++++++ .../JNI/JNISubscriptsTests.swift | 155 +++++++++++++ 15 files changed, 717 insertions(+), 88 deletions(-) create mode 100644 Tests/JExtractSwiftTests/FFM/FFMSubscriptsTests.swift create mode 100644 Tests/JExtractSwiftTests/JNI/JNISubscriptsTests.swift diff --git a/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftStruct.swift b/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftStruct.swift index 363e06834..5b5c2d322 100644 --- a/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftStruct.swift +++ b/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftStruct.swift @@ -16,10 +16,14 @@ public struct MySwiftStruct { private var cap: Int private var len: Int + private var subscriptValue: Int + private var subscriptArray: [Int] public init(cap: Int, len: Int) { self.cap = cap self.len = len + self.subscriptValue = 0 + self.subscriptArray = [10, 20, 15, 75] } public func voidMethod() { @@ -61,4 +65,22 @@ public struct MySwiftStruct { public func makeRandomIntMethod() -> Int { return Int.random(in: 1..<256) } + + public func getSubscriptValue() -> Int { + return self.subscriptValue + } + + public func getSubscriptArrayValue(index: Int) -> Int { + return self.subscriptArray[index] + } + + public subscript() -> Int { + get { return subscriptValue } + set { subscriptValue = newValue } + } + + public subscript(index: Int) -> Int { + get { return subscriptArray[index] } + set { subscriptArray[index] = newValue } + } } diff --git a/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/org/swift/swiftkitffm/MySwiftStructTest.java b/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/org/swift/swiftkitffm/MySwiftStructTest.java index 6b994137c..d904f7e82 100644 --- a/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/org/swift/swiftkitffm/MySwiftStructTest.java +++ b/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/org/swift/swiftkitffm/MySwiftStructTest.java @@ -33,4 +33,26 @@ void create_struct() { assertEquals(len, struct.getLength()); } } + + @Test + void testSubscript() { + try (var arena = AllocatingSwiftArena.ofConfined()) { + MySwiftStruct s = MySwiftStruct.init(1337, 42, arena); + long currentValue = s.getSubscript(); + s.setSubscript(66); + assertEquals(0, currentValue); + assertEquals(66, s.getSubscriptValue()); + } + } + + @Test + void testSubscriptWithParams() { + try (var arena = AllocatingSwiftArena.ofConfined()) { + MySwiftStruct s = MySwiftStruct.init(1337, 42, arena); + long currentValue = s.getSubscript(1); + s.setSubscript(1, 66); + assertEquals(20, currentValue); + assertEquals(66, s.getSubscriptArrayValue(1)); + } + } } diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftStruct.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftStruct.swift index ddd77132d..34686f410 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftStruct.swift +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftStruct.swift @@ -15,10 +15,14 @@ public struct MySwiftStruct { private var cap: Int64 public var len: Int64 + private var subscriptValue: Int64 + private var subscriptArray: [Int64] public init(cap: Int64, len: Int64) { self.cap = cap self.len = len + self.subscriptValue = 0 + self.subscriptArray = [10, 20, 15, 75] } public init?(doInit: Bool) { @@ -38,4 +42,22 @@ public struct MySwiftStruct { self.cap += value return self.cap } + + public func getSubscriptValue() -> Int64 { + return self.subscriptValue + } + + public func getSubscriptArrayValue(index: Int64) -> Int64 { + return self.subscriptArray[Int(index)] + } + + public subscript() -> Int64 { + get { return subscriptValue } + set { subscriptValue = newValue } + } + + public subscript(index: Int64) -> Int64 { + get { return subscriptArray[Int(index)] } + set { subscriptArray[Int(index)] = newValue } + } } diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/MySwiftStructTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/MySwiftStructTest.java index e52e19591..24b1fdbf9 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/MySwiftStructTest.java +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/MySwiftStructTest.java @@ -61,4 +61,26 @@ void increaseCap() { assertEquals(1347, s.getCapacity()); } } + + @Test + void testSubscript() { + try (var arena = SwiftArena.ofConfined()) { + MySwiftStruct s = MySwiftStruct.init(1337, 42, arena); + long currentValue = s.getSubscript(); + s.setSubscript(66); + assertEquals(0, currentValue); + assertEquals(66, s.getSubscriptValue()); + } + } + + @Test + void testSubscriptWithParams() { + try (var arena = SwiftArena.ofConfined()) { + MySwiftStruct s = MySwiftStruct.init(1337, 42, arena); + long currentValue = s.getSubscript(1); + s.setSubscript(1, 66); + assertEquals(20, currentValue); + assertEquals(66, s.getSubscriptArrayValue(1)); + } + } } \ No newline at end of file diff --git a/Sources/JExtractSwiftLib/FFM/CDeclLowering/FFMSwift2JavaGenerator+FunctionLowering.swift b/Sources/JExtractSwiftLib/FFM/CDeclLowering/FFMSwift2JavaGenerator+FunctionLowering.swift index 832aff262..a7da370b3 100644 --- a/Sources/JExtractSwiftLib/FFM/CDeclLowering/FFMSwift2JavaGenerator+FunctionLowering.swift +++ b/Sources/JExtractSwiftLib/FFM/CDeclLowering/FFMSwift2JavaGenerator+FunctionLowering.swift @@ -812,11 +812,10 @@ extension LoweredFunctionSignature { // Build callee expression. let callee: ExprSyntax = if let selfExpr { - if case .initializer = apiKind { + switch apiKind { // Don't bother to create explicit ${Self}.init expression. - selfExpr - } else { - ExprSyntax(MemberAccessExprSyntax(base: selfExpr, name: .identifier(swiftAPIName))) + case .initializer, .subscriptGetter, .subscriptSetter: selfExpr + default: ExprSyntax(MemberAccessExprSyntax(base: selfExpr, name: .identifier(swiftAPIName))) } } else { ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier(swiftAPIName))) @@ -845,6 +844,18 @@ extension LoweredFunctionSignature { case .enumCase: // This should not be called, but let's fatalError. fatalError("Enum cases are not supported with FFM.") + + case .subscriptGetter: + let parameters = paramExprs.map { $0.description }.joined(separator: ", ") + resultExpr = "\(callee)[\(raw: parameters)]" + case .subscriptSetter: + assert(paramExprs.count >= 1) + + var argumentsWithoutNewValue = paramExprs + let newValueArgument = argumentsWithoutNewValue.removeLast() + + let parameters = argumentsWithoutNewValue.map { $0.description }.joined(separator: ", ") + resultExpr = "\(callee)[\(raw: parameters)] = \(newValueArgument)" } // Lower the result. diff --git a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift index 72da323c1..76284b787 100644 --- a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift +++ b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift @@ -144,8 +144,8 @@ extension FFMSwift2JavaGenerator { // Name. let javaName = switch decl.apiKind { - case .getter: decl.javaGetterName - case .setter: decl.javaSetterName + case .getter, .subscriptGetter: decl.javaGetterName + case .setter, .subscriptSetter: decl.javaSetterName case .function, .initializer, .enumCase: decl.name } diff --git a/Sources/JExtractSwiftLib/ImportedDecls.swift b/Sources/JExtractSwiftLib/ImportedDecls.swift index 9ba1cd6f6..b9cc2d497 100644 --- a/Sources/JExtractSwiftLib/ImportedDecls.swift +++ b/Sources/JExtractSwiftLib/ImportedDecls.swift @@ -23,6 +23,8 @@ package enum SwiftAPIKind { case getter case setter case enumCase + case subscriptGetter + case subscriptSetter } /// Describes a Swift nominal type (e.g., a class, struct, enum) that has been @@ -179,6 +181,8 @@ public final class ImportedFunc: ImportedDecl, CustomStringConvertible { case .setter: "setter:" case .enumCase: "case:" case .function, .initializer: "" + case .subscriptGetter: "subscriptGetter:" + case .subscriptSetter: "subscriptSetter:" } let context = if let parentType { diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift index 9c3cdbf1a..c8e4bbf34 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift @@ -176,8 +176,8 @@ extension JNISwift2JavaGenerator { // Name. let javaName = switch decl.apiKind { - case .getter: decl.javaGetterName - case .setter: decl.javaSetterName + case .getter, .subscriptGetter: decl.javaGetterName + case .setter, .subscriptSetter: decl.javaSetterName case .function, .initializer, .enumCase: decl.name } diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift index 91a0b0e73..d3b477326 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift @@ -348,6 +348,19 @@ extension JNISwift2JavaGenerator { } result = "\(callee).\(decl.name) = \(newValueArgument)" + case .subscriptGetter: + let parameters = arguments.joined(separator: ", ") + result = "\(callee)[\(parameters)]" + case .subscriptSetter: + guard let newValueArgument = arguments.last else { + fatalError("Setter did not contain newValue parameter: \(decl)") + } + + var argumentsWithoutNewValue = arguments + argumentsWithoutNewValue.removeLast() + + let parameters = argumentsWithoutNewValue.joined(separator: ", ") + result = "\(callee)[\(parameters)] = \(newValueArgument)" } // Lower the result. diff --git a/Sources/JExtractSwiftLib/Swift2JavaVisitor.swift b/Sources/JExtractSwiftLib/Swift2JavaVisitor.swift index 247b26626..ab1ce32fe 100644 --- a/Sources/JExtractSwiftLib/Swift2JavaVisitor.swift +++ b/Sources/JExtractSwiftLib/Swift2JavaVisitor.swift @@ -13,9 +13,9 @@ //===----------------------------------------------------------------------===// import Foundation +import SwiftJavaConfigurationShared import SwiftParser import SwiftSyntax -import SwiftJavaConfigurationShared final class Swift2JavaVisitor { let translator: Swift2JavaTranslator @@ -53,9 +53,9 @@ final class Swift2JavaVisitor { case .extensionDecl(let node): self.visit(extensionDecl: node, in: parent, sourceFilePath: sourceFilePath) case .typeAliasDecl: - break // TODO: Implement; https://github.com/swiftlang/swift-java/issues/338 + break // TODO: Implement; https://github.com/swiftlang/swift-java/issues/338 case .associatedTypeDecl: - break // TODO: Implement associated types + break // TODO: Implement associated types case .initializerDecl(let node): self.visit(initializerDecl: node, in: parent) @@ -63,9 +63,8 @@ final class Swift2JavaVisitor { self.visit(functionDecl: node, in: parent, sourceFilePath: sourceFilePath) case .variableDecl(let node): self.visit(variableDecl: node, in: parent, sourceFilePath: sourceFilePath) - case .subscriptDecl: - // TODO: Implement subscripts - break + case .subscriptDecl(let node): + self.visit(subscriptDecl: node, in: parent) case .enumCaseDecl(let node): self.visit(enumCaseDecl: node, in: parent) @@ -75,7 +74,8 @@ final class Swift2JavaVisitor { } func visit( - nominalDecl node: some DeclSyntaxProtocol & DeclGroupSyntax & NamedDeclSyntax & WithAttributesSyntax & WithModifiersSyntax, + nominalDecl node: some DeclSyntaxProtocol & DeclGroupSyntax & NamedDeclSyntax + & WithAttributesSyntax & WithModifiersSyntax, in parent: ImportedNominalType?, sourceFilePath: String ) { @@ -115,7 +115,7 @@ final class Swift2JavaVisitor { } func visit( - functionDecl node: FunctionDeclSyntax, + functionDecl node: FunctionDeclSyntax, in typeContext: ImportedNominalType?, sourceFilePath: String ) { @@ -154,7 +154,7 @@ final class Swift2JavaVisitor { } func visit( - enumCaseDecl node: EnumCaseDeclSyntax, + enumCaseDecl node: EnumCaseDeclSyntax, in typeContext: ImportedNominalType? ) { guard let typeContext else { @@ -200,7 +200,7 @@ final class Swift2JavaVisitor { } func visit( - variableDecl node: VariableDeclSyntax, + variableDecl node: VariableDeclSyntax, in typeContext: ImportedNominalType?, sourceFilePath: String ) { @@ -216,37 +216,21 @@ final class Swift2JavaVisitor { self.log.debug("Import variable: \(node.kind) '\(node.qualifiedNameForDebug)'") - func importAccessor(kind: SwiftAPIKind) throws { - let signature = try SwiftFunctionSignature( - node, - isSet: kind == .setter, - enclosingType: typeContext?.swiftType, - lookupContext: translator.lookupContext - ) - - let imported = ImportedFunc( - module: translator.swiftModuleName, - swiftDecl: node, - name: varName, - apiKind: kind, - functionSignature: signature - ) - - log.debug("Record imported variable accessor \(kind == .getter ? "getter" : "setter"):\(node.qualifiedNameForDebug)") - if let typeContext { - typeContext.variables.append(imported) - } else { - translator.importedGlobalVariables.append(imported) - } - } - do { let supportedAccessors = node.supportedAccessorKinds(binding: binding) if supportedAccessors.contains(.get) { - try importAccessor(kind: .getter) + try importAccessor( + from: DeclSyntax(node), + in: typeContext, + kind: .getter, + name: varName) } if supportedAccessors.contains(.set) { - try importAccessor(kind: .setter) + try importAccessor( + from: DeclSyntax(node), + in: typeContext, + kind: .setter, + name: varName) } } catch { self.log.debug("Failed to import: \(node.qualifiedNameForDebug); \(error)") @@ -289,10 +273,89 @@ final class Swift2JavaVisitor { typeContext.initializers.append(imported) } + private func visit( + subscriptDecl node: SubscriptDeclSyntax, + in typeContext: ImportedNominalType?, + ) { + guard node.shouldExtract(config: config, log: log, in: typeContext) else { + return + } + + guard let accessorBlock = node.accessorBlock else { + return + } + + let name = "subscript" + let accessors = accessorBlock.supportedAccessorKinds() + + do { + if accessors.contains(.get) { + try importAccessor( + from: DeclSyntax(node), + in: typeContext, + kind: .subscriptGetter, + name: name) + } + if accessors.contains(.set) { + try importAccessor( + from: DeclSyntax(node), + in: typeContext, + kind: .subscriptSetter, + name: name) + } + } catch { + self.log.debug("Failed to import: \(node.qualifiedNameForDebug); \(error)") + } + } + + private func importAccessor( + from node: DeclSyntax, + in typeContext: ImportedNominalType?, + kind: SwiftAPIKind, + name: String + ) throws { + let signature: SwiftFunctionSignature + + switch node.as(DeclSyntaxEnum.self) { + case .variableDecl(let varNode): + signature = try SwiftFunctionSignature( + varNode, + isSet: kind == .setter, + enclosingType: typeContext?.swiftType, + lookupContext: translator.lookupContext) + case .subscriptDecl(let subscriptNode): + signature = try SwiftFunctionSignature( + subscriptNode, + isSet: kind == .subscriptSetter, + enclosingType: typeContext?.swiftType, + lookupContext: translator.lookupContext) + default: + log.warning("Not supported declaration type \(node.kind) while calling importAccessor!") + return + } + + let imported = ImportedFunc( + module: translator.swiftModuleName, + swiftDecl: node, + name: name, + apiKind: kind, + functionSignature: signature + ) + + log.debug( + "Record imported variable accessor \(kind == .getter || kind == .subscriptGetter ? "getter" : "setter"):\(node.qualifiedNameForDebug)" + ) + if let typeContext { + typeContext.variables.append(imported) + } else { + translator.importedGlobalVariables.append(imported) + } + } + private func synthesizeRawRepresentableConformance( enumDecl node: EnumDeclSyntax, in parent: ImportedNominalType? - ) { + ) { guard let imported = translator.importedNominalType(node, parent: parent) else { return } @@ -304,7 +367,9 @@ final class Swift2JavaVisitor { ), inheritanceType.isRawTypeCompatible { - if !imported.variables.contains(where: { $0.name == "rawValue" && $0.functionSignature.result.type != inheritanceType }) { + 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, sourceFilePath: imported.sourceFilePath) } @@ -312,7 +377,11 @@ final class Swift2JavaVisitor { // FIXME: why is this un-used 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 }) { + 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, sourceFilePath: imported.sourceFilePath) } @@ -330,7 +399,9 @@ extension DeclSyntaxProtocol where Self: WithModifiersSyntax & WithAttributesSyn } guard meetsRequiredAccessLevel else { - log.debug("Skip import '\(self.qualifiedNameForDebug)': not at least \(config.effectiveMinimumInputAccessLevelMode)") + log.debug( + "Skip import '\(self.qualifiedNameForDebug)': not at least \(config.effectiveMinimumInputAccessLevelMode)" + ) return false } guard !attributes.contains(where: { $0.isJava }) else { diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftFunctionSignature.swift b/Sources/JExtractSwiftLib/SwiftTypes/SwiftFunctionSignature.swift index 1f2fbd365..804769e59 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftFunctionSignature.swift +++ b/Sources/JExtractSwiftLib/SwiftTypes/SwiftFunctionSignature.swift @@ -271,31 +271,10 @@ extension SwiftFunctionSignature { lookupContext: SwiftTypeLookupContext ) throws { - // If this is a member of a type, so we will have a self parameter. Figure out the - // type and convention for the self parameter. - if let enclosingType { - var isStatic = false - for modifier in varNode.modifiers { - switch modifier.name.tokenKind { - case .keyword(.static): isStatic = true - case .keyword(.class): throw SwiftFunctionTranslationError.classMethod(modifier.name) - default: break - } - } - - if isStatic { - self.selfParameter = .staticMethod(enclosingType) - } else { - self.selfParameter = .instance( - SwiftParameter( - convention: isSet && !enclosingType.isReferenceType ? .inout : .byValue, - type: enclosingType - ) - ) - } - } else { - self.selfParameter = nil - } + self.selfParameter = try Self.variableSelfParameter( + for: DeclSyntax(varNode), + enclosingType: enclosingType, + isSet: isSet) guard let binding = varNode.bindings.first, varNode.bindings.count == 1 else { throw SwiftFunctionTranslationError.multipleBindings(varNode) @@ -323,7 +302,9 @@ extension SwiftFunctionSignature { self.effectSpecifiers = effectSpecifiers ?? [] if isSet { - self.parameters = [SwiftParameter(convention: .byValue, parameterName: "newValue", type: valueType)] + self.parameters = [ + SwiftParameter(convention: .byValue, parameterName: "newValue", type: valueType) + ] self.result = .void } else { self.parameters = [] @@ -333,6 +314,50 @@ extension SwiftFunctionSignature { self.genericRequirements = [] } + init( + _ subscriptNode: SubscriptDeclSyntax, + isSet: Bool, + enclosingType: SwiftType?, + lookupContext: SwiftTypeLookupContext + ) throws { + self.selfParameter = try Self.variableSelfParameter( + for: DeclSyntax(subscriptNode), + enclosingType: enclosingType, + isSet: isSet) + + let valueType: SwiftType = try SwiftType(subscriptNode.returnClause.type, lookupContext: lookupContext) + var nodeParameters = try subscriptNode.parameterClause.parameters.map { param in + try SwiftParameter(param, lookupContext: lookupContext) + } + + var effectSpecifiers: [SwiftEffectSpecifier]? = nil + switch subscriptNode.accessorBlock?.accessors { + case .getter(let getter): + if let getter = getter.as(AccessorDeclSyntax.self) { + effectSpecifiers = try Self.effectSpecifiers(from: getter) + } + case .accessors(let accessors): + if let getter = accessors.first(where: { $0.accessorSpecifier.tokenKind == .keyword(.get) }) { + effectSpecifiers = try Self.effectSpecifiers(from: getter) + } + default: + break + } + + self.effectSpecifiers = effectSpecifiers ?? [] + + if isSet { + nodeParameters.append(SwiftParameter(convention: .byValue, parameterName: "newValue", type: valueType)) + self.result = .void + } else { + self.result = .init(convention: .direct, type: valueType) + } + + self.parameters = nodeParameters + self.genericParameters = [] + self.genericRequirements = [] + } + private static func effectSpecifiers(from decl: AccessorDeclSyntax) throws -> [SwiftEffectSpecifier] { var effectSpecifiers = [SwiftEffectSpecifier]() if decl.effectSpecifiers?.throwsClause != nil { @@ -343,27 +368,80 @@ extension SwiftFunctionSignature { } return effectSpecifiers } -} -extension VariableDeclSyntax { - struct SupportedAccessorKinds: OptionSet { - var rawValue: UInt8 + private static func variableSelfParameter( + for decl: DeclSyntax, + enclosingType: SwiftType?, + isSet: Bool + ) throws -> SwiftSelfParameter? { + let modifiers: DeclModifierListSyntax? = + switch decl.as(DeclSyntaxEnum.self) { + case .variableDecl(let varDecl): varDecl.modifiers + case .subscriptDecl(let subscriptDecl): subscriptDecl.modifiers + default: nil + } - static var get: Self = .init(rawValue: 1 << 0) - static var set: Self = .init(rawValue: 1 << 1) + guard let modifiers else { + return nil + } + + // If this is a member of a type, so we will have a self parameter. Figure out the + // type and convention for the self parameter. + if let enclosingType { + var isStatic = false + for modifier in modifiers { + switch modifier.name.tokenKind { + case .keyword(.static): isStatic = true + case .keyword(.class): throw SwiftFunctionTranslationError.classMethod(modifier.name) + default: break + } + } + + if isStatic { + return .staticMethod(enclosingType) + } else { + return .instance( + SwiftParameter( + convention: isSet && !enclosingType.isReferenceType ? .inout : .byValue, + type: enclosingType + ) + ) + } + } else { + return nil + } } +} +extension VariableDeclSyntax { /// Determine what operations (i.e. get and/or set) supported in this `VariableDeclSyntax` /// /// - Parameters: /// - binding the pattern binding in this declaration. - func supportedAccessorKinds(binding: PatternBindingSyntax) -> SupportedAccessorKinds { + func supportedAccessorKinds(binding: PatternBindingSyntax) -> AccessorBlockSyntax.SupportedAccessorKinds { if self.bindingSpecifier.tokenKind == .keyword(.let) { return [.get] } if let accessorBlock = binding.accessorBlock { - switch accessorBlock.accessors { + return accessorBlock.supportedAccessorKinds() + } + + return [.get, .set] + } +} + +extension AccessorBlockSyntax { + struct SupportedAccessorKinds: OptionSet { + var rawValue: UInt8 + + static var get: Self = .init(rawValue: 1 << 0) + static var set: Self = .init(rawValue: 1 << 1) + } + + /// Determine what operations (i.e. get and/or set) supported in this `AccessorBlockSyntax` + func supportedAccessorKinds() -> SupportedAccessorKinds { + switch self.accessors { case .getter: return [.get] case .accessors(let accessors): @@ -379,9 +457,6 @@ extension VariableDeclSyntax { } return [.get] } - } - - return [.get, .set] } } diff --git a/Sources/JExtractSwiftLib/ThunkNameRegistry.swift b/Sources/JExtractSwiftLib/ThunkNameRegistry.swift index 3369ec629..da2e95c1b 100644 --- a/Sources/JExtractSwiftLib/ThunkNameRegistry.swift +++ b/Sources/JExtractSwiftLib/ThunkNameRegistry.swift @@ -32,9 +32,9 @@ package struct ThunkNameRegistry { let suffix: String switch decl.apiKind { - case .getter: + case .getter, .subscriptGetter: suffix = "$get" - case .setter: + case .setter, .subscriptSetter: suffix = "$set" default: suffix = decl.functionSignature.parameters diff --git a/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md b/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md index 7e8c66d3d..849b7f01f 100644 --- a/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md +++ b/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md @@ -83,7 +83,7 @@ SwiftJava's `swift-java jextract` tool automates generating Java bindings from S | Ownership modifiers: `inout`, `borrowing`, `consuming` | ❌ | ❌ | | Default parameter values: `func p(name: String = "")` | ❌ | ❌ | | Operators: `+`, `-`, user defined | ❌ | ❌ | -| Subscripts: `subscript()` | ❌ | ❌ | +| Subscripts: `subscript()` | ✅ | ✅ | | Equatable | ❌ | ❌ | | Pointers: `UnsafeRawPointer`, UnsafeBufferPointer (?) | 🟡 | ❌ | | Nested types: `struct Hello { struct World {} }` | ❌ | ✅ | diff --git a/Tests/JExtractSwiftTests/FFM/FFMSubscriptsTests.swift b/Tests/JExtractSwiftTests/FFM/FFMSubscriptsTests.swift new file mode 100644 index 000000000..89029dd70 --- /dev/null +++ b/Tests/JExtractSwiftTests/FFM/FFMSubscriptsTests.swift @@ -0,0 +1,212 @@ +//===----------------------------------------------------------------------===// +// +// 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 + +import JExtractSwiftLib +import Testing + +@Suite +struct FFMSubscriptsTests { + private let noParamsSubscriptSource = """ + public struct MyStruct { + private var testVariable: Double = 0 + + public subscript() -> Double { + get { return testVariable } + set { testVariable = newValue } + } + } + """ + + private let subscriptWithParamsSource = """ + public struct MyStruct { + private var testVariable: [Int32] = [] + + public subscript(index: Int32) -> Int32 { + get { return testVariable[Int(index)] } + set { testVariable[Int(index)] = newValue } + } + } + """ + + @Test("Test generation of JavaClass for subscript with no parameters") + func generatesJavaClassForNoParams() throws { + try assertOutput(input: noParamsSubscriptSource, .ffm, .java, expectedChunks: [ + """ + private static class swiftjava_SwiftModule_MyStruct_subscript$get { + private static final FunctionDescriptor DESC = FunctionDescriptor.of( + /* -> */SwiftValueLayout.SWIFT_DOUBLE, + /* self: */SwiftValueLayout.SWIFT_POINTER + ); + """, + """ + private static final MemorySegment ADDR = + SwiftModule.findOrThrow("swiftjava_SwiftModule_MyStruct_subscript$get"); + """, + """ + private static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); + public static double call(java.lang.foreign.MemorySegment self) { + try { + if (CallTraces.TRACE_DOWNCALLS) { + CallTraces.traceDowncall(self); + } + return (double) HANDLE.invokeExact(self); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + """, + """ + public double getSubscript() { + $ensureAlive(); + return swiftjava_SwiftModule_MyStruct_subscript$get.call(this.$memorySegment()); + """, + ]) + try assertOutput(input: noParamsSubscriptSource, .ffm, .java, expectedChunks: [ + """ + private static class swiftjava_SwiftModule_MyStruct_subscript$set { + private static final FunctionDescriptor DESC = FunctionDescriptor.ofVoid( + /* newValue: */SwiftValueLayout.SWIFT_DOUBLE, + /* self: */SwiftValueLayout.SWIFT_POINTER + ); + private static final MemorySegment ADDR = + SwiftModule.findOrThrow("swiftjava_SwiftModule_MyStruct_subscript$set"); + private static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); + public static void call(double newValue, java.lang.foreign.MemorySegment self) { + try { + if (CallTraces.TRACE_DOWNCALLS) { + CallTraces.traceDowncall(newValue, self); + } + HANDLE.invokeExact(newValue, self); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + """, + """ + public void setSubscript(double newValue) { + $ensureAlive(); + swiftjava_SwiftModule_MyStruct_subscript$set.call(newValue, this.$memorySegment()); + """ + ]) + } + + @Test("Test generation of JavaClass for subscript with parameters") + func generatesJavaClassForParameters() throws { + try assertOutput(input: subscriptWithParamsSource, .ffm, .java, expectedChunks: [ + """ + private static class swiftjava_SwiftModule_MyStruct_subscript$get { + private static final FunctionDescriptor DESC = FunctionDescriptor.of( + /* -> */SwiftValueLayout.SWIFT_INT32, + /* index: */SwiftValueLayout.SWIFT_INT32, + /* self: */SwiftValueLayout.SWIFT_POINTER + ); + private static final MemorySegment ADDR = + SwiftModule.findOrThrow("swiftjava_SwiftModule_MyStruct_subscript$get"); + private static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); + public static int call(int index, java.lang.foreign.MemorySegment self) { + try { + if (CallTraces.TRACE_DOWNCALLS) { + CallTraces.traceDowncall(index, self); + } + return (int) HANDLE.invokeExact(index, self); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + """, + """ + public int getSubscript(int index) { + $ensureAlive(); + return swiftjava_SwiftModule_MyStruct_subscript$get.call(index, this.$memorySegment()); + """, + ]) + try assertOutput(input: subscriptWithParamsSource, .ffm, .java, expectedChunks: [ + """ + private static class swiftjava_SwiftModule_MyStruct_subscript$set { + private static final FunctionDescriptor DESC = FunctionDescriptor.ofVoid( + /* index: */SwiftValueLayout.SWIFT_INT32, + /* newValue: */SwiftValueLayout.SWIFT_INT32, + /* self: */SwiftValueLayout.SWIFT_POINTER + ); + private static final MemorySegment ADDR = + SwiftModule.findOrThrow("swiftjava_SwiftModule_MyStruct_subscript$set"); + private static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); + public static void call(int index, int newValue, java.lang.foreign.MemorySegment self) { + try { + if (CallTraces.TRACE_DOWNCALLS) { + CallTraces.traceDowncall(index, newValue, self); + } + HANDLE.invokeExact(index, newValue, self); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + """, + """ + public void setSubscript(int index, int newValue) { + $ensureAlive(); + swiftjava_SwiftModule_MyStruct_subscript$set.call(index, newValue, this.$memorySegment()); + """, + ]) + } + + @Test("Test generation of Swift thunks for subscript without parameters") + func subscriptWithoutParamsMethodSwiftThunk() throws { + try assertOutput( + input: noParamsSubscriptSource, + .ffm, + .swift, + expectedChunks: [ + """ + @_cdecl("swiftjava_SwiftModule_MyStruct_subscript$get") + public func swiftjava_SwiftModule_MyStruct_subscript$get(_ self: UnsafeRawPointer) -> Double { + return self.assumingMemoryBound(to: MyStruct.self).pointee[] + } + """, + """ + @_cdecl("swiftjava_SwiftModule_MyStruct_subscript$set") + public func swiftjava_SwiftModule_MyStruct_subscript$set(_ newValue: Double, _ self: UnsafeMutableRawPointer) { + self.assumingMemoryBound(to: MyStruct.self).pointee[] = newValue + } + """ + ] + ) + } + + @Test("Test generation of Swift thunks for subscript with parameters") + func subscriptWithParamsMethodSwiftThunk() throws { + try assertOutput( + input: subscriptWithParamsSource, + .ffm, + .swift, + expectedChunks: [ + """ + @_cdecl("swiftjava_SwiftModule_MyStruct_subscript$get") + public func swiftjava_SwiftModule_MyStruct_subscript$get(_ index: Int32, _ self: UnsafeRawPointer) -> Int32 { + return self.assumingMemoryBound(to: MyStruct.self).pointee[index] + } + """, + """ + @_cdecl("swiftjava_SwiftModule_MyStruct_subscript$set") + public func swiftjava_SwiftModule_MyStruct_subscript$set(_ index: Int32, _ newValue: Int32, _ self: UnsafeMutableRawPointer) { + self.assumingMemoryBound(to: MyStruct.self).pointee[index] = newValue + } + """ + ] + ) + } +} diff --git a/Tests/JExtractSwiftTests/JNI/JNISubscriptsTests.swift b/Tests/JExtractSwiftTests/JNI/JNISubscriptsTests.swift new file mode 100644 index 000000000..0f7b131d0 --- /dev/null +++ b/Tests/JExtractSwiftTests/JNI/JNISubscriptsTests.swift @@ -0,0 +1,155 @@ +//===----------------------------------------------------------------------===// +// +// 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 JNISubscriptsTests { + private let noParamsSubscriptSource = """ + public struct MyStruct { + private var testVariable: Double = 0 + + public subscript() -> Double { + get { return testVariable } + set { testVariable = newValue } + } + } + """ + + private let subscriptWithParamsSource = """ + public struct MyStruct { + private var testVariable: [Int32] = [] + + public subscript(index: Int32) -> Int32 { + get { return testVariable[Int(index)] } + set { testVariable[Int(index)] = newValue } + } + } + """ + + @Test("Test generation of JavaClass for subscript with no parameters") + func generatesJavaClassForNoParams() throws { + try assertOutput(input: noParamsSubscriptSource, .jni, .java, expectedChunks: [ + """ + public double getSubscript() { + return MyStruct.$getSubscript(this.$memoryAddress()); + """, + """ + private static native double $getSubscript(long self); + """, + """ + public void setSubscript(double newValue) { + MyStruct.$setSubscript(newValue, this.$memoryAddress()); + """, + """ + private static native void $setSubscript(double newValue, long self); + """ + ]) + try assertOutput( + input: noParamsSubscriptSource, .jni, .java, + expectedChunks: [ + """ + private static native void $destroy(long selfPointer); + """ + ]) + } + + @Test("Test generation of JavaClass for subscript with parameters") + func generatesJavaClassForParameters() throws { + try assertOutput(input: subscriptWithParamsSource, .jni, .java, expectedChunks: [ + """ + public int getSubscript(int index) { + return MyStruct.$getSubscript(index, this.$memoryAddress()); + + """, + """ + private static native int $getSubscript(int index, long self); + """, + """ + public void setSubscript(int index, int newValue) { + MyStruct.$setSubscript(index, newValue, this.$memoryAddress()); + """, + """ + private static native void $setSubscript(int index, int newValue, long self); + """ + ]) + } + + @Test("Test generation of Swift thunks for subscript without parameters") + func subscriptWithoutParamsMethodSwiftThunk() throws { + try assertOutput( + input: noParamsSubscriptSource, + .jni, + .swift, + expectedChunks: [ + """ + @_cdecl("Java_com_example_swift_MyStruct__00024getSubscript__J") + func Java_com_example_swift_MyStruct__00024getSubscript__J(environment: UnsafeMutablePointer!, thisClass: jclass, self: jlong) -> jdouble { + assert(self != 0, "self memory address was null") + let selfBits$ = Int(Int64(fromJNI: self, in: environment)) + let self$ = UnsafeMutablePointer(bitPattern: selfBits$) + guard let self$ else { + fatalError("self memory address was null in call to \\(#function)!") + } + return self$.pointee[].getJNIValue(in: environment) + """, + """ + @_cdecl("Java_com_example_swift_MyStruct__00024setSubscript__DJ") + func Java_com_example_swift_MyStruct__00024setSubscript__DJ(environment: UnsafeMutablePointer!, thisClass: jclass, newValue: jdouble, self: jlong) { + assert(self != 0, "self memory address was null") + let selfBits$ = Int(Int64(fromJNI: self, in: environment)) + let self$ = UnsafeMutablePointer(bitPattern: selfBits$) + guard let self$ else { + fatalError("self memory address was null in call to \\(#function)!") + } + self$.pointee[] = Double(fromJNI: newValue, in: environment) + """ + ] + ) + } + + @Test("Test generation of Swift thunks for subscript with parameters") + func subscriptWithParamsMethodSwiftThunk() throws { + try assertOutput( + input: subscriptWithParamsSource, + .jni, + .swift, + expectedChunks: [ + """ + @_cdecl("Java_com_example_swift_MyStruct__00024getSubscript__IJ") + func Java_com_example_swift_MyStruct__00024getSubscript__IJ(environment: UnsafeMutablePointer!, thisClass: jclass, index: jint, self: jlong) -> jint { + assert(self != 0, "self memory address was null") + let selfBits$ = Int(Int64(fromJNI: self, in: environment)) + let self$ = UnsafeMutablePointer(bitPattern: selfBits$) + guard let self$ else { + fatalError("self memory address was null in call to \\(#function)!") + } + return self$.pointee[Int32(fromJNI: index, in: environment)].getJNIValue(in: environment) + """, + """ + @_cdecl("Java_com_example_swift_MyStruct__00024setSubscript__IIJ") + func Java_com_example_swift_MyStruct__00024setSubscript__IIJ(environment: UnsafeMutablePointer!, thisClass: jclass, index: jint, newValue: jint, self: jlong) { + assert(self != 0, "self memory address was null") + let selfBits$ = Int(Int64(fromJNI: self, in: environment)) + let self$ = UnsafeMutablePointer(bitPattern: selfBits$) + guard let self$ else { + fatalError("self memory address was null in call to \\(#function)!") + } + self$.pointee[Int32(fromJNI: index, in: environment)] = Int32(fromJNI: newValue, in: environment) + """ + ] + ) + } +} From c0c6fd593f68d14d7084a0a5935e02331991193d Mon Sep 17 00:00:00 2001 From: Mads Odgaard Date: Mon, 1 Dec 2025 11:56:29 +0100 Subject: [PATCH 04/14] Use thread environment instead for JNI methods (#465) --- Sources/SwiftJava/AnyJavaObject.swift | 2 +- .../SwiftJava/JavaObject+MethodCalls.swift | 50 ++++++++++++------- Sources/SwiftJava/JavaObjectHolder.swift | 2 + Tests/SwiftJavaTests/BasicRuntimeTests.swift | 11 ++++ 4 files changed, 46 insertions(+), 19 deletions(-) diff --git a/Sources/SwiftJava/AnyJavaObject.swift b/Sources/SwiftJava/AnyJavaObject.swift index fe77bdbd4..33a83159c 100644 --- a/Sources/SwiftJava/AnyJavaObject.swift +++ b/Sources/SwiftJava/AnyJavaObject.swift @@ -66,7 +66,7 @@ extension AnyJavaObject { javaHolder.object } - /// Retrieve the environment in which this Java object resides. + /// Retrieve the environment in which this Java object was created. public var javaEnvironment: JNIEnvironment { javaHolder.environment } diff --git a/Sources/SwiftJava/JavaObject+MethodCalls.swift b/Sources/SwiftJava/JavaObject+MethodCalls.swift index 4880f7503..0626be23a 100644 --- a/Sources/SwiftJava/JavaObject+MethodCalls.swift +++ b/Sources/SwiftJava/JavaObject+MethodCalls.swift @@ -104,7 +104,8 @@ extension AnyJavaObject { resultType: Result.Type ) throws -> jmethodID { // Retrieve the Java class instance from the object. - let environment = javaEnvironment + let environment = try JavaVirtualMachine.shared().environment() + let thisClass = try environment.translatingJNIExceptions { environment.interface.GetObjectClass(environment, javaThis) }! @@ -115,7 +116,7 @@ extension AnyJavaObject { methodName: methodName, parameterTypes: repeat each parameterTypes, resultType: Result.javaType, - in: javaEnvironment + in: environment ) } } @@ -126,7 +127,8 @@ extension AnyJavaObject { parameterTypes: repeat (each Param).Type ) throws -> jmethodID { // Retrieve the Java class instance from the object. - let environment = javaEnvironment + let environment = try JavaVirtualMachine.shared().environment() + let thisClass = try environment.translatingJNIExceptions { environment.interface.GetObjectClass(environment, javaThis) }! @@ -137,7 +139,7 @@ extension AnyJavaObject { methodName: methodName, parameterTypes: repeat each parameterTypes, resultType: .void, - in: javaEnvironment + in: environment ) } } @@ -167,8 +169,10 @@ extension AnyJavaObject { method: jmethodID, args: repeat each Param ) throws -> Result { + let environment = try JavaVirtualMachine.shared().environment() + return try Self.javaMethodCall( - in: javaEnvironment, + in: environment, this: javaThis, method: method, args: repeat each args @@ -229,8 +233,10 @@ extension AnyJavaObject { method: jmethodID, args: repeat each Param ) throws { + let environment = try JavaVirtualMachine.shared().environment() + try Self.javaMethodCall( - in: javaEnvironment, + in: environment, this: javaThis, method: method, args: repeat each args @@ -276,7 +282,7 @@ extension AnyJavaObject { private func getJNIFieldID(_ fieldName: String, fieldType: FieldType.Type) -> jfieldID? where FieldType: ~Copyable { let this = javaThis - let environment = javaEnvironment + let environment = try! JavaVirtualMachine.shared().environment() // Retrieve the Java class instance from the object. let thisClass = environment.interface.GetObjectClass(environment, this)! @@ -289,15 +295,19 @@ extension AnyJavaObject { fieldType fieldType: FieldType.Type ) -> FieldType where FieldType: ~Copyable { get { + let environment = try! JavaVirtualMachine.shared().environment() + let fieldID = getJNIFieldID(fieldName, fieldType: fieldType)! - let jniMethod = FieldType.jniFieldGet(in: javaEnvironment) - return FieldType(fromJNI: jniMethod(javaEnvironment, javaThis, fieldID), in: javaEnvironment) + let jniMethod = FieldType.jniFieldGet(in: environment) + return FieldType(fromJNI: jniMethod(environment, javaThis, fieldID), in: environment) } nonmutating set { + let environment = try! JavaVirtualMachine.shared().environment() + let fieldID = getJNIFieldID(fieldName, fieldType: fieldType)! - let jniMethod = FieldType.jniFieldSet(in: javaEnvironment) - jniMethod(javaEnvironment, javaThis, fieldID, newValue.getJNIValue(in: javaEnvironment)) + let jniMethod = FieldType.jniFieldSet(in: environment) + jniMethod(environment, javaThis, fieldID, newValue.getJNIValue(in: environment)) } } } @@ -311,7 +321,7 @@ extension JavaClass { resultType: Result.Type ) throws -> Result { let thisClass = javaThis - let environment = javaEnvironment + let environment = try! JavaVirtualMachine.shared().environment() // Compute the method signature so we can find the right method, then look up the // method within the class. @@ -345,7 +355,7 @@ extension JavaClass { arguments: repeat each Param ) throws { let thisClass = javaThis - let environment = javaEnvironment + let environment = try JavaVirtualMachine.shared().environment() // Compute the method signature so we can find the right method, then look up the // method within the class. @@ -372,7 +382,7 @@ extension JavaClass { /// Retrieve the JNI field ID for a field with the given name and type. private func getJNIStaticFieldID(_ fieldName: String, fieldType: FieldType.Type) -> jfieldID? { - let environment = javaEnvironment + let environment = try! JavaVirtualMachine.shared().environment() return environment.interface.GetStaticFieldID(environment, javaThis, fieldName, FieldType.jniMangling) } @@ -382,15 +392,19 @@ extension JavaClass { fieldType fieldType: FieldType.Type ) -> FieldType { get { + let environment = try! JavaVirtualMachine.shared().environment() + let fieldID = getJNIStaticFieldID(fieldName, fieldType: fieldType)! - let jniMethod = FieldType.jniStaticFieldGet(in: javaEnvironment) - return FieldType(fromJNI: jniMethod(javaEnvironment, javaThis, fieldID), in: javaEnvironment) + let jniMethod = FieldType.jniStaticFieldGet(in: environment) + return FieldType(fromJNI: jniMethod(environment, javaThis, fieldID), in: environment) } set { + let environment = try! JavaVirtualMachine.shared().environment() + let fieldID = getJNIStaticFieldID(fieldName, fieldType: fieldType)! - let jniMethod = FieldType.jniStaticFieldSet(in: javaEnvironment) - jniMethod(javaEnvironment, javaThis, fieldID, newValue.getJNIValue(in: javaEnvironment)) + let jniMethod = FieldType.jniStaticFieldSet(in: environment) + jniMethod(environment, javaThis, fieldID, newValue.getJNIValue(in: environment)) } } } diff --git a/Sources/SwiftJava/JavaObjectHolder.swift b/Sources/SwiftJava/JavaObjectHolder.swift index 5930da59b..b5e888351 100644 --- a/Sources/SwiftJava/JavaObjectHolder.swift +++ b/Sources/SwiftJava/JavaObjectHolder.swift @@ -32,6 +32,8 @@ public final class JavaObjectHolder { /// in Swift and the Java virtual machine is free to move or deallocate it. func forget() { if let object { + let environment = try! JavaVirtualMachine.shared().environment() + environment.interface.DeleteGlobalRef(environment, object) self.object = nil } diff --git a/Tests/SwiftJavaTests/BasicRuntimeTests.swift b/Tests/SwiftJavaTests/BasicRuntimeTests.swift index b6c18bee5..292c2a688 100644 --- a/Tests/SwiftJavaTests/BasicRuntimeTests.swift +++ b/Tests/SwiftJavaTests/BasicRuntimeTests.swift @@ -82,6 +82,17 @@ class BasicRuntimeTests: XCTestCase { let nullString = String(fromJNI: nil, in: environment) XCTAssertEqual(nullString, "") } + + func testCrossThreadAccess() async throws { + let environment = try jvm.environment() + let url = try URL("https://swift.org", environment: environment) + let string = await Task.detached { + // This should be called on a different thread + url.toString() + }.value + + XCTAssertEqual(string, "https://swift.org") + } } @JavaClass("org.swift.javakit.Nonexistent") From 2ed85e25ed3831c4c9fdb00e3c92fc4b62255885 Mon Sep 17 00:00:00 2001 From: Mads Odgaard Date: Mon, 1 Dec 2025 12:21:21 +0100 Subject: [PATCH 05/14] [jextract] Add async legacy mode (#462) --- .../java/com/example/swift/AsyncTest.java | 33 +-- .../com/example/swift/MySwiftClassTest.java | 4 +- ...ISwift2JavaGenerator+JavaTranslation.swift | 71 +++--- ...wift2JavaGenerator+NativeTranslation.swift | 14 +- .../JavaTypes/JavaType+JDK.swift | 5 + .../JavaTypes/JavaType+SwiftKit.swift | 5 + .../JExtract/JExtractAsyncFuncMode.swift | 2 +- ...ltCaches.swift => JNIMethodIDCaches.swift} | 30 +++ .../core/SimpleCompletableFuture.java | 223 ++++++++++++++++++ .../core/SimpleCompletableFutureTest.java | 185 +++++++++++++++ .../JNI/JNIAsyncTests.swift | 92 ++++++-- 11 files changed, 597 insertions(+), 67 deletions(-) rename Sources/SwiftJavaRuntimeSupport/{DefaultCaches.swift => JNIMethodIDCaches.swift} (70%) create mode 100644 SwiftKitCore/src/main/java/org/swift/swiftkit/core/SimpleCompletableFuture.java create mode 100644 SwiftKitCore/src/test/java/org/swift/swiftkit/core/SimpleCompletableFutureTest.java diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/AsyncTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/AsyncTest.java index 5fe7c1310..400844fdc 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/AsyncTest.java +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/AsyncTest.java @@ -26,31 +26,32 @@ import java.util.OptionalLong; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; import static org.junit.jupiter.api.Assertions.*; public class AsyncTest { @Test - void asyncSum() { - CompletableFuture future = MySwiftLibrary.asyncSum(10, 12); + void asyncSum() throws Exception { + Future future = MySwiftLibrary.asyncSum(10, 12); - Long result = future.join(); + Long result = future.get(); assertEquals(22, result); } @Test - void asyncSleep() { - CompletableFuture future = MySwiftLibrary.asyncSleep(); - future.join(); + void asyncSleep() throws Exception { + Future future = MySwiftLibrary.asyncSleep(); + future.get(); } @Test - void asyncCopy() { + void asyncCopy() throws Exception { try (var arena = SwiftArena.ofConfined()) { MySwiftClass obj = MySwiftClass.init(10, 5, arena); - CompletableFuture future = MySwiftLibrary.asyncCopy(obj, arena); + Future future = MySwiftLibrary.asyncCopy(obj, arena); - MySwiftClass result = future.join(); + MySwiftClass result = future.get(); assertEquals(10, result.getX()); assertEquals(5, result.getY()); @@ -59,7 +60,7 @@ void asyncCopy() { @Test void asyncThrows() { - CompletableFuture future = MySwiftLibrary.asyncThrows(); + Future future = MySwiftLibrary.asyncThrows(); ExecutionException ex = assertThrows(ExecutionException.class, future::get); @@ -70,14 +71,14 @@ void asyncThrows() { } @Test - void asyncOptional() { - CompletableFuture future = MySwiftLibrary.asyncOptional(42); - assertEquals(OptionalLong.of(42), future.join()); + void asyncOptional() throws Exception { + Future future = MySwiftLibrary.asyncOptional(42); + assertEquals(OptionalLong.of(42), future.get()); } @Test - void asyncString() { - CompletableFuture future = MySwiftLibrary.asyncString("hey"); - assertEquals("hey", future.join()); + void asyncString() throws Exception { + Future future = MySwiftLibrary.asyncString("hey"); + assertEquals("hey", future.get()); } } \ No newline at end of file diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/MySwiftClassTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/MySwiftClassTest.java index a58386299..860f1641c 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/MySwiftClassTest.java +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/MySwiftClassTest.java @@ -165,10 +165,10 @@ void addXWithJavaLong() { } @Test - void getAsyncVariable() { + void getAsyncVariable() throws Exception { try (var arena = SwiftArena.ofConfined()) { MySwiftClass c1 = MySwiftClass.init(20, 10, arena); - assertEquals(42, c1.getGetAsync().join()); + assertEquals(42, c1.getGetAsync().get()); } } } \ No newline at end of file diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift index c8e4bbf34..6a2771c30 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift @@ -501,40 +501,55 @@ extension JNISwift2JavaGenerator { originalFunctionSignature: SwiftFunctionSignature, mode: JExtractAsyncFuncMode ) { + // Update translated function + let nativeFutureType: JavaType + let translatedFutureType: JavaType + let completeMethodID: String + let completeExceptionallyMethodID: String + switch mode { case .completableFuture: - // Update translated function - - let nativeFutureType = JavaType.completableFuture(nativeFunctionSignature.result.javaType) + nativeFutureType = .completableFuture(nativeFunctionSignature.result.javaType) + translatedFutureType = .completableFuture(translatedFunctionSignature.resultType.javaType) + completeMethodID = "_JNIMethodIDCache.CompletableFuture.complete" + completeExceptionallyMethodID = "_JNIMethodIDCache.CompletableFuture.completeExceptionally" + + case .legacyFuture: + nativeFutureType = .simpleCompletableFuture(nativeFunctionSignature.result.javaType) + translatedFutureType = .future(translatedFunctionSignature.resultType.javaType) + completeMethodID = "_JNIMethodIDCache.SimpleCompletableFuture.complete" + completeExceptionallyMethodID = "_JNIMethodIDCache.SimpleCompletableFuture.completeExceptionally" + } - let futureOutParameter = OutParameter( - name: "$future", - type: nativeFutureType, - allocation: .new - ) + let futureOutParameter = OutParameter( + name: "future$", + type: nativeFutureType, + allocation: .new + ) - let result = translatedFunctionSignature.resultType - translatedFunctionSignature.resultType = TranslatedResult( - javaType: .completableFuture(translatedFunctionSignature.resultType.javaType), - annotations: result.annotations, - outParameters: result.outParameters + [futureOutParameter], - conversion: .aggregate(variable: nil, [ - .print(.placeholder), // Make the downcall - .method(.constant("$future"), function: "thenApply", arguments: [ - .lambda(args: ["futureResult$"], body: .replacingPlaceholder(result.conversion, placeholder: "futureResult$")) - ]) + let result = translatedFunctionSignature.resultType + translatedFunctionSignature.resultType = TranslatedResult( + javaType: translatedFutureType, + annotations: result.annotations, + outParameters: result.outParameters + [futureOutParameter], + conversion: .aggregate(variable: nil, [ + .print(.placeholder), // Make the downcall + .method(.constant("future$"), function: "thenApply", arguments: [ + .lambda(args: ["futureResult$"], body: .replacingPlaceholder(result.conversion, placeholder: "futureResult$")) ]) - ) + ]) + ) - // Update native function - nativeFunctionSignature.result.conversion = .asyncCompleteFuture( - swiftFunctionResultType: originalFunctionSignature.result.type, - nativeFunctionSignature: nativeFunctionSignature, - isThrowing: originalFunctionSignature.isThrowing - ) - nativeFunctionSignature.result.javaType = .void - nativeFunctionSignature.result.outParameters.append(.init(name: "result_future", type: nativeFutureType)) - } + // Update native function + nativeFunctionSignature.result.conversion = .asyncCompleteFuture( + swiftFunctionResultType: originalFunctionSignature.result.type, + nativeFunctionSignature: nativeFunctionSignature, + isThrowing: originalFunctionSignature.isThrowing, + completeMethodID: completeMethodID, + completeExceptionallyMethodID: completeExceptionallyMethodID + ) + nativeFunctionSignature.result.javaType = .void + nativeFunctionSignature.result.outParameters.append(.init(name: "result_future", type: nativeFutureType)) } func translateProtocolParameter( diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift index 1994dce04..24e469067 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift @@ -689,7 +689,9 @@ extension JNISwift2JavaGenerator { indirect case asyncCompleteFuture( swiftFunctionResultType: SwiftType, nativeFunctionSignature: NativeFunctionSignature, - isThrowing: Bool + isThrowing: Bool, + completeMethodID: String, + completeExceptionallyMethodID: String ) /// `{ (args) -> return body }` @@ -927,7 +929,9 @@ extension JNISwift2JavaGenerator { case .asyncCompleteFuture( let swiftFunctionResultType, let nativeFunctionSignature, - let isThrowing + let isThrowing, + let completeMethodID, + let completeExceptionallyMethodID ): var globalRefs: [String] = ["globalFuture"] @@ -954,7 +958,7 @@ extension JNISwift2JavaGenerator { printer.print("environment = try! JavaVirtualMachine.shared().environment()") let inner = nativeFunctionSignature.result.conversion.render(&printer, "swiftResult$") if swiftFunctionResultType.isVoid { - printer.print("environment.interface.CallBooleanMethodA(environment, globalFuture, _JNIMethodIDCache.CompletableFuture.complete, [jvalue(l: nil)])") + printer.print("environment.interface.CallBooleanMethodA(environment, globalFuture, \(completeMethodID), [jvalue(l: nil)])") } else { let result: String if nativeFunctionSignature.result.javaType.requiresBoxing { @@ -964,7 +968,7 @@ extension JNISwift2JavaGenerator { result = inner } - printer.print("environment.interface.CallBooleanMethodA(environment, globalFuture, _JNIMethodIDCache.CompletableFuture.complete, [jvalue(l: \(result))])") + printer.print("environment.interface.CallBooleanMethodA(environment, globalFuture, \(completeMethodID), [jvalue(l: \(result))])") } } @@ -986,7 +990,7 @@ extension JNISwift2JavaGenerator { """ let catchEnvironment = try! JavaVirtualMachine.shared().environment() let exception = catchEnvironment.interface.NewObjectA(catchEnvironment, _JNIMethodIDCache.Exception.class, _JNIMethodIDCache.Exception.constructWithMessage, [String(describing: error).getJValue(in: catchEnvironment)]) - catchEnvironment.interface.CallBooleanMethodA(catchEnvironment, globalFuture, _JNIMethodIDCache.CompletableFuture.completeExceptionally, [jvalue(l: exception)]) + catchEnvironment.interface.CallBooleanMethodA(catchEnvironment, globalFuture, \(completeExceptionallyMethodID), [jvalue(l: exception)]) """ ) } diff --git a/Sources/JExtractSwiftLib/JavaTypes/JavaType+JDK.swift b/Sources/JExtractSwiftLib/JavaTypes/JavaType+JDK.swift index 5e5a72688..511bf8de6 100644 --- a/Sources/JExtractSwiftLib/JavaTypes/JavaType+JDK.swift +++ b/Sources/JExtractSwiftLib/JavaTypes/JavaType+JDK.swift @@ -44,4 +44,9 @@ extension JavaType { static func completableFuture(_ T: JavaType) -> JavaType { .class(package: "java.util.concurrent", name: "CompletableFuture", typeParameters: [T.boxedType]) } + + /// The description of the type java.util.concurrent.Future + static func future(_ T: JavaType) -> JavaType { + .class(package: "java.util.concurrent", name: "Future", typeParameters: [T.boxedType]) + } } diff --git a/Sources/JExtractSwiftLib/JavaTypes/JavaType+SwiftKit.swift b/Sources/JExtractSwiftLib/JavaTypes/JavaType+SwiftKit.swift index 2ab9c0a22..7319b2942 100644 --- a/Sources/JExtractSwiftLib/JavaTypes/JavaType+SwiftKit.swift +++ b/Sources/JExtractSwiftLib/JavaTypes/JavaType+SwiftKit.swift @@ -85,4 +85,9 @@ extension JavaType { } } + /// The description of the type org.swift.swiftkit.core.SimpleCompletableFuture + static func simpleCompletableFuture(_ T: JavaType) -> JavaType { + .class(package: "org.swift.swiftkit.core", name: "SimpleCompletableFuture", typeParameters: [T.boxedType]) + } + } diff --git a/Sources/SwiftJavaConfigurationShared/JExtract/JExtractAsyncFuncMode.swift b/Sources/SwiftJavaConfigurationShared/JExtract/JExtractAsyncFuncMode.swift index 221649c52..d7fd84623 100644 --- a/Sources/SwiftJavaConfigurationShared/JExtract/JExtractAsyncFuncMode.swift +++ b/Sources/SwiftJavaConfigurationShared/JExtract/JExtractAsyncFuncMode.swift @@ -23,7 +23,7 @@ public enum JExtractAsyncFuncMode: String, Codable { /// Android 23 and below. /// /// - Note: Prefer using the `completableFuture` mode instead, if possible. -// case future + case legacyFuture } extension JExtractAsyncFuncMode { diff --git a/Sources/SwiftJavaRuntimeSupport/DefaultCaches.swift b/Sources/SwiftJavaRuntimeSupport/JNIMethodIDCaches.swift similarity index 70% rename from Sources/SwiftJavaRuntimeSupport/DefaultCaches.swift rename to Sources/SwiftJavaRuntimeSupport/JNIMethodIDCaches.swift index 1c3079bc3..56fe0351a 100644 --- a/Sources/SwiftJavaRuntimeSupport/DefaultCaches.swift +++ b/Sources/SwiftJavaRuntimeSupport/JNIMethodIDCaches.swift @@ -47,6 +47,36 @@ extension _JNIMethodIDCache { } } + public enum SimpleCompletableFuture { + private static let completeMethod = Method( + name: "complete", + signature: "(Ljava/lang/Object;)Z" + ) + + private static let completeExceptionallyMethod = Method( + name: "completeExceptionally", + signature: "(Ljava/lang/Throwable;)Z" + ) + + private static let cache = _JNIMethodIDCache( + environment: try! JavaVirtualMachine.shared().environment(), + className: "org/swift/swiftkit/core/SimpleCompletableFuture", + methods: [completeMethod, completeExceptionallyMethod] + ) + + public static var `class`: jclass { + cache.javaClass + } + + public static var complete: jmethodID { + cache.methods[completeMethod]! + } + + public static var completeExceptionally: jmethodID { + cache.methods[completeExceptionallyMethod]! + } + } + public enum Exception { private static let messageConstructor = Method(name: "", signature: "(Ljava/lang/String;)V") diff --git a/SwiftKitCore/src/main/java/org/swift/swiftkit/core/SimpleCompletableFuture.java b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/SimpleCompletableFuture.java new file mode 100644 index 000000000..b92527236 --- /dev/null +++ b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/SimpleCompletableFuture.java @@ -0,0 +1,223 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +package org.swift.swiftkit.core; + +import java.util.Deque; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Function; + +/** + * A simple completable {@link Future} for platforms that do not support {@link java.util.concurrent.CompletableFuture}, + * e.g. before Java 8, and/or before Android 23. + *

+ * Prefer using the {@link CompletableFuture} for bridging Swift asynchronous functions, i.e. use the {@code completableFuture} + * mode in {@code swift-java jextract}. + * + * @param The result type + */ +public final class SimpleCompletableFuture implements Future { + // Marker object used to indicate the Future has not yet been completed. + private static final Object PENDING = new Object(); + private static final Object NULL = new Object(); + private final AtomicReference result = new AtomicReference<>(PENDING); + + private final Deque callbacks = new ConcurrentLinkedDeque<>(); + + /** + * Wrapper type we use to indicate that a recorded result was a failure (recorded using {@link SimpleCompletableFuture#completeExceptionally(Throwable)}. + * Since no-one else can instantiate this type, we know for sure that a recorded CompletedExceptionally indicates a failure. + */ + static final class CompletedExceptionally { + private final Throwable exception; + + private CompletedExceptionally(Throwable exception) { + this.exception = exception; + } + } + + /** + * Returns a new future that, when this stage completes + * normally, is executed with this stage's result as the argument + * to the supplied function. + * + *

This method is analogous to + * {@link java.util.Optional#map Optional.map} and + * {@link java.util.stream.Stream#map Stream.map}. + * + * @return the new Future + */ + public Future thenApply(Function fn) { + SimpleCompletableFuture newFuture = new SimpleCompletableFuture<>(); + addCallback(() -> { + Object observed = this.result.get(); + if (observed instanceof CompletedExceptionally) { + newFuture.completeExceptionally(((CompletedExceptionally) observed).exception); + } else { + try { + // We're guaranteed that an observed result is of type T. + // noinspection unchecked + U newResult = fn.apply(observed == NULL ? null : (T) observed); + newFuture.complete(newResult); + } catch (Throwable t) { + newFuture.completeExceptionally(t); + } + } + }); + return newFuture; + } + + /** + * If not already completed, sets the value returned by {@link #get()} and + * related methods to the given value. + * + * @param value the result value + * @return {@code true} if this invocation caused this CompletableFuture + * to transition to a completed state, else {@code false} + */ + public boolean complete(T value) { + if (result.compareAndSet(PENDING, value == null ? NULL : value)) { + synchronized (result) { + result.notifyAll(); + } + runCallbacks(); + return true; + } + + return false; + } + + /** + * If not already completed, causes invocations of {@link #get()} + * and related methods to throw the given exception. + * + * @param ex the exception + * @return {@code true} if this invocation caused this CompletableFuture + * to transition to a completed state, else {@code false} + */ + public boolean completeExceptionally(Throwable ex) { + if (result.compareAndSet(PENDING, new CompletedExceptionally(ex))) { + synchronized (result) { + result.notifyAll(); + } + runCallbacks(); + return true; + } + + return false; + } + + private void runCallbacks() { + // This is a pretty naive implementation; even if we enter this by racing a thenApply, + // with a completion; we're using a concurrent deque so we won't happen to trigger a callback twice. + Runnable callback; + while ((callback = callbacks.pollFirst()) != null) { + callback.run(); + } + } + + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + // TODO: If we're representing a Swift Task computation with this future, + // we could trigger a Task.cancel() from here + return false; + } + + @Override + public boolean isCancelled() { + return false; + } + + @Override + public boolean isDone() { + return this.result.get() != PENDING; + } + + @Override + public T get() throws InterruptedException, ExecutionException { + Object observed; + // If PENDING check fails immediately, we have no need to take the result lock at all. + while ((observed = result.get()) == PENDING) { + synchronized (result) { + if (result.get() == PENDING) { + result.wait(); + } + } + } + + return getReturn(observed); + } + + @Override + public T get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { + Object observed; + + // Fast path: are we already completed and don't need to do any waiting? + if ((observed = result.get()) != PENDING) { + return get(); + } + + long nanos = unit.toNanos(timeout); + synchronized (result) { + if (!isDone()) { + if (nanos <= 0) { + throw new TimeoutException(); + } + long deadline = System.nanoTime() + nanos; + while (!isDone()) { + nanos = deadline - System.nanoTime(); + if (nanos <= 0L) { + throw new TimeoutException(); + } + result.wait(nanos / 1000000, (int) (nanos % 1000000)); + } + } + } + + // Seems we broke out of the wait loop, let's trigger the 'get()' implementation + observed = result.get(); + if (observed == PENDING) { + throw new ExecutionException("Unexpectedly finished wait-loop while future was not completed, this is a bug.", null); + } + return getReturn(observed); + } + + private T getReturn(Object observed) throws ExecutionException { + if (observed instanceof CompletedExceptionally) { + // We observed a failure, unwrap and throw it + Throwable exception = ((CompletedExceptionally) observed).exception; + if (exception instanceof CancellationException) { + throw (CancellationException) exception; + } + throw new ExecutionException(exception); + } else if (observed == NULL) { + return null; + } else { + // We're guaranteed that we only allowed registering completions of type `T` + // noinspection unchecked + return (T) observed; + } + } + + private void addCallback(Runnable action) { + callbacks.add(action); + if (isDone()) { + // This may race, but we don't care since triggering the callbacks is going to be at-most-once + // by means of using the concurrent deque as our list of callbacks. + runCallbacks(); + } + } + +} \ No newline at end of file diff --git a/SwiftKitCore/src/test/java/org/swift/swiftkit/core/SimpleCompletableFutureTest.java b/SwiftKitCore/src/test/java/org/swift/swiftkit/core/SimpleCompletableFutureTest.java new file mode 100644 index 000000000..b4bb98b3a --- /dev/null +++ b/SwiftKitCore/src/test/java/org/swift/swiftkit/core/SimpleCompletableFutureTest.java @@ -0,0 +1,185 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +package org.swift.swiftkit.core; + +import org.junit.jupiter.api.Test; + +import java.util.Objects; +import java.util.concurrent.*; +import static org.junit.jupiter.api.Assertions.*; + +public class SimpleCompletableFutureTest { + + @Test + void testCompleteAndGet() throws ExecutionException, InterruptedException { + SimpleCompletableFuture future = new SimpleCompletableFuture<>(); + assertFalse(future.isDone()); + assertTrue(future.complete("test")); + assertTrue(future.isDone()); + assertEquals("test", future.get()); + } + + @Test + void testCompleteWithNullAndGet() throws ExecutionException, InterruptedException { + SimpleCompletableFuture future = new SimpleCompletableFuture<>(); + assertFalse(future.isDone()); + assertTrue(future.complete(null)); + assertTrue(future.isDone()); + assertNull(future.get()); + } + + @Test + void testCompleteExceptionallyAndGet() { + SimpleCompletableFuture future = new SimpleCompletableFuture<>(); + RuntimeException ex = new RuntimeException("Test Exception"); + assertTrue(future.completeExceptionally(ex)); + assertTrue(future.isDone()); + + ExecutionException thrown = assertThrows(ExecutionException.class, future::get); + assertEquals(ex, thrown.getCause()); + } + + @Test + void testGetWithTimeout_timesOut() { + SimpleCompletableFuture future = new SimpleCompletableFuture<>(); + assertThrows(TimeoutException.class, () -> future.get(10, TimeUnit.MILLISECONDS)); + } + + @Test + void testGetWithTimeout_completesInTime() throws ExecutionException, InterruptedException, TimeoutException { + SimpleCompletableFuture future = new SimpleCompletableFuture<>(); + future.complete("fast"); + assertEquals("fast", future.get(10, TimeUnit.MILLISECONDS)); + } + + @Test + void testGetWithTimeout_completesInTimeAfterWait() throws Exception { + SimpleCompletableFuture future = new SimpleCompletableFuture<>(); + Thread t = new Thread(() -> { + try { + Thread.sleep(50); + } catch (InterruptedException e) { + // ignore + } + future.complete("late"); + }); + t.start(); + assertEquals("late", future.get(200, TimeUnit.MILLISECONDS)); + } + + @Test + void testThenApply() throws ExecutionException, InterruptedException { + SimpleCompletableFuture future = new SimpleCompletableFuture<>(); + Future mapped = future.thenApply(String::length); + + future.complete("hello"); + + assertEquals(5, mapped.get()); + } + + @Test + void testThenApplyOnCompletedFuture() throws ExecutionException, InterruptedException { + SimpleCompletableFuture future = new SimpleCompletableFuture<>(); + future.complete("done"); + + Future mapped = future.thenApply(String::length); + + assertEquals(4, mapped.get()); + } + + @Test + void testThenApplyWithNull() throws ExecutionException, InterruptedException { + SimpleCompletableFuture future = new SimpleCompletableFuture<>(); + Future mapped = future.thenApply(Objects::isNull); + + future.complete(null); + + assertTrue(mapped.get()); + } + + @Test + void testThenApplyExceptionally() { + SimpleCompletableFuture future = new SimpleCompletableFuture<>(); + RuntimeException ex = new RuntimeException("Initial Exception"); + Future mapped = future.thenApply(String::length); + + future.completeExceptionally(ex); + + ExecutionException thrown = assertThrows(ExecutionException.class, mapped::get); + assertEquals(ex, thrown.getCause()); + } + + @Test + void testThenApplyTransformationThrows() { + SimpleCompletableFuture future = new SimpleCompletableFuture<>(); + RuntimeException ex = new RuntimeException("Transformation Exception"); + Future mapped = future.thenApply(s -> { + throw ex; + }); + + future.complete("hello"); + + ExecutionException thrown = assertThrows(ExecutionException.class, mapped::get); + assertEquals(ex, thrown.getCause()); + } + + @Test + void testCompleteTwice() throws ExecutionException, InterruptedException { + SimpleCompletableFuture future = new SimpleCompletableFuture<>(); + + assertTrue(future.complete("first")); + assertFalse(future.complete("second")); + + assertEquals("first", future.get()); + } + + @Test + void testCompleteThenCompleteExceptionally() throws ExecutionException, InterruptedException { + SimpleCompletableFuture future = new SimpleCompletableFuture<>(); + + assertTrue(future.complete("first")); + assertFalse(future.completeExceptionally(new RuntimeException("second"))); + + assertEquals("first", future.get()); + } + + @Test + void testCompleteExceptionallyThenComplete() { + SimpleCompletableFuture future = new SimpleCompletableFuture<>(); + RuntimeException ex = new RuntimeException("first"); + + assertTrue(future.completeExceptionally(ex)); + assertFalse(future.complete("second")); + + ExecutionException thrown = assertThrows(ExecutionException.class, future::get); + assertEquals(ex, thrown.getCause()); + } + + @Test + void testIsDone() { + SimpleCompletableFuture future = new SimpleCompletableFuture<>(); + assertFalse(future.isDone()); + future.complete("done"); + assertTrue(future.isDone()); + } + + @Test + void testIsDoneExceptionally() { + SimpleCompletableFuture future = new SimpleCompletableFuture<>(); + assertFalse(future.isDone()); + future.completeExceptionally(new RuntimeException()); + assertTrue(future.isDone()); + } +} diff --git a/Tests/JExtractSwiftTests/JNI/JNIAsyncTests.swift b/Tests/JExtractSwiftTests/JNI/JNIAsyncTests.swift index 1930e601a..f246bd0f7 100644 --- a/Tests/JExtractSwiftTests/JNI/JNIAsyncTests.swift +++ b/Tests/JExtractSwiftTests/JNI/JNIAsyncTests.swift @@ -14,6 +14,7 @@ import JExtractSwiftLib import Testing +import SwiftJavaConfigurationShared @Suite struct JNIAsyncTests { @@ -33,9 +34,9 @@ struct JNIAsyncTests { * } */ public static java.util.concurrent.CompletableFuture asyncVoid() { - java.util.concurrent.CompletableFuture $future = new java.util.concurrent.CompletableFuture(); - SwiftModule.$asyncVoid($future); - return $future.thenApply((futureResult$) -> { + java.util.concurrent.CompletableFuture future$ = new java.util.concurrent.CompletableFuture(); + SwiftModule.$asyncVoid(future$); + return future$.thenApply((futureResult$) -> { return futureResult$; } ); @@ -107,9 +108,9 @@ struct JNIAsyncTests { * } */ public static java.util.concurrent.CompletableFuture async() { - java.util.concurrent.CompletableFuture $future = new java.util.concurrent.CompletableFuture(); - SwiftModule.$async($future); - return $future.thenApply((futureResult$) -> { + java.util.concurrent.CompletableFuture future$ = new java.util.concurrent.CompletableFuture(); + SwiftModule.$async(future$); + return future$.thenApply((futureResult$) -> { return futureResult$; } ); @@ -195,9 +196,9 @@ struct JNIAsyncTests { * } */ public static java.util.concurrent.CompletableFuture async(long i) { - java.util.concurrent.CompletableFuture $future = new java.util.concurrent.CompletableFuture(); - SwiftModule.$async(i, $future); - return $future.thenApply((futureResult$) -> { + java.util.concurrent.CompletableFuture future$ = new java.util.concurrent.CompletableFuture(); + SwiftModule.$async(i, future$); + return future$.thenApply((futureResult$) -> { return futureResult$; } ); @@ -276,9 +277,9 @@ struct JNIAsyncTests { * } */ public static java.util.concurrent.CompletableFuture async(MyClass c, SwiftArena swiftArena$) { - java.util.concurrent.CompletableFuture $future = new java.util.concurrent.CompletableFuture(); - SwiftModule.$async(c.$memoryAddress(), $future); - return $future.thenApply((futureResult$) -> { + java.util.concurrent.CompletableFuture future$ = new java.util.concurrent.CompletableFuture(); + SwiftModule.$async(c.$memoryAddress(), future$); + return future$.thenApply((futureResult$) -> { return MyClass.wrapMemoryAddressUnsafe(futureResult$, swiftArena$); } ); @@ -365,9 +366,9 @@ struct JNIAsyncTests { expectedChunks: [ """ public static java.util.concurrent.CompletableFuture async(java.lang.String s) { - java.util.concurrent.CompletableFuture $future = new java.util.concurrent.CompletableFuture(); - SwiftModule.$async(s, $future); - return $future.thenApply((futureResult$) -> { + java.util.concurrent.CompletableFuture future$ = new java.util.concurrent.CompletableFuture(); + SwiftModule.$async(s, future$); + return future$.thenApply((futureResult$) -> { return futureResult$; } ); @@ -408,4 +409,65 @@ struct JNIAsyncTests { ] ) } + + @Test("Import: (MyClass) async -> MyClass (Java, LegacyFuture)") + func legacyFuture_asyncMyClassToMyClass_java() throws { + var config = Configuration() + config.asyncFuncMode = .legacyFuture + + try assertOutput( + input: """ + class MyClass { } + + public func async(c: MyClass) async -> MyClass + """, + config: config, + .jni, .java, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + public static java.util.concurrent.Future async(MyClass c, SwiftArena swiftArena$) { + org.swift.swiftkit.core.SimpleCompletableFuture future$ = new org.swift.swiftkit.core.SimpleCompletableFuture(); + SwiftModule.$async(c.$memoryAddress(), future$); + return future$.thenApply((futureResult$) -> { + return MyClass.wrapMemoryAddressUnsafe(futureResult$, swiftArena$); + } + ); + } + """, + """ + private static native void $async(long c, org.swift.swiftkit.core.SimpleCompletableFuture result_future); + """, + ] + ) + } + + @Test("Import: (MyClass) async -> MyClass (Swift, LegacyFuture)") + func legacyFuture_asyncMyClassToMyClass_swift() throws { + var config = Configuration() + config.asyncFuncMode = .legacyFuture + + try assertOutput( + input: """ + class MyClass { } + + public func async(c: MyClass) async -> MyClass + """, + config: config, + .jni, .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + @_cdecl("Java_com_example_swift_SwiftModule__00024async__JLorg_swift_swiftkit_core_SimpleCompletableFuture_2") + func Java_com_example_swift_SwiftModule__00024async__JLorg_swift_swiftkit_core_SimpleCompletableFuture_2(environment: UnsafeMutablePointer!, thisClass: jclass, c: jlong, result_future: jobject?) { + ... + var task: Task? = nil + ... + environment.interface.CallBooleanMethodA(environment, globalFuture, _JNIMethodIDCache.SimpleCompletableFuture.complete, [jvalue(l: boxedResult$)]) + ... + } + """ + ] + ) + } } From eea0638c5573c89bf67698a7e131be04970042c8 Mon Sep 17 00:00:00 2001 From: Mads Odgaard Date: Mon, 1 Dec 2025 13:22:18 +0100 Subject: [PATCH 06/14] link log on android (#467) --- Package.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Package.swift b/Package.swift index b4c202082..d877ae696 100644 --- a/Package.swift +++ b/Package.swift @@ -386,6 +386,9 @@ let package = Package( swiftSettings: [ .swiftLanguageMode(.v5), .unsafeFlags(["-I\(javaIncludePath)", "-I\(javaPlatformIncludePath)"], .when(platforms: [.macOS, .linux, .windows])) + ], + linkerSettings: [ + .linkedLibrary("log", .when(platforms: [.android])) ] ), From 67c20f76d083e18a95abfd7c9a2c830ba350cb7d Mon Sep 17 00:00:00 2001 From: Mads Odgaard Date: Mon, 1 Dec 2025 15:21:23 +0100 Subject: [PATCH 07/14] Fix JNI caching for native threads (#464) --- ...ift2JavaGenerator+SwiftThunkPrinting.swift | 16 +- Sources/SwiftJava/generated/JavaThread.swift | 229 ++++++++++++++++++ Sources/SwiftJava/swift-java.config | 1 + Sources/SwiftJavaRuntimeSupport/JNI.swift | 31 +++ .../JNIMethodIDCaches.swift | 3 - .../_JNIBoxedConversions.swift | 8 - .../_JNIMethodIDCache.swift | 31 ++- 7 files changed, 302 insertions(+), 17 deletions(-) create mode 100644 Sources/SwiftJava/generated/JavaThread.swift create mode 100644 Sources/SwiftJavaRuntimeSupport/JNI.swift diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift index d3b477326..6628be333 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift @@ -119,6 +119,8 @@ extension JNISwift2JavaGenerator { private func printGlobalSwiftThunkSources(_ printer: inout CodePrinter) throws { printHeader(&printer) + printJNIOnLoad(&printer) + for decl in analysis.importedGlobalFuncs { printSwiftFunctionThunk(&printer, decl) printer.println() @@ -130,6 +132,18 @@ extension JNISwift2JavaGenerator { } } + private func printJNIOnLoad(_ printer: inout CodePrinter) { + printer.print( + """ + @_cdecl("JNI_OnLoad") + func JNI_OnLoad(javaVM: JavaVMPointer, reserved: UnsafeMutableRawPointer) -> jint { + SwiftJavaRuntimeSupport._JNI_OnLoad(javaVM, reserved) + return JNI_VERSION_1_6 + } + """ + ) + } + private func printNominalTypeThunks(_ printer: inout CodePrinter, _ type: ImportedNominalType) throws { printHeader(&printer) @@ -222,7 +236,7 @@ extension JNISwift2JavaGenerator { let methodSignature = MethodSignature(resultType: .void, parameterTypes: enumCase.parameterConversions.map(\.native.javaType)) let methods = #"[.init(name: "", signature: "\#(methodSignature.mangledName)")]"# - return #"_JNIMethodIDCache(environment: try! JavaVirtualMachine.shared().environment(), className: "\#(nativeParametersClassName)", methods: \#(methods))"# + return #"_JNIMethodIDCache(className: "\#(nativeParametersClassName)", methods: \#(methods))"# } private func printEnumGetAsCaseThunk( diff --git a/Sources/SwiftJava/generated/JavaThread.swift b/Sources/SwiftJava/generated/JavaThread.swift new file mode 100644 index 000000000..c71e933b4 --- /dev/null +++ b/Sources/SwiftJava/generated/JavaThread.swift @@ -0,0 +1,229 @@ +// Auto-generated by Java-to-Swift wrapper generator. +import CSwiftJavaJNI + +@JavaClass("java.lang.Thread") +open class JavaThread: JavaObject { + @JavaMethod + @_nonoverride public convenience init(_ arg0: String, environment: JNIEnvironment? = nil) + + @JavaMethod + @_nonoverride public convenience init(environment: JNIEnvironment? = nil) + + @JavaMethod + open func getName() -> String + + @JavaMethod + open func run() + + @JavaMethod + open func interrupt() + + @JavaMethod + open override func toString() -> String + + @JavaMethod + open override func clone() throws -> JavaObject! + + @JavaMethod + open func join(_ arg0: Int64, _ arg1: Int32) throws + + @JavaMethod + open func join() throws + + @JavaMethod + open func join(_ arg0: Int64) throws + + @JavaMethod + open func setContextClassLoader(_ arg0: JavaClassLoader?) + + @JavaMethod + open func setPriority(_ arg0: Int32) + + @JavaMethod + open func setDaemon(_ arg0: Bool) + + @JavaMethod + open func start() + + @JavaMethod + open func getPriority() -> Int32 + + @JavaMethod + open func isDaemon() -> Bool + + @JavaMethod + open func getContextClassLoader() -> JavaClassLoader! + + @JavaMethod + open func isVirtual() -> Bool + + @JavaMethod + open func isAlive() -> Bool + + @JavaMethod + open func threadId() -> Int64 + + @JavaMethod + open func getUncaughtExceptionHandler() -> JavaThread.UncaughtExceptionHandler! + + @JavaMethod + open func stop() + + @JavaMethod + open func isInterrupted() -> Bool + + @JavaMethod + open func setName(_ arg0: String) + + @JavaMethod + open func checkAccess() + + @JavaMethod + open func getId() -> Int64 + + @JavaMethod + open func setUncaughtExceptionHandler(_ arg0: JavaThread.UncaughtExceptionHandler?) +} +extension JavaThread { + @JavaInterface("java.lang.Thread$Builder") + public struct Builder { + @JavaMethod + public func name(_ arg0: String) -> JavaThread.Builder! + + @JavaMethod + public func name(_ arg0: String, _ arg1: Int64) -> JavaThread.Builder! + + @JavaMethod + public func uncaughtExceptionHandler(_ arg0: JavaThread.UncaughtExceptionHandler?) -> JavaThread.Builder! + + @JavaMethod + public func inheritInheritableThreadLocals(_ arg0: Bool) -> JavaThread.Builder! + } +} +extension JavaThread.Builder { + @JavaInterface("java.lang.Thread$Builder$OfPlatform", extends: JavaThread.Builder.self) + public struct OfPlatform { + @JavaMethod + public func name(_ arg0: String, _ arg1: Int64) -> JavaThread.Builder! + + @JavaMethod + public func name(_ arg0: String, _ arg1: Int64) -> JavaThread.Builder.OfPlatform! + + @JavaMethod + public func name(_ arg0: String) -> JavaThread.Builder! + + @JavaMethod + public func name(_ arg0: String) -> JavaThread.Builder.OfPlatform! + + @JavaMethod + public func priority(_ arg0: Int32) -> JavaThread.Builder.OfPlatform! + + @JavaMethod + public func daemon() -> JavaThread.Builder.OfPlatform! + + @JavaMethod + public func daemon(_ arg0: Bool) -> JavaThread.Builder.OfPlatform! + + @JavaMethod + public func uncaughtExceptionHandler(_ arg0: JavaThread.UncaughtExceptionHandler?) -> JavaThread.Builder.OfPlatform! + + @JavaMethod + public func uncaughtExceptionHandler(_ arg0: JavaThread.UncaughtExceptionHandler?) -> JavaThread.Builder! + + @JavaMethod + public func stackSize(_ arg0: Int64) -> JavaThread.Builder.OfPlatform! + + @JavaMethod + public func inheritInheritableThreadLocals(_ arg0: Bool) -> JavaThread.Builder.OfPlatform! + + @JavaMethod + public func inheritInheritableThreadLocals(_ arg0: Bool) -> JavaThread.Builder! + } +} +extension JavaThread.Builder { + @JavaInterface("java.lang.Thread$Builder$OfVirtual", extends: JavaThread.Builder.self) + public struct OfVirtual { + @JavaMethod + public func name(_ arg0: String, _ arg1: Int64) -> JavaThread.Builder! + + @JavaMethod + public func name(_ arg0: String, _ arg1: Int64) -> JavaThread.Builder.OfVirtual! + + @JavaMethod + public func name(_ arg0: String) -> JavaThread.Builder! + + @JavaMethod + public func name(_ arg0: String) -> JavaThread.Builder.OfVirtual! + + @JavaMethod + public func uncaughtExceptionHandler(_ arg0: JavaThread.UncaughtExceptionHandler?) -> JavaThread.Builder! + + @JavaMethod + public func uncaughtExceptionHandler(_ arg0: JavaThread.UncaughtExceptionHandler?) -> JavaThread.Builder.OfVirtual! + + @JavaMethod + public func inheritInheritableThreadLocals(_ arg0: Bool) -> JavaThread.Builder.OfVirtual! + + @JavaMethod + public func inheritInheritableThreadLocals(_ arg0: Bool) -> JavaThread.Builder! + } +} +extension JavaThread { + @JavaInterface("java.lang.Thread$UncaughtExceptionHandler") + public struct UncaughtExceptionHandler { + @JavaMethod + public func uncaughtException(_ arg0: JavaThread?, _ arg1: Throwable?) + } +} +extension JavaClass { + @JavaStaticField(isFinal: true) + public var MIN_PRIORITY: Int32 + + @JavaStaticField(isFinal: true) + public var NORM_PRIORITY: Int32 + + @JavaStaticField(isFinal: true) + public var MAX_PRIORITY: Int32 + + @JavaStaticMethod + public func currentThread() -> JavaThread! + + @JavaStaticMethod + public func onSpinWait() + + @JavaStaticMethod + public func holdsLock(_ arg0: JavaObject?) -> Bool + + @JavaStaticMethod + public func interrupted() -> Bool + + @JavaStaticMethod + public func activeCount() -> Int32 + + @JavaStaticMethod + public func enumerate(_ arg0: [JavaThread?]) -> Int32 + + @JavaStaticMethod + public func yield() + + @JavaStaticMethod + public func sleep(_ arg0: Int64) throws + + @JavaStaticMethod + public func sleep(_ arg0: Int64, _ arg1: Int32) throws + + @JavaStaticMethod + public func ofPlatform() -> JavaThread.Builder.OfPlatform! + + @JavaStaticMethod + public func ofVirtual() -> JavaThread.Builder.OfVirtual! + + @JavaStaticMethod + public func dumpStack() + + @JavaStaticMethod + public func setDefaultUncaughtExceptionHandler(_ arg0: JavaThread.UncaughtExceptionHandler?) + + @JavaStaticMethod + public func getDefaultUncaughtExceptionHandler() -> JavaThread.UncaughtExceptionHandler! +} diff --git a/Sources/SwiftJava/swift-java.config b/Sources/SwiftJava/swift-java.config index d07ff1620..d43096a73 100644 --- a/Sources/SwiftJava/swift-java.config +++ b/Sources/SwiftJava/swift-java.config @@ -23,6 +23,7 @@ "java.lang.Void" : "JavaVoid", "java.lang.CharSequence": "CharSequence", "java.lang.Appendable": "Appendable", + "java.lang.Thread": "JavaThread", "java.util.Optional": "JavaOptional", "java.util.OptionalDouble": "JavaOptionalDouble", "java.util.OptionalInt": "JavaOptionalInt", diff --git a/Sources/SwiftJavaRuntimeSupport/JNI.swift b/Sources/SwiftJavaRuntimeSupport/JNI.swift new file mode 100644 index 000000000..b6f0117df --- /dev/null +++ b/Sources/SwiftJavaRuntimeSupport/JNI.swift @@ -0,0 +1,31 @@ +//===----------------------------------------------------------------------===// +// +// 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 SwiftJava +import CSwiftJavaJNI + +final class JNI { + static var shared: JNI! + + let applicationClassLoader: JavaClassLoader + + init(fromVM javaVM: JavaVirtualMachine) { + self.applicationClassLoader = try! JavaClass(environment: javaVM.environment()).currentThread().getContextClassLoader() + } +} + +// Called by generated code, and not automatically by Java. +public func _JNI_OnLoad(_ javaVM: JavaVMPointer, _ reserved: UnsafeMutableRawPointer) { + JNI.shared = JNI(fromVM: JavaVirtualMachine(adoptingJVM: javaVM)) +} diff --git a/Sources/SwiftJavaRuntimeSupport/JNIMethodIDCaches.swift b/Sources/SwiftJavaRuntimeSupport/JNIMethodIDCaches.swift index 56fe0351a..16a9c899e 100644 --- a/Sources/SwiftJavaRuntimeSupport/JNIMethodIDCaches.swift +++ b/Sources/SwiftJavaRuntimeSupport/JNIMethodIDCaches.swift @@ -27,7 +27,6 @@ extension _JNIMethodIDCache { ) private static let cache = _JNIMethodIDCache( - environment: try! JavaVirtualMachine.shared().environment(), className: "java/util/concurrent/CompletableFuture", methods: [completeMethod, completeExceptionallyMethod] ) @@ -59,7 +58,6 @@ extension _JNIMethodIDCache { ) private static let cache = _JNIMethodIDCache( - environment: try! JavaVirtualMachine.shared().environment(), className: "org/swift/swiftkit/core/SimpleCompletableFuture", methods: [completeMethod, completeExceptionallyMethod] ) @@ -81,7 +79,6 @@ extension _JNIMethodIDCache { private static let messageConstructor = Method(name: "", signature: "(Ljava/lang/String;)V") private static let cache = _JNIMethodIDCache( - environment: try! JavaVirtualMachine.shared().environment(), className: "java/lang/Exception", methods: [messageConstructor] ) diff --git a/Sources/SwiftJavaRuntimeSupport/_JNIBoxedConversions.swift b/Sources/SwiftJavaRuntimeSupport/_JNIBoxedConversions.swift index 68d98ffcd..ad4572caa 100644 --- a/Sources/SwiftJavaRuntimeSupport/_JNIBoxedConversions.swift +++ b/Sources/SwiftJavaRuntimeSupport/_JNIBoxedConversions.swift @@ -26,47 +26,39 @@ public enum _JNIBoxedConversions { private static let doubleMethod = _JNIMethodIDCache.Method(name: "valueOf", signature: "(D)Ljava/lang/Double;", isStatic: true) private static let booleanCache = _JNIMethodIDCache( - environment: try! JavaVirtualMachine.shared().environment(), className: "java/lang/Boolean", methods: [booleanMethod] ) private static let byteCache = _JNIMethodIDCache( - environment: try! JavaVirtualMachine.shared().environment(), className: "java/lang/Byte", methods: [byteMethod] ) private static let charCache = _JNIMethodIDCache( - environment: try! JavaVirtualMachine.shared().environment(), className: "java/lang/Character", methods: [charMethod] ) private static let shortCache = _JNIMethodIDCache( - environment: try! JavaVirtualMachine.shared().environment(), className: "java/lang/Short", methods: [shortMethod] ) private static let intCache = _JNIMethodIDCache( - environment: try! JavaVirtualMachine.shared().environment(), className: "java/lang/Integer", methods: [intMethod] ) private static let longCache = _JNIMethodIDCache( - environment: try! JavaVirtualMachine.shared().environment(), className: "java/lang/Long", methods: [longMethod] ) private static let floatCache = _JNIMethodIDCache( - environment: try! JavaVirtualMachine.shared().environment(), className: "java/lang/Float", methods: [floatMethod] ) private static let doubleCache = _JNIMethodIDCache( - environment: try! JavaVirtualMachine.shared().environment(), className: "java/lang/Double", methods: [doubleMethod] ) diff --git a/Sources/SwiftJavaRuntimeSupport/_JNIMethodIDCache.swift b/Sources/SwiftJavaRuntimeSupport/_JNIMethodIDCache.swift index dd7eb5d13..fbadf1916 100644 --- a/Sources/SwiftJavaRuntimeSupport/_JNIMethodIDCache.swift +++ b/Sources/SwiftJavaRuntimeSupport/_JNIMethodIDCache.swift @@ -39,11 +39,33 @@ public final class _JNIMethodIDCache: Sendable { 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!") + /// An optional reference to a java object holder + /// if we cached this class through the class loader + /// This is to make sure that the underlying reference remains valid + nonisolated(unsafe) private let javaObjectHolder: JavaObjectHolder? + + public init(className: String, methods: [Method]) { + let environment = try! JavaVirtualMachine.shared().environment() + + let clazz: jobject + if let jniClass = environment.interface.FindClass(environment, className) { + clazz = environment.interface.NewGlobalRef(environment, jniClass)! + self.javaObjectHolder = nil + } else { + // Clear any ClassNotFound exceptions from FindClass + environment.interface.ExceptionClear(environment) + + if let javaClass = try? JNI.shared.applicationClassLoader.loadClass( + className.replacingOccurrences(of: "/", with: ".") + ) { + clazz = javaClass.javaThis + self.javaObjectHolder = javaClass.javaHolder + } else { + fatalError("Class \(className) could not be found!") + } } - self._class = environment.interface.NewGlobalRef(environment, clazz)! + + self._class = clazz self.methods = methods.reduce(into: [:]) { (result, method) in if method.isStatic { if let methodID = environment.interface.GetStaticMethodID(environment, clazz, method.name, method.signature) { @@ -61,7 +83,6 @@ public final class _JNIMethodIDCache: Sendable { } } - public subscript(_ method: Method) -> jmethodID? { methods[method] } From 3be8df2d8d47f45cb8e14805de3685814adaba82 Mon Sep 17 00:00:00 2001 From: Mads Odgaard Date: Tue, 2 Dec 2025 15:35:58 +0100 Subject: [PATCH 08/14] jextract: add support for implementing Swift protocols in Java (#449) Co-authored-by: Konrad `ktoso` Malawski --- .../JExtractSwiftPlugin.swift | 199 +++++++++- Plugins/SwiftJavaPlugin/SwiftJavaPlugin.swift | 4 +- .../JavaDependencySampleApp/ci-validate.sh | 11 +- Samples/JavaKitSampleApp/ci-validate.sh | 10 +- Samples/JavaProbablyPrime/ci-validate.sh | 10 +- .../MySwiftLibrary/CallbackProtcol.swift | 77 ++++ .../Sources/MySwiftLibrary/Storage.swift | 36 ++ .../Sources/MySwiftLibrary/swift-java.config | 1 + .../SwiftJavaExtractJNISampleApp/build.gradle | 10 +- .../ci-validate.sh | 41 ++- .../example/swift/ProtocolCallbacksTest.java | 117 ++++++ .../java/com/example/swift/ProtocolTest.java | 30 ++ .../Convenience/JavaType+Extensions.swift | 2 +- .../Convenience/String+Extensions.swift | 2 +- .../Convenience/SwiftSyntax+Extensions.swift | 2 +- Sources/JExtractSwiftLib/ImportedDecls.swift | 9 + ...Generator+InterfaceWrapperGeneration.swift | 342 ++++++++++++++++++ ...t2JavaGenerator+JavaBindingsPrinting.swift | 63 +++- ...ISwift2JavaGenerator+JavaTranslation.swift | 59 +-- ...wift2JavaGenerator+NativeTranslation.swift | 133 +++++-- ...ift2JavaGenerator+SwiftThunkPrinting.swift | 226 +++++++++++- .../JNI/JNISwift2JavaGenerator.swift | 8 + .../JavaTypes/JavaType+JDK.swift | 6 + .../SwiftTypes/SwiftKnownTypeDecls.swift | 14 + .../SwiftTypes/SwiftType.swift | 6 +- .../Constructor+Utilities.swift | 5 - .../JavaLangReflect/HasJavaModifiers.swift | 47 +++ .../JavaLangReflect/Method+Utilities.swift | 22 -- .../SwiftJava/JVM/JavaVirtualMachine.swift | 2 +- Sources/SwiftJava/Macros.swift | 5 +- Sources/SwiftJava/String+Extensions.swift | 10 +- .../Configuration.swift | 12 +- .../Documentation.docc/index.md | 2 +- Sources/SwiftJavaMacros/JavaMethodMacro.swift | 18 +- .../JNIMethodIDCaches.swift | 29 ++ .../generated/JavaJNISwiftInstance.swift | 21 ++ .../Commands/ConfigureCommand.swift | 115 +++--- .../Commands/JExtractCommand.swift | 18 +- .../Commands/WrapJavaCommand.swift | 102 +++++- Sources/SwiftJavaTool/CommonOptions.swift | 5 +- .../SwiftJavaBaseAsyncParsableCommand.swift | 14 +- .../JavaClassTranslator.swift | 14 + Sources/SwiftJavaToolLib/StringExtras.swift | 5 + .../JExtractSwiftTests/JNI/JNIEnumTests.swift | 10 +- .../JNI/JNIProtocolTests.swift | 237 ++++++++---- .../MemoryManagementModeTests.swift | 2 +- .../WrapJavaTests/GenericsWrapJavaTests.swift | 41 ++- 47 files changed, 1871 insertions(+), 283 deletions(-) create mode 100644 Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/CallbackProtcol.swift create mode 100644 Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Storage.swift create mode 100644 Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ProtocolCallbacksTest.java create mode 100644 Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+InterfaceWrapperGeneration.swift create mode 100644 Sources/JavaStdlib/JavaLangReflect/HasJavaModifiers.swift create mode 100644 Sources/SwiftJavaRuntimeSupport/generated/JavaJNISwiftInstance.swift diff --git a/Plugins/JExtractSwiftPlugin/JExtractSwiftPlugin.swift b/Plugins/JExtractSwiftPlugin/JExtractSwiftPlugin.swift index 8d0be4559..2808b6d31 100644 --- a/Plugins/JExtractSwiftPlugin/JExtractSwiftPlugin.swift +++ b/Plugins/JExtractSwiftPlugin/JExtractSwiftPlugin.swift @@ -25,9 +25,12 @@ struct JExtractSwiftBuildToolPlugin: SwiftJavaPluginProtocol, BuildToolPlugin { func createBuildCommands(context: PluginContext, target: Target) throws -> [Command] { let toolURL = try context.tool(named: "SwiftJavaTool").url - + + var commands: [Command] = [] + guard let sourceModule = target.sourceModule else { return [] } + // Note: Target doesn't have a directoryURL counterpart to directory, // so we cannot eliminate this deprecation warning. for dependency in target.dependencies { @@ -80,7 +83,7 @@ struct JExtractSwiftBuildToolPlugin: SwiftJavaPluginProtocol, BuildToolPlugin { let (moduleName, configFile) = moduleAndConfigFile return [ "--depends-on", - "\(configFile.path(percentEncoded: false))" + "\(moduleName)=\(configFile.path(percentEncoded: false))" ] } arguments += dependentConfigFilesArguments @@ -123,15 +126,165 @@ struct JExtractSwiftBuildToolPlugin: SwiftJavaPluginProtocol, BuildToolPlugin { print("[swift-java-plugin] Output swift files:\n - \(outputSwiftFiles.map({$0.absoluteString}).joined(separator: "\n - "))") - return [ + var jextractOutputFiles = outputSwiftFiles + + // If the developer has enabled java callbacks in the configuration (default is false) + // and we are running in JNI mode, we will run additional phases in this build plugin + // to generate Swift wrappers using wrap-java that can be used to callback to Java. + let shouldRunJavaCallbacksPhases = + if let configuration, + configuration.enableJavaCallbacks == true, + configuration.effectiveMode == .jni { + true + } else { + false + } + + // Extract list of all sources + let javaSourcesListFileName = "jextract-generated-sources.txt" + let javaSourcesFile = outputJavaDirectory.appending(path: javaSourcesListFileName) + if shouldRunJavaCallbacksPhases { + arguments += [ + "--generated-java-sources-list-file-output", javaSourcesListFileName + ] + jextractOutputFiles += [javaSourcesFile] + } + + commands += [ .buildCommand( displayName: "Generate Java wrappers for Swift types", executable: toolURL, arguments: arguments, inputFiles: [ configFile ] + swiftFiles, - outputFiles: outputSwiftFiles + outputFiles: jextractOutputFiles + ) + ] + + // If we do not need Java callbacks, we can skip the remaining steps. + guard shouldRunJavaCallbacksPhases else { + return commands + } + + // The URL of the compiled Java sources + let javaCompiledClassesURL = context.pluginWorkDirectoryURL + .appending(path: "compiled-java-output") + + // Build SwiftKitCore and get the classpath + // as the jextracted sources will depend on that + + guard let swiftJavaDirectory = findSwiftJavaDirectory(for: target) else { + fatalError("Unable to find the path to the swift-java sources, please file an issue.") + } + log("Found swift-java at \(swiftJavaDirectory)") + + let swiftKitCoreClassPath = swiftJavaDirectory.appending(path: "SwiftKitCore/build/classes/java/main") + + // We need to use a different gradle home, because + // this plugin might be run from inside another gradle task + // and that would cause conflicts. + let gradleUserHome = context.pluginWorkDirectoryURL.appending(path: "gradle-user-home") + + let GradleUserHome = "GRADLE_USER_HOME" + let gradleUserHomePath = gradleUserHome.path(percentEncoded: false) + log("Prepare command: :SwiftKitCore:build in \(GradleUserHome)=\(gradleUserHomePath)") + var gradlewEnvironment = ProcessInfo.processInfo.environment + gradlewEnvironment[GradleUserHome] = gradleUserHomePath + log("Forward environment: \(gradlewEnvironment)") + + let gradleExecutable = findExecutable(name: "gradle") ?? // try using installed 'gradle' if available in PATH + swiftJavaDirectory.appending(path: "gradlew") // fallback to calling ./gradlew if gradle is not installed + log("Detected 'gradle' executable (or gradlew fallback): \(gradleExecutable)") + + commands += [ + .buildCommand( + displayName: "Build SwiftKitCore using Gradle (Java)", + executable: gradleExecutable, + arguments: [ + ":SwiftKitCore:build", + "--project-dir", swiftJavaDirectory.path(percentEncoded: false), + "--gradle-user-home", gradleUserHomePath, + "--configure-on-demand", + "--no-daemon" + ], + environment: gradlewEnvironment, + inputFiles: [swiftJavaDirectory], + outputFiles: [swiftKitCoreClassPath] + ) + ] + + // Compile the jextracted sources + let javaHome = URL(filePath: findJavaHome()) + + commands += [ + .buildCommand( + displayName: "Build extracted Java sources", + executable: javaHome + .appending(path: "bin") + .appending(path: self.javacName), + arguments: [ + "@\(javaSourcesFile.path(percentEncoded: false))", + "-d", javaCompiledClassesURL.path(percentEncoded: false), + "-parameters", + "-classpath", swiftKitCoreClassPath.path(percentEncoded: false) + ], + inputFiles: [javaSourcesFile, swiftKitCoreClassPath], + outputFiles: [javaCompiledClassesURL] + ) + ] + + // Run `configure` to extract a swift-java config to use for wrap-java + let swiftJavaConfigURL = context.pluginWorkDirectoryURL.appending(path: "swift-java.config") + + commands += [ + .buildCommand( + displayName: "Output swift-java.config that contains all extracted Java sources", + executable: toolURL, + arguments: [ + "configure", + "--output-directory", context.pluginWorkDirectoryURL.path(percentEncoded: false), + "--cp", javaCompiledClassesURL.path(percentEncoded: false), + "--swift-module", sourceModule.name, + "--swift-type-prefix", "Java" + ], + inputFiles: [javaCompiledClassesURL], + outputFiles: [swiftJavaConfigURL] ) ] + + let singleSwiftFileOutputName = "WrapJavaGenerated.swift" + + // In the end we can run wrap-java on the previous inputs + var wrapJavaArguments = [ + "wrap-java", + "--swift-module", sourceModule.name, + "--output-directory", outputSwiftDirectory.path(percentEncoded: false), + "--config", swiftJavaConfigURL.path(percentEncoded: false), + "--cp", swiftKitCoreClassPath.path(percentEncoded: false), + "--single-swift-file-output", singleSwiftFileOutputName + ] + + // Add any dependent config files as arguments + wrapJavaArguments += dependentConfigFilesArguments + + commands += [ + .buildCommand( + displayName: "Wrap compiled Java sources using wrap-java", + executable: toolURL, + arguments: wrapJavaArguments, + inputFiles: [swiftJavaConfigURL, swiftKitCoreClassPath], + outputFiles: [outputSwiftDirectory.appending(path: singleSwiftFileOutputName)] + ) + ] + + return commands + } + + var javacName: String { +#if os(Windows) + "javac.exe" +#else + "javac" +#endif } /// Find the manifest files from other swift-java executions in any targets @@ -181,5 +334,43 @@ struct JExtractSwiftBuildToolPlugin: SwiftJavaPluginProtocol, BuildToolPlugin { return dependentConfigFiles } + + private func findSwiftJavaDirectory(for target: any Target) -> URL? { + for dependency in target.dependencies { + switch dependency { + case .target(let target): + continue + + case .product(let product): + guard let swiftJava = product.sourceModules.first(where: { $0.name == "SwiftJava" }) else { + return nil + } + + // We are inside Sources/SwiftJava + return swiftJava.directoryURL.deletingLastPathComponent().deletingLastPathComponent() + + @unknown default: + continue + } + } + + return nil + } } +func findExecutable(name: String) -> URL? { + let fileManager = FileManager.default + + guard let path = ProcessInfo.processInfo.environment["PATH"] else { + return nil + } + + for path in path.split(separator: ":") { + let fullURL = URL(fileURLWithPath: String(path)).appendingPathComponent(name) + if fileManager.isExecutableFile(atPath: fullURL.path) { + return fullURL + } + } + + return nil +} \ No newline at end of file diff --git a/Plugins/SwiftJavaPlugin/SwiftJavaPlugin.swift b/Plugins/SwiftJavaPlugin/SwiftJavaPlugin.swift index 7908932d1..a578091f1 100644 --- a/Plugins/SwiftJavaPlugin/SwiftJavaPlugin.swift +++ b/Plugins/SwiftJavaPlugin/SwiftJavaPlugin.swift @@ -171,8 +171,6 @@ struct SwiftJavaBuildToolPlugin: SwiftJavaPluginProtocol, BuildToolPlugin { arguments += javaStdlibModules.flatMap { ["--depends-on", $0] } if !outputSwiftFiles.isEmpty { - arguments += [ configFile.path(percentEncoded: false) ] - let displayName = "Wrapping \(classes.count) Java classes in Swift target '\(sourceModule.name)'" log("Prepared: \(displayName)") commands += [ @@ -266,4 +264,4 @@ func getExtractedJavaStdlibModules() -> [String] { } return url.lastPathComponent }.sorted() -} \ No newline at end of file +} diff --git a/Samples/JavaDependencySampleApp/ci-validate.sh b/Samples/JavaDependencySampleApp/ci-validate.sh index feeb87675..1c3e2d552 100755 --- a/Samples/JavaDependencySampleApp/ci-validate.sh +++ b/Samples/JavaDependencySampleApp/ci-validate.sh @@ -3,9 +3,16 @@ set -e set -x +# WORKAROUND: prebuilts broken on Swift 6.2.1 and Linux and tests using macros https://github.com/swiftlang/swift-java/issues/418 +if [ "$(uname)" = "Darwin" ]; then + DISABLE_EXPERIMENTAL_PREBUILTS='' +else + DISABLE_EXPERIMENTAL_PREBUILTS='--disable-experimental-prebuilts' +fi + # invoke resolve as part of a build run swift build \ - --disable-experimental-prebuilts \ + $DISABLE_EXPERIMENTAL_PREBUILTS \ --disable-sandbox # explicitly invoke resolve without explicit path or dependency @@ -13,7 +20,7 @@ swift build \ # FIXME: until prebuilt swift-syntax isn't broken on 6.2 anymore: https://github.com/swiftlang/swift-java/issues/418 swift run \ - --disable-experimental-prebuilts \ + $DISABLE_EXPERIMENTAL_PREBUILTS \ swift-java resolve \ Sources/JavaCommonsCSV/swift-java.config \ --swift-module JavaCommonsCSV \ diff --git a/Samples/JavaKitSampleApp/ci-validate.sh b/Samples/JavaKitSampleApp/ci-validate.sh index 297f5c885..327baadf9 100755 --- a/Samples/JavaKitSampleApp/ci-validate.sh +++ b/Samples/JavaKitSampleApp/ci-validate.sh @@ -3,8 +3,14 @@ set -e set -x -swift build \ - --disable-experimental-prebuilts # FIXME: until prebuilt swift-syntax isn't broken on 6.2 anymore: https://github.com/swiftlang/swift-java/issues/418 +# WORKAROUND: prebuilts broken on Swift 6.2.1 and Linux and tests using macros https://github.com/swiftlang/swift-java/issues/418 +if [ "$(uname)" = "Darwin" ]; then + DISABLE_EXPERIMENTAL_PREBUILTS='' +else + DISABLE_EXPERIMENTAL_PREBUILTS='--disable-experimental-prebuilts' +fi + +swift build $DISABLE_EXPERIMENTAL_PREBUILTS "$JAVA_HOME/bin/java" \ -cp .build/plugins/outputs/javakitsampleapp/JavaKitExample/destination/JavaCompilerPlugin/Java \ diff --git a/Samples/JavaProbablyPrime/ci-validate.sh b/Samples/JavaProbablyPrime/ci-validate.sh index dc6249969..202dcbabe 100755 --- a/Samples/JavaProbablyPrime/ci-validate.sh +++ b/Samples/JavaProbablyPrime/ci-validate.sh @@ -3,7 +3,13 @@ set -e set -x -# FIXME: until prebuilt swift-syntax isn't broken on 6.2 anymore: https://github.com/swiftlang/swift-java/issues/418 +# WORKAROUND: prebuilts broken on Swift 6.2.1 and Linux and tests using macros https://github.com/swiftlang/swift-java/issues/418 +if [ "$(uname)" = "Darwin" ]; then + DISABLE_EXPERIMENTAL_PREBUILTS='' +else + DISABLE_EXPERIMENTAL_PREBUILTS='--disable-experimental-prebuilts' +fi + swift run \ - --disable-experimental-prebuilts \ + $DISABLE_EXPERIMENTAL_PREBUILTS \ JavaProbablyPrime 1337 \ No newline at end of file diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/CallbackProtcol.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/CallbackProtcol.swift new file mode 100644 index 000000000..985771bef --- /dev/null +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/CallbackProtcol.swift @@ -0,0 +1,77 @@ +//===----------------------------------------------------------------------===// +// +// 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 SwiftJava + +public protocol CallbackProtocol { + func withBool(_ input: Bool) -> Bool + func withInt8(_ input: Int8) -> Int8 + func withUInt16(_ input: UInt16) -> UInt16 + func withInt16(_ input: Int16) -> Int16 + func withInt32(_ input: Int32) -> Int32 + func withInt64(_ input: Int64) -> Int64 + func withFloat(_ input: Float) -> Float + func withDouble(_ input: Double) -> Double + func withString(_ input: String) -> String + func withVoid() + func withObject(_ input: MySwiftClass) -> MySwiftClass + func withOptionalInt64(_ input: Int64?) -> Int64? + func withOptionalObject(_ input: MySwiftClass?) -> Optional +} + +public struct CallbackOutput { + public let bool: Bool + public let int8: Int8 + public let uint16: UInt16 + public let int16: Int16 + public let int32: Int32 + public let int64: Int64 + public let _float: Float + public let _double: Double + public let string: String + public let object: MySwiftClass + public let optionalInt64: Int64? + public let optionalObject: MySwiftClass? +} + +public func outputCallbacks( + _ callbacks: some CallbackProtocol, + bool: Bool, + int8: Int8, + uint16: UInt16, + int16: Int16, + int32: Int32, + int64: Int64, + _float: Float, + _double: Double, + string: String, + object: MySwiftClass, + optionalInt64: Int64?, + optionalObject: MySwiftClass? +) -> CallbackOutput { + return CallbackOutput( + bool: callbacks.withBool(bool), + int8: callbacks.withInt8(int8), + uint16: callbacks.withUInt16(uint16), + int16: callbacks.withInt16(int16), + int32: callbacks.withInt32(int32), + int64: callbacks.withInt64(int64), + _float: callbacks.withFloat(_float), + _double: callbacks.withDouble(_double), + string: callbacks.withString(string), + object: callbacks.withObject(object), + optionalInt64: callbacks.withOptionalInt64(optionalInt64), + optionalObject: callbacks.withOptionalObject(optionalObject) + ) +} diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Storage.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Storage.swift new file mode 100644 index 000000000..488a78cc1 --- /dev/null +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Storage.swift @@ -0,0 +1,36 @@ +//===----------------------------------------------------------------------===// +// +// 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 SwiftJava + +public class StorageItem { + public let value: Int64 + + public init(value: Int64) { + self.value = value + } +} + +public protocol Storage { + func load() -> StorageItem + func save(_ item: StorageItem) +} + +public func saveWithStorage(_ item: StorageItem, s: any Storage) { + s.save(item); +} + +public func loadWithStorage(s: any Storage) -> StorageItem { + return s.load(); +} diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/swift-java.config b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/swift-java.config index 3d6a12012..52143b7f7 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/swift-java.config +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/swift-java.config @@ -1,5 +1,6 @@ { "javaPackage": "com.example.swift", "mode": "jni", + "enableJavaCallbacks": true, "logLevel": "debug" } diff --git a/Samples/SwiftJavaExtractJNISampleApp/build.gradle b/Samples/SwiftJavaExtractJNISampleApp/build.gradle index a8ce51d27..7bb64c554 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/build.gradle +++ b/Samples/SwiftJavaExtractJNISampleApp/build.gradle @@ -101,7 +101,7 @@ def jextract = tasks.register("jextract", Exec) { } // FIXME: disable prebuilts until swift-syntax isn't broken on 6.2 anymore: https://github.com/swiftlang/swift-java/issues/418 - def cmdArgs = ["build", "--disable-experimental-prebuilts"] + def cmdArgs = ["build", "--disable-experimental-prebuilts", "--disable-sandbox"] // Check if the 'swiftSdk' project property was passed if (project.hasProperty('swiftSdk')) { @@ -209,3 +209,11 @@ jmh { "-Djextract.trace.downcalls=false" ] } + +task printGradleHome { + doLast { + println "Gradle Home: ${gradle.gradleHomeDir}" + println "Gradle Version: ${gradle.gradleVersion}" + println "Gradle User Home: ${gradle.gradleUserHomeDir}" + } +} \ No newline at end of file diff --git a/Samples/SwiftJavaExtractJNISampleApp/ci-validate.sh b/Samples/SwiftJavaExtractJNISampleApp/ci-validate.sh index ff5c32c80..d62a09102 100755 --- a/Samples/SwiftJavaExtractJNISampleApp/ci-validate.sh +++ b/Samples/SwiftJavaExtractJNISampleApp/ci-validate.sh @@ -3,7 +3,44 @@ set -x set -e -swift build --disable-experimental-prebuilts # FIXME: until prebuilt swift-syntax isn't broken on 6.2 anymore: https://github.com/swiftlang/swift-java/issues/418 +# WORKAROUND: prebuilts broken on Swift 6.2.1 and Linux and tests using macros https://github.com/swiftlang/swift-java/issues/418 +if [ "$(uname)" = "Darwin" ]; then + DISABLE_EXPERIMENTAL_PREBUILTS='' +else + DISABLE_EXPERIMENTAL_PREBUILTS='--disable-experimental-prebuilts' +fi + +if [[ "$(uname)" == "Darwin" && -n "$GITHUB_ACTION" ]]; then + # WORKAROUND: GitHub Actions on macOS issue with downloading gradle wrapper + # We seem to be hitting a problem when the swiftpm plugin, needs to execute gradle wrapper in a new gradle_user_home. + # Normally, this would just download gradle again and kick off a build, this seems to timeout *specifically* on + # github actions runners. + # + # It is not a sandbox problem, becuase the ./gradlew is run without sandboxing as we already execute + # the entire swift build with '--disable-sandbox' for other reasons. + # + # We cannot use the same gradle user home as the default one since we might make gradle think we're + # building the same project concurrently, which we kind of are, however only a limited subset in order + # to trigger wrap-java with those dependencies. + # + # TODO: this may use some further improvements so normal usage does not incur another wrapper download. + + ./gradlew -h # prime ~/.gradle/wrapper/dists/... + + # Worst part of workaround here; we make sure to pre-load the resolved gradle wrapper downloaded distribution + # to the "known" location the plugin will use for its local builds, which are done in order to compile SwiftKitCore. + # This build is only necessary in order to drive wrap-java on sources generated during the build itself + # which enables the "Implement Swift protocols in Java" feature of jextract/jni mode. + GRADLE_USER_HOME="$(pwd)/.build/plugins/outputs/swiftjavaextractjnisampleapp/MySwiftLibrary/destination/JExtractSwiftPlugin/gradle-user-home" + if [ -d "$HOME/.gradle" ] ; then + echo "COPY $HOME/.gradle to $GRADLE_USER_HOME" + mkdir -p "$GRADLE_USER_HOME" + cp -r "$HOME/.gradle/"* "$GRADLE_USER_HOME/" || true + fi +fi + +# FIXME: disable prebuilts until prebuilt swift-syntax isn't broken on 6.2 anymore: https://github.com/swiftlang/swift-java/issues/418 +swift build $DISABLE_EXPERIMENTAL_PREBUILTS --disable-sandbox ./gradlew run -./gradlew test \ No newline at end of file +./gradlew test diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ProtocolCallbacksTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ProtocolCallbacksTest.java new file mode 100644 index 000000000..e79fd4a3b --- /dev/null +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ProtocolCallbacksTest.java @@ -0,0 +1,117 @@ +//===----------------------------------------------------------------------===// +// +// 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.SwiftArena; +import org.swift.swiftkit.core.annotations.Unsigned; + +import java.util.Optional; +import java.util.OptionalLong; + +import static org.junit.jupiter.api.Assertions.*; + +public class ProtocolCallbacksTest { + static class JavaCallbacks implements CallbackProtocol { + @Override + public boolean withBool(boolean input) { + return input; + } + + @Override + public byte withInt8(byte input) { + return input; + } + + @Override + public @Unsigned char withUInt16(char input) { + return input; + } + + @Override + public short withInt16(short input) { + return input; + } + + @Override + public int withInt32(int input) { + return input; + } + + @Override + public long withInt64(long input) { + return input; + } + + @Override + public float withFloat(float input) { + return input; + } + + @Override + public double withDouble(double input) { + return input; + } + + @Override + public String withString(String input) { + return input; + } + + @Override + public void withVoid() {} + + @Override + public MySwiftClass withObject(MySwiftClass input) { + return input; + } + + @Override + public OptionalLong withOptionalInt64(OptionalLong input) { + return input; + } + + @Override + public Optional withOptionalObject(Optional input) { + return input; + } + } + + @Test + void primitiveCallbacks() { + try (var arena = SwiftArena.ofConfined()) { + JavaCallbacks callbacks = new JavaCallbacks(); + var object = MySwiftClass.init(5, 3, arena); + var optionalObject = Optional.of(MySwiftClass.init(10, 10, arena)); + var output = MySwiftLibrary.outputCallbacks(callbacks, true, (byte) 1, (char) 16, (short) 16, (int) 32, 64L, 1.34f, 1.34, "Hello from Java!", object, OptionalLong.empty(), optionalObject, arena); + + assertEquals(1, output.getInt8()); + assertEquals(16, output.getUint16()); + assertEquals(16, output.getInt16()); + assertEquals(32, output.getInt32()); + assertEquals(64, output.getInt64()); + assertEquals(1.34f, output.get_float()); + assertEquals(1.34, output.get_double()); + assertEquals("Hello from Java!", output.getString()); + assertFalse(output.getOptionalInt64().isPresent()); + assertEquals(5, output.getObject(arena).getX()); + assertEquals(3, output.getObject(arena).getY()); + + var optionalObjectOutput = output.getOptionalObject(arena); + assertTrue(optionalObjectOutput.isPresent()); + assertEquals(10, optionalObjectOutput.get().getX()); + } + } +} \ No newline at end of file diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ProtocolTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ProtocolTest.java index c095a42a4..f5d1ffcf7 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ProtocolTest.java +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ProtocolTest.java @@ -72,4 +72,34 @@ void protocolMethod() { assertEquals("ConcreteProtocolAB", proto1.name()); } } + + static class JavaStorage implements Storage { + StorageItem item; + + JavaStorage(StorageItem item) { + this.item = item; + } + + @Override + public StorageItem load() { + return item; + } + + @Override + public void save(StorageItem item) { + this.item = item; + } + } + + @Test + void useStorage() { + try (var arena = SwiftArena.ofConfined()) { + JavaStorage storage = new JavaStorage(null); + MySwiftLibrary.saveWithStorage(StorageItem.init(10, arena), storage); + assertEquals(10, MySwiftLibrary.loadWithStorage(storage, arena).getValue()); + MySwiftLibrary.saveWithStorage(StorageItem.init(7, arena), storage); + MySwiftLibrary.saveWithStorage(StorageItem.init(5, arena), storage); + assertEquals(5, MySwiftLibrary.loadWithStorage(storage, arena).getValue()); + } + } } \ No newline at end of file diff --git a/Sources/JExtractSwiftLib/Convenience/JavaType+Extensions.swift b/Sources/JExtractSwiftLib/Convenience/JavaType+Extensions.swift index 645e5aa48..9f7a19cce 100644 --- a/Sources/JExtractSwiftLib/Convenience/JavaType+Extensions.swift +++ b/Sources/JExtractSwiftLib/Convenience/JavaType+Extensions.swift @@ -102,7 +102,7 @@ extension JavaType { } } - /// Returns whether this type returns `JavaValue` from JavaKit + /// Returns whether this type returns `JavaValue` from SwiftJava var implementsJavaValue: Bool { return switch self { case .boolean, .byte, .char, .short, .int, .long, .float, .double, .void, .javaLangString: diff --git a/Sources/JExtractSwiftLib/Convenience/String+Extensions.swift b/Sources/JExtractSwiftLib/Convenience/String+Extensions.swift index 82ce5c1c0..5641c7f23 100644 --- a/Sources/JExtractSwiftLib/Convenience/String+Extensions.swift +++ b/Sources/JExtractSwiftLib/Convenience/String+Extensions.swift @@ -68,7 +68,7 @@ extension String { /// Looks up self as a SwiftJava wrapped class name and converts it /// into a `JavaType.class` if it exists in `lookupTable`. - func parseJavaClassFromJavaKitName(in lookupTable: [String: String]) -> JavaType? { + func parseJavaClassFromSwiftJavaName(in lookupTable: [String: String]) -> JavaType? { guard let canonicalJavaName = lookupTable[self] else { return nil } diff --git a/Sources/JExtractSwiftLib/Convenience/SwiftSyntax+Extensions.swift b/Sources/JExtractSwiftLib/Convenience/SwiftSyntax+Extensions.swift index d3902aa4d..c9f87b6dd 100644 --- a/Sources/JExtractSwiftLib/Convenience/SwiftSyntax+Extensions.swift +++ b/Sources/JExtractSwiftLib/Convenience/SwiftSyntax+Extensions.swift @@ -128,7 +128,7 @@ extension WithModifiersSyntax { } extension AttributeListSyntax.Element { - /// Whether this node has `JavaKit` attributes. + /// Whether this node has `SwiftJava` attributes. var isJava: Bool { guard case let .attribute(attr) = self else { // FIXME: Handle #if. diff --git a/Sources/JExtractSwiftLib/ImportedDecls.swift b/Sources/JExtractSwiftLib/ImportedDecls.swift index b9cc2d497..aa4014d21 100644 --- a/Sources/JExtractSwiftLib/ImportedDecls.swift +++ b/Sources/JExtractSwiftLib/ImportedDecls.swift @@ -264,3 +264,12 @@ extension ImportedFunc { } } } + +extension ImportedNominalType: Hashable { + public func hash(into hasher: inout Hasher) { + hasher.combine(ObjectIdentifier(self)) + } + public static func == (lhs: ImportedNominalType, rhs: ImportedNominalType) -> Bool { + return lhs === rhs + } +} diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+InterfaceWrapperGeneration.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+InterfaceWrapperGeneration.swift new file mode 100644 index 000000000..f1e7c6851 --- /dev/null +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+InterfaceWrapperGeneration.swift @@ -0,0 +1,342 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import JavaTypes +import SwiftJavaConfigurationShared +import SwiftSyntax + +extension JNISwift2JavaGenerator { + + func generateInterfaceWrappers( + _ types: [ImportedNominalType] + ) -> [ImportedNominalType: JavaInterfaceSwiftWrapper] { + var wrappers = [ImportedNominalType: JavaInterfaceSwiftWrapper]() + + for type in types { + do { + let translator = JavaInterfaceProtocolWrapperGenerator() + wrappers[type] = try translator.generate(for: type) + } catch { + self.logger.warning("Failed to generate protocol wrapper for: '\(type.swiftNominal.qualifiedName)'; \(error)") + } + } + + return wrappers + } + + /// A type that describes a Swift protocol + /// that uses an underlying wrap-java `@JavaInterface` + /// to make callbacks to Java from Swift using protocols. + struct JavaInterfaceSwiftWrapper { + let protocolType: SwiftNominalType + let functions: [Function] + let variables: [Variable] + + var wrapperName: String { + protocolType.nominalTypeDecl.javaInterfaceSwiftProtocolWrapperName + } + + var swiftName: String { + protocolType.nominalTypeDecl.qualifiedName + } + + var javaInterfaceVariableName: String { + protocolType.nominalTypeDecl.javaInterfaceVariableName + } + + var javaInterfaceName: String { + protocolType.nominalTypeDecl.javaInterfaceName + } + + struct Function { + let swiftFunctionName: String + let originalFunctionSignature: SwiftFunctionSignature + let swiftDecl: any DeclSyntaxProtocol + let parameterConversions: [UpcallConversionStep] + let resultConversion: UpcallConversionStep + } + + struct Variable { + let swiftDecl: any DeclSyntaxProtocol + let getter: Function + let setter: Function? + } + } + + + struct JavaInterfaceProtocolWrapperGenerator { + func generate(for type: ImportedNominalType) throws -> JavaInterfaceSwiftWrapper { + let functions = try type.methods.map { method in + try translate(function: method) + } + + // FIXME: Finish support for variables + if !type.variables.isEmpty { + throw JavaTranslationError.protocolVariablesNotSupported + } + + let variables = try Dictionary(grouping: type.variables, by: { $0.swiftDecl.id }).map { (id, funcs) in + precondition(funcs.count > 0 && funcs.count <= 2, "Variables must contain a getter and optionally a setter") + guard let getter = funcs.first(where: { $0.apiKind == .getter }) else { + fatalError("Getter not found for variable with imported funcs: \(funcs)") + } + let setter = funcs.first(where: { $0.apiKind == .setter }) + + return try self.translateVariable(getter: getter, setter: setter) + } + + return JavaInterfaceSwiftWrapper( + protocolType: SwiftNominalType(nominalTypeDecl: type.swiftNominal), + functions: functions, + variables: variables + ) + } + + private func translate(function: ImportedFunc) throws -> JavaInterfaceSwiftWrapper.Function { + let parameters = try function.functionSignature.parameters.map { + try self.translateParameter($0) + } + + let result = try translateResult(function.functionSignature.result, methodName: function.name) + + return JavaInterfaceSwiftWrapper.Function( + swiftFunctionName: function.name, + originalFunctionSignature: function.functionSignature, + swiftDecl: function.swiftDecl, + parameterConversions: parameters, + resultConversion: result + ) + } + + private func translateVariable(getter: ImportedFunc, setter: ImportedFunc?) throws -> JavaInterfaceSwiftWrapper.Variable { + return try JavaInterfaceSwiftWrapper.Variable( + swiftDecl: getter.swiftDecl, // they should be the same + getter: translate(function: getter), + setter: setter.map { try self.translate(function: $0) } + ) + } + + private func translateParameter(_ parameter: SwiftParameter) throws -> UpcallConversionStep { + try self.translateParameter(parameterName: parameter.parameterName!, type: parameter.type) + } + + private func translateParameter(parameterName: String, type: SwiftType) throws -> UpcallConversionStep { + + switch type { + case .nominal(let nominalType): + if let knownType = nominalType.nominalTypeDecl.knownTypeKind { + switch knownType { + case .optional: + guard let genericArgs = nominalType.genericArguments, genericArgs.count == 1 else { + throw JavaTranslationError.unsupportedSwiftType(type) + } + return try translateOptionalParameter( + name: parameterName, + wrappedType: genericArgs[0] + ) + + default: + guard knownType.isDirectlyTranslatedToWrapJava else { + throw JavaTranslationError.unsupportedSwiftType(type) + } + + return .placeholder + } + } + + // We assume this is then a JExtracted Swift class + return .toJavaWrapper( + .placeholder, + name: parameterName, + nominalType: nominalType + ) + + case .tuple([]): // void + return .placeholder + + case .optional(let wrappedType): + return try translateOptionalParameter( + name: parameterName, + wrappedType: wrappedType + ) + + case .genericParameter, .function, .metatype, .tuple, .existential, .opaque, .composite, .array: + throw JavaTranslationError.unsupportedSwiftType(type) + } + } + + private func translateOptionalParameter(name: String, wrappedType: SwiftType) throws -> UpcallConversionStep { + let wrappedConversion = try translateParameter(parameterName: name, type: wrappedType) + return .toJavaOptional(.map(.placeholder, body: wrappedConversion)) + } + + private func translateResult(_ result: SwiftResult, methodName: String) throws -> UpcallConversionStep { + try self.translateResult(type: result.type, methodName: methodName) + } + + private func translateResult( + type: SwiftType, + methodName: String, + allowNilForObjects: Bool = false + ) throws -> UpcallConversionStep { + switch type { + case .nominal(let nominalType): + if let knownType = nominalType.nominalTypeDecl.knownTypeKind { + switch knownType { + case .optional: + guard let genericArgs = nominalType.genericArguments, genericArgs.count == 1 else { + throw JavaTranslationError.unsupportedSwiftType(type) + } + return try self.translateOptionalResult( + wrappedType: genericArgs[0], + methodName: methodName + ) + + default: + guard knownType.isDirectlyTranslatedToWrapJava else { + throw JavaTranslationError.unsupportedSwiftType(type) + } + return .placeholder + } + } + + let inner: UpcallConversionStep = !allowNilForObjects ? + .unwrapOptional(.placeholder, message: "Upcall to \(methodName) unexpectedly returned nil") + : .placeholder + + // We assume this is then a JExtracted Swift class + return .toSwiftClass( + inner, + name: "result$", + nominalType: nominalType + ) + + case .tuple([]): // void + return .placeholder + + case .optional(let wrappedType): + return try self.translateOptionalResult(wrappedType: wrappedType, methodName: methodName) + + case .genericParameter, .function, .metatype, .tuple, .existential, .opaque, .composite, .array: + throw JavaTranslationError.unsupportedSwiftType(type) + } + } + + private func translateOptionalResult(wrappedType: SwiftType, methodName: String) throws -> UpcallConversionStep { + // The `fromJavaOptional` will handle the nullability + let wrappedConversion = try translateResult( + type: wrappedType, + methodName: methodName, + allowNilForObjects: true + ) + return .map(.fromJavaOptional(.placeholder), body: wrappedConversion) + } + } +} + + /// Describes how to convert values from and to wrap-java types + enum UpcallConversionStep { + case placeholder + + case constant(String) + + indirect case toJavaWrapper( + UpcallConversionStep, + name: String, + nominalType: SwiftNominalType + ) + + indirect case toSwiftClass( + UpcallConversionStep, + name: String, + nominalType: SwiftNominalType + ) + + indirect case unwrapOptional( + UpcallConversionStep, + message: String + ) + + indirect case toJavaOptional(UpcallConversionStep) + + indirect case fromJavaOptional(UpcallConversionStep) + + indirect case map(UpcallConversionStep, body: UpcallConversionStep) + + /// Returns the conversion string applied to the placeholder. + func render(_ printer: inout CodePrinter, _ placeholder: String) -> String { + switch self { + case .placeholder: + return placeholder + + case .constant(let constant): + return constant + + case .toJavaWrapper(let inner, let name, let nominalType): + let inner = inner.render(&printer, placeholder) + printer.print( + """ + let \(name)Class = try! JavaClass<\(nominalType.nominalTypeDecl.generatedJavaClassMacroName)>(environment: JavaVirtualMachine.shared().environment()) + let \(name)Pointer = UnsafeMutablePointer<\(nominalType.nominalTypeDecl.qualifiedName)>.allocate(capacity: 1) + \(name)Pointer.initialize(to: \(inner)) + """ + ) + + return "\(name)Class.wrapMemoryAddressUnsafe(Int64(Int(bitPattern: \(name)Pointer)))" + + case .toSwiftClass(let inner, let name, let nominalType): + let inner = inner.render(&printer, placeholder) + + // The wrap-java methods will return null + printer.print( + """ + let \(name)MemoryAddress$ = \(inner).as(JavaJNISwiftInstance.self)!.memoryAddress() + let \(name)Pointer = UnsafeMutablePointer<\(nominalType.nominalTypeDecl.qualifiedName)>(bitPattern: Int(\(name)MemoryAddress$))! + """ + ) + + return "\(name)Pointer.pointee" + + case .unwrapOptional(let inner, let message): + let inner = inner.render(&printer, placeholder) + + printer.print( + """ + guard let unwrapped$ = \(inner) else { + fatalError("\(message)") + } + """ + ) + + return "unwrapped$" + + case .toJavaOptional(let inner): + let inner = inner.render(&printer, placeholder) + return "\(inner).toJavaOptional()" + + case .fromJavaOptional(let inner): + let inner = inner.render(&printer, placeholder) + return "Optional(javaOptional: \(inner))" + + case .map(let inner, let body): + let inner = inner.render(&printer, placeholder) + var printer = CodePrinter() + printer.printBraceBlock("\(inner).map") { printer in + let body = body.render(&printer, "$0") + printer.print("return \(body)") + } + return printer.finalize() + } + } +} diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift index 1e18cbe8f..c492d439d 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift @@ -12,7 +12,9 @@ // //===----------------------------------------------------------------------===// +import Foundation import JavaTypes +import OrderedCollections // MARK: Defaults @@ -40,6 +42,8 @@ extension JNISwift2JavaGenerator { package func writeExportedJavaSources(_ printer: inout CodePrinter) throws { let importedTypes = analysis.importedTypes.sorted(by: { (lhs, rhs) in lhs.key < rhs.key }) + var exportedFileNames: OrderedSet = [] + // Each parent type goes into its own file // any nested types are printed inside the body as `static class` for (_, ty) in importedTypes.filter({ _, type in type.parent == nil }) { @@ -52,6 +56,7 @@ extension JNISwift2JavaGenerator { javaPackagePath: javaPackagePath, filename: filename ) { + exportedFileNames.append(outputFile.path(percentEncoded: false)) logger.info("[swift-java] Generated: \(ty.swiftNominal.name.bold).java (at \(outputFile))") } } @@ -65,10 +70,24 @@ extension JNISwift2JavaGenerator { javaPackagePath: javaPackagePath, filename: filename ) { + exportedFileNames.append(outputFile.path(percentEncoded: false)) logger.info("[swift-java] Generated: \(self.swiftModuleName).java (at \(outputFile))") } + + // Write java sources list file + if let generatedJavaSourcesListFileOutput = config.generatedJavaSourcesListFileOutput, !exportedFileNames.isEmpty { + let outputPath = URL(fileURLWithPath: javaOutputDirectory).appending(path: generatedJavaSourcesListFileOutput) + try exportedFileNames.joined(separator: "\n").write( + to: outputPath, + atomically: true, + encoding: .utf8 + ) + logger.info("Generated file at \(outputPath)") + } } + + private func printModule(_ printer: inout CodePrinter) { printHeader(&printer) printPackage(&printer) @@ -113,20 +132,29 @@ extension JNISwift2JavaGenerator { } private func printProtocol(_ printer: inout CodePrinter, _ decl: ImportedNominalType) { - let extends = ["JNISwiftInstance"] - printer.printBraceBlock("public interface \(decl.swiftNominal.name) extends \(extends.joined(separator: ", "))") { printer in + var extends = [String]() + + // If we cannot generate Swift wrappers + // that allows the user to implement the wrapped interface in Java + // then we require only JExtracted types can conform to this. + if !self.interfaceProtocolWrappers.keys.contains(decl) { + extends.append("JNISwiftInstance") + } + let extendsString = extends.isEmpty ? "" : " extends \(extends.joined(separator: ", "))" + + printer.printBraceBlock("public interface \(decl.swiftNominal.name)\(extendsString)") { printer in for initializer in decl.initializers { - printFunctionDowncallMethods(&printer, initializer, signaturesOnly: true) + printFunctionDowncallMethods(&printer, initializer, skipMethodBody: true, skipArenas: true) printer.println() } for method in decl.methods { - printFunctionDowncallMethods(&printer, method, signaturesOnly: true) + printFunctionDowncallMethods(&printer, method, skipMethodBody: true, skipArenas: true) printer.println() } for variable in decl.variables { - printFunctionDowncallMethods(&printer, variable, signaturesOnly: true) + printFunctionDowncallMethods(&printer, variable, skipMethodBody: true, skipArenas: true) printer.println() } } @@ -184,6 +212,10 @@ extension JNISwift2JavaGenerator { public static \(decl.swiftNominal.name) wrapMemoryAddressUnsafe(long selfPointer, SwiftArena swiftArena) { return new \(decl.swiftNominal.name)(selfPointer, swiftArena); } + + public static \(decl.swiftNominal.name) wrapMemoryAddressUnsafe(long selfPointer) { + return new \(decl.swiftNominal.name)(selfPointer, SwiftMemoryManagement.GLOBAL_SWIFT_JAVA_ARENA); + } """ ) @@ -359,10 +391,10 @@ extension JNISwift2JavaGenerator { ["\(conversion.native.javaType) \(value.parameter.name)"] } - printer.print("record $NativeParameters(\(nativeParameters.joined(separator: ", "))) {}") + printer.print("record _NativeParameters(\(nativeParameters.joined(separator: ", "))) {}") } - self.printJavaBindingWrapperMethod(&printer, translatedCase.getAsCaseFunction, signaturesOnly: false) + self.printJavaBindingWrapperMethod(&printer, translatedCase.getAsCaseFunction, skipMethodBody: false, skipArenas: false) printer.println() } } @@ -370,7 +402,8 @@ extension JNISwift2JavaGenerator { private func printFunctionDowncallMethods( _ printer: inout CodePrinter, _ decl: ImportedFunc, - signaturesOnly: Bool = false + skipMethodBody: Bool = false, + skipArenas: Bool = false ) { guard translatedDecl(for: decl) != nil else { // Failed to translate. Skip. @@ -381,7 +414,7 @@ extension JNISwift2JavaGenerator { printJavaBindingWrapperHelperClass(&printer, decl) - printJavaBindingWrapperMethod(&printer, decl, signaturesOnly: signaturesOnly) + printJavaBindingWrapperMethod(&printer, decl, skipMethodBody: skipMethodBody, skipArenas: skipArenas) } /// Print the helper type container for a user-facing Java API. @@ -427,19 +460,21 @@ extension JNISwift2JavaGenerator { private func printJavaBindingWrapperMethod( _ printer: inout CodePrinter, _ decl: ImportedFunc, - signaturesOnly: Bool + skipMethodBody: Bool, + skipArenas: Bool ) { guard let translatedDecl = translatedDecl(for: decl) else { fatalError("Decl was not translated, \(decl)") } - printJavaBindingWrapperMethod(&printer, translatedDecl, importedFunc: decl, signaturesOnly: signaturesOnly) + printJavaBindingWrapperMethod(&printer, translatedDecl, importedFunc: decl, skipMethodBody: skipMethodBody, skipArenas: skipArenas) } private func printJavaBindingWrapperMethod( _ printer: inout CodePrinter, _ translatedDecl: TranslatedFunctionDecl, importedFunc: ImportedFunc? = nil, - signaturesOnly: Bool + skipMethodBody: Bool, + skipArenas: Bool ) { var modifiers = ["public"] if translatedDecl.isStatic { @@ -494,14 +529,14 @@ extension JNISwift2JavaGenerator { printer.println() } - if translatedSignature.requiresSwiftArena { + if translatedSignature.requiresSwiftArena, !skipArenas { parameters.append("SwiftArena swiftArena$") } if let importedFunc { printDeclDocumentation(&printer, importedFunc) } let signature = "\(annotationsStr)\(modifiers.joined(separator: " ")) \(resultType) \(translatedDecl.name)(\(parameters.joined(separator: ", ")))\(throwsClause)" - if signaturesOnly { + if skipMethodBody { printer.print("\(signature);") } else { printer.printBraceBlock(signature) { printer in diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift index 6a2771c30..553b3e10b 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift @@ -16,6 +16,17 @@ import JavaTypes import SwiftJavaConfigurationShared extension JNISwift2JavaGenerator { + var javaTranslator: JavaTranslation { + JavaTranslation( + config: config, + swiftModuleName: swiftModuleName, + javaPackage: self.javaPackage, + javaClassLookupTable: self.javaClassLookupTable, + knownTypes: SwiftKnownTypes(symbolTable: lookupContext.symbolTable), + protocolWrappers: self.interfaceProtocolWrappers + ) + } + func translatedDecl( for decl: ImportedFunc ) -> TranslatedFunctionDecl? { @@ -25,14 +36,7 @@ extension JNISwift2JavaGenerator { let translated: TranslatedFunctionDecl? do { - let translation = JavaTranslation( - config: config, - swiftModuleName: swiftModuleName, - javaPackage: self.javaPackage, - javaClassLookupTable: self.javaClassLookupTable, - knownTypes: SwiftKnownTypes(symbolTable: lookupContext.symbolTable) - ) - translated = try translation.translate(decl) + translated = try self.javaTranslator.translate(decl) } catch { self.logger.debug("Failed to translate: '\(decl.swiftDecl.qualifiedNameForDebug)'; \(error)") translated = nil @@ -56,7 +60,8 @@ extension JNISwift2JavaGenerator { swiftModuleName: swiftModuleName, javaPackage: self.javaPackage, javaClassLookupTable: self.javaClassLookupTable, - knownTypes: SwiftKnownTypes(symbolTable: lookupContext.symbolTable) + knownTypes: SwiftKnownTypes(symbolTable: lookupContext.symbolTable), + protocolWrappers: self.interfaceProtocolWrappers ) translated = try translation.translate(enumCase: decl) } catch { @@ -74,13 +79,15 @@ extension JNISwift2JavaGenerator { let javaPackage: String let javaClassLookupTable: JavaClassLookupTable var knownTypes: SwiftKnownTypes + let protocolWrappers: [ImportedNominalType: JavaInterfaceSwiftWrapper] func translate(enumCase: ImportedEnumCase) throws -> TranslatedEnumCase { let nativeTranslation = NativeJavaTranslation( config: self.config, javaPackage: self.javaPackage, javaClassLookupTable: self.javaClassLookupTable, - knownTypes: self.knownTypes + knownTypes: self.knownTypes, + protocolWrappers: self.protocolWrappers ) let methodName = "" // TODO: Used for closures, replace with better name? @@ -105,7 +112,7 @@ extension JNISwift2JavaGenerator { let caseName = enumCase.name.firstCharacterUppercased let enumName = enumCase.enumType.nominalTypeDecl.name - let nativeParametersType = JavaType.class(package: nil, name: "\(caseName).$NativeParameters") + 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: [ @@ -168,7 +175,8 @@ extension JNISwift2JavaGenerator { config: self.config, javaPackage: self.javaPackage, javaClassLookupTable: self.javaClassLookupTable, - knownTypes: self.knownTypes + knownTypes: self.knownTypes, + protocolWrappers: self.protocolWrappers ) // Types with no parent will be outputted inside a "module" class. @@ -405,8 +413,8 @@ extension JNISwift2JavaGenerator { } } - if nominalType.isJavaKitWrapper { - guard let javaType = nominalTypeName.parseJavaClassFromJavaKitName(in: self.javaClassLookupTable) else { + if nominalType.isSwiftJavaWrapper { + guard let javaType = nominalTypeName.parseJavaClassFromSwiftJavaName(in: self.javaClassLookupTable) else { throw JavaTranslationError.wrappedJavaClassTranslationNotProvided(swiftType) } @@ -456,7 +464,7 @@ extension JNISwift2JavaGenerator { return try translateProtocolParameter( protocolType: proto, parameterName: parameterName, - javaGenericName: "$T\(parameterPosition)" + javaGenericName: "_T\(parameterPosition)" ) case .genericParameter(let generic): @@ -585,14 +593,14 @@ extension JNISwift2JavaGenerator { } } - // We assume this is a JExtract class. + // We just pass down the jobject return TranslatedParameter( parameter: JavaParameter( name: parameterName, type: .generic(name: javaGenericName, extends: javaProtocolTypes), annotations: [] ), - conversion: .commaSeparated([.valueMemoryAddress(.placeholder), .typeMetadataAddress(.placeholder)]) + conversion: .placeholder ) } @@ -628,8 +636,8 @@ extension JNISwift2JavaGenerator { ) } - if nominalType.isJavaKitWrapper { - guard let javaType = nominalTypeName.parseJavaClassFromJavaKitName(in: self.javaClassLookupTable) else { + if nominalType.isSwiftJavaWrapper { + guard let javaType = nominalTypeName.parseJavaClassFromSwiftJavaName(in: self.javaClassLookupTable) else { throw JavaTranslationError.wrappedJavaClassTranslationNotProvided(swiftType) } @@ -703,7 +711,7 @@ extension JNISwift2JavaGenerator { } } - if nominalType.isJavaKitWrapper { + if nominalType.isSwiftJavaWrapper { throw JavaTranslationError.unsupportedSwiftType(swiftType) } @@ -789,7 +797,7 @@ extension JNISwift2JavaGenerator { } } - guard !nominalType.isJavaKitWrapper else { + guard !nominalType.isSwiftJavaWrapper else { throw JavaTranslationError.unsupportedSwiftType(swiftType) } @@ -836,7 +844,7 @@ extension JNISwift2JavaGenerator { ) } - guard !nominalType.isJavaKitWrapper else { + guard !nominalType.isSwiftJavaWrapper else { throw JavaTranslationError.unsupportedSwiftType(elementType) } @@ -885,7 +893,7 @@ extension JNISwift2JavaGenerator { ) } - guard !nominalType.isJavaKitWrapper else { + guard !nominalType.isSwiftJavaWrapper else { throw JavaTranslationError.unsupportedSwiftType(elementType) } @@ -965,7 +973,7 @@ extension JNISwift2JavaGenerator { /// Function signature of the native function that will be implemented by Swift let nativeFunctionSignature: NativeFunctionSignature - + /// Annotations to include on the Java function declaration var annotations: [JavaAnnotation] { self.translatedFunctionSignature.annotations @@ -1332,5 +1340,8 @@ extension JNISwift2JavaGenerator { /// The user has not supplied a mapping from `SwiftType` to /// a java class. case wrappedJavaClassTranslationNotProvided(SwiftType) + + // FIXME: Remove once we support protocol variables + case protocolVariablesNotSupported } } diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift index 24e469067..b744a33b4 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift @@ -22,6 +22,7 @@ extension JNISwift2JavaGenerator { let javaPackage: String let javaClassLookupTable: JavaClassLookupTable var knownTypes: SwiftKnownTypes + let protocolWrappers: [ImportedNominalType: JavaInterfaceSwiftWrapper] /// Translates a Swift function into the native JNI method signature. func translate( @@ -113,8 +114,8 @@ extension JNISwift2JavaGenerator { } } - if nominalType.isJavaKitWrapper { - guard let javaType = nominalTypeName.parseJavaClassFromJavaKitName(in: self.javaClassLookupTable) else { + if nominalType.isSwiftJavaWrapper { + guard let javaType = nominalTypeName.parseJavaClassFromSwiftJavaName(in: self.javaClassLookupTable) else { throw JavaTranslationError.wrappedJavaClassTranslationNotProvided(type) } @@ -122,7 +123,7 @@ extension JNISwift2JavaGenerator { parameters: [ JavaParameter(name: parameterName, type: javaType) ], - conversion: .initializeJavaKitWrapper( + conversion: .initializeSwiftJavaWrapper( .unwrapOptional( .placeholder, name: parameterName, @@ -181,14 +182,18 @@ extension JNISwift2JavaGenerator { case .opaque(let proto), .existential(let proto): return try translateProtocolParameter( protocolType: proto, - parameterName: parameterName + methodName: methodName, + parameterName: parameterName, + parentName: parentName ) case .genericParameter: if let concreteTy = type.typeIn(genericParameters: genericParameters, genericRequirements: genericRequirements) { return try translateProtocolParameter( protocolType: concreteTy, - parameterName: parameterName + methodName: methodName, + parameterName: parameterName, + parentName: parentName ) } @@ -207,22 +212,23 @@ extension JNISwift2JavaGenerator { func translateProtocolParameter( protocolType: SwiftType, - parameterName: String + methodName: String, + parameterName: String, + parentName: String? ) throws -> NativeParameter { switch protocolType { case .nominal(let nominalType): - let protocolName = nominalType.nominalTypeDecl.qualifiedName - return try translateProtocolParameter(protocolNames: [protocolName], parameterName: parameterName) + return try translateProtocolParameter(protocolTypes: [nominalType], methodName: methodName, parameterName: parameterName, parentName: parentName) case .composite(let types): - let protocolNames = try types.map { - guard let nominalTypeName = $0.asNominalType?.nominalTypeDecl.qualifiedName else { + let protocolTypes = try types.map { + guard let nominalTypeName = $0.asNominalType else { throw JavaTranslationError.unsupportedSwiftType($0) } return nominalTypeName } - return try translateProtocolParameter(protocolNames: protocolNames, parameterName: parameterName) + return try translateProtocolParameter(protocolTypes: protocolTypes, methodName: methodName, parameterName: parameterName, parentName: parentName) default: throw JavaTranslationError.unsupportedSwiftType(protocolType) @@ -230,18 +236,30 @@ extension JNISwift2JavaGenerator { } private func translateProtocolParameter( - protocolNames: [String], - parameterName: String + protocolTypes: [SwiftNominalType], + methodName: String, + parameterName: String, + parentName: String? ) throws -> NativeParameter { + // We allow Java implementations if we are able to generate the needed + // Swift wrappers for all the protocol types. + let allowsJavaImplementations = protocolTypes.allSatisfy { protocolType in + self.protocolWrappers.contains(where: { $0.value.protocolType == protocolType }) + } + return NativeParameter( parameters: [ - JavaParameter(name: parameterName, type: .long), - JavaParameter(name: "\(parameterName)_typeMetadataAddress", type: .long) + JavaParameter(name: parameterName, type: .javaLangObject) ], - conversion: .extractSwiftProtocolValue( + conversion: .interfaceToSwiftObject( .placeholder, - typeMetadataVariableName: .combinedName(component: "typeMetadataAddress"), - protocolNames: protocolNames + swiftWrapperClassName: JNISwift2JavaGenerator.protocolParameterWrapperClassName( + methodName: methodName, + parameterName: parameterName, + parentName: parentName + ), + protocolTypes: protocolTypes, + allowsJavaImplementations: allowsJavaImplementations ) ) } @@ -276,8 +294,8 @@ extension JNISwift2JavaGenerator { ) } - if nominalType.isJavaKitWrapper { - guard let javaType = nominalTypeName.parseJavaClassFromJavaKitName(in: self.javaClassLookupTable) else { + if nominalType.isSwiftJavaWrapper { + guard let javaType = nominalTypeName.parseJavaClassFromSwiftJavaName(in: self.javaClassLookupTable) else { throw JavaTranslationError.wrappedJavaClassTranslationNotProvided(swiftType) } @@ -285,7 +303,7 @@ extension JNISwift2JavaGenerator { parameters: [ JavaParameter(name: parameterName, type: javaType) ], - conversion: .optionalMap(.initializeJavaKitWrapper(.placeholder, wrapperName: nominalTypeName)) + conversion: .optionalMap(.initializeSwiftJavaWrapper(.placeholder, wrapperName: nominalTypeName)) ) } @@ -359,7 +377,7 @@ extension JNISwift2JavaGenerator { } } - guard !nominalType.isJavaKitWrapper else { + guard !nominalType.isSwiftJavaWrapper else { // TODO: Should be the same as above throw JavaTranslationError.unsupportedSwiftType(swiftType) } @@ -479,7 +497,7 @@ extension JNISwift2JavaGenerator { } } - if nominalType.isJavaKitWrapper { + if nominalType.isSwiftJavaWrapper { throw JavaTranslationError.unsupportedSwiftType(swiftResult.type) } @@ -526,7 +544,7 @@ extension JNISwift2JavaGenerator { ) } - guard !nominalType.isJavaKitWrapper else { + guard !nominalType.isSwiftJavaWrapper else { throw JavaTranslationError.unsupportedSwiftType(.array(elementType)) } @@ -574,7 +592,7 @@ extension JNISwift2JavaGenerator { ) } - guard !nominalType.isJavaKitWrapper else { + guard !nominalType.isSwiftJavaWrapper else { throw JavaTranslationError.unsupportedSwiftType(.array(elementType)) } @@ -645,6 +663,13 @@ extension JNISwift2JavaGenerator { /// `SwiftType(from: value, in: environment)` indirect case initFromJNI(NativeSwiftConversionStep, swiftType: SwiftType) + indirect case interfaceToSwiftObject( + NativeSwiftConversionStep, + swiftWrapperClassName: String, + protocolTypes: [SwiftNominalType], + allowsJavaImplementations: Bool + ) + indirect case extractSwiftProtocolValue( NativeSwiftConversionStep, typeMetadataVariableName: NativeSwiftConversionStep, @@ -668,7 +693,7 @@ extension JNISwift2JavaGenerator { indirect case closureLowering(parameters: [NativeParameter], result: NativeResult) - indirect case initializeJavaKitWrapper(NativeSwiftConversionStep, wrapperName: String) + indirect case initializeSwiftJavaWrapper(NativeSwiftConversionStep, wrapperName: String) indirect case optionalLowering(NativeSwiftConversionStep, discriminatorName: String, valueName: String) @@ -723,6 +748,60 @@ extension JNISwift2JavaGenerator { let inner = inner.render(&printer, placeholder) return "\(swiftType)(fromJNI: \(inner), in: environment)" + case .interfaceToSwiftObject( + let inner, + let swiftWrapperClassName, + let protocolTypes, + let allowsJavaImplementations + ): + let protocolNames = protocolTypes.map { $0.nominalTypeDecl.qualifiedName } + + let inner = inner.render(&printer, placeholder) + let variableName = "\(inner)swiftObject$" + let compositeProtocolName = "(\(protocolNames.joined(separator: " & ")))" + printer.print("let \(variableName): \(compositeProtocolName)") + + func printStandardJExtractBlock(_ printer: inout CodePrinter) { + let pointerVariableName = "\(inner)pointer$" + let typeMetadataVariableName = "\(inner)typeMetadata$" + printer.print( + """ + let \(pointerVariableName) = environment.interface.CallLongMethodA(environment, \(inner), _JNIMethodIDCache.JNISwiftInstance.memoryAddress, []) + let \(typeMetadataVariableName) = environment.interface.CallLongMethodA(environment, \(inner), _JNIMethodIDCache.JNISwiftInstance.typeMetadataAddress, []) + """ + ) + let existentialName = NativeSwiftConversionStep.extractSwiftProtocolValue( + .constant(pointerVariableName), + typeMetadataVariableName: .constant(typeMetadataVariableName), + protocolNames: protocolNames + ).render(&printer, placeholder) + + printer.print("\(variableName) = \(existentialName)") + } + + // If this protocol type supports being implemented by the user + // then we will check whether it is a JNI SwiftInstance type + // or if its a custom class implementing the interface. + if allowsJavaImplementations { + printer.printBraceBlock( + "if environment.interface.IsInstanceOf(environment, \(inner), _JNIMethodIDCache.JNISwiftInstance.class) != 0" + ) { printer in + printStandardJExtractBlock(&printer) + } + printer.printBraceBlock("else") { printer in + let arguments = protocolTypes.map { protocolType in + let nominalTypeDecl = protocolType.nominalTypeDecl + return "\(nominalTypeDecl.javaInterfaceVariableName): \(nominalTypeDecl.javaInterfaceName)(javaThis: \(inner)!, environment: environment)" + } + printer.print("\(variableName) = \(swiftWrapperClassName)(\(arguments.joined(separator: ", ")))") + } + } else { + printStandardJExtractBlock(&printer) + } + + + return variableName + case .extractSwiftProtocolValue(let inner, let typeMetadataVariableName, let protocolNames): let inner = inner.render(&printer, placeholder) let typeMetadataVariableName = typeMetadataVariableName.render(&printer, placeholder) @@ -839,7 +918,7 @@ extension JNISwift2JavaGenerator { return printer.finalize() - case .initializeJavaKitWrapper(let inner, let wrapperName): + case .initializeSwiftJavaWrapper(let inner, let wrapperName): let inner = inner.render(&printer, placeholder) return "\(wrapperName)(javaThis: \(inner), environment: environment)" diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift index 6628be333..85fdc6404 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift @@ -116,6 +116,75 @@ extension JNISwift2JavaGenerator { } } + /// Prints the extension needed to make allow upcalls from Swift to Java for protocols + private func printSwiftInterfaceWrapper( + _ printer: inout CodePrinter, + _ translatedWrapper: JavaInterfaceSwiftWrapper + ) throws { + printer.printBraceBlock("protocol \(translatedWrapper.wrapperName): \(translatedWrapper.swiftName)") { printer in + printer.print("var \(translatedWrapper.javaInterfaceVariableName): \(translatedWrapper.javaInterfaceName) { get }") + } + printer.println() + printer.printBraceBlock("extension \(translatedWrapper.wrapperName)") { printer in + for function in translatedWrapper.functions { + printInterfaceWrapperFunctionImpl(&printer, function, inside: translatedWrapper) + printer.println() + } + + // FIXME: Add support for protocol variables https://github.com/swiftlang/swift-java/issues/457 +// for variable in translatedWrapper.variables { +// printerInterfaceWrapperVariable(&printer, variable, inside: translatedWrapper) +// printer.println() +// } + } + } + + private func printInterfaceWrapperFunctionImpl( + _ printer: inout CodePrinter, + _ function: JavaInterfaceSwiftWrapper.Function, + inside wrapper: JavaInterfaceSwiftWrapper + ) { + printer.printBraceBlock(function.swiftDecl.signatureString) { printer in + let upcallArguments = zip( + function.originalFunctionSignature.parameters, + function.parameterConversions + ).map { param, conversion in + // Wrap-java does not extract parameter names, so no labels + conversion.render(&printer, param.parameterName!) + } + + let javaUpcall = "\(wrapper.javaInterfaceVariableName).\(function.swiftFunctionName)(\(upcallArguments.joined(separator: ", ")))" + + let resultType = function.originalFunctionSignature.result.type + let result = function.resultConversion.render(&printer, javaUpcall) + if resultType.isVoid { + printer.print(result) + } else { + printer.print("return \(result)") + } + } + } + + private func printerInterfaceWrapperVariable( + _ printer: inout CodePrinter, + _ variable: JavaInterfaceSwiftWrapper.Variable, + inside wrapper: JavaInterfaceSwiftWrapper + ) { + // FIXME: Add support for variables. This won't get printed yet + // so we no need to worry about fatalErrors. + printer.printBraceBlock(variable.swiftDecl.signatureString) { printer in + printer.printBraceBlock("get") { printer in + printer.print("fatalError()") + } + + if let setter = variable.setter { + printer.printBraceBlock("set") { printer in + printer.print("fatalError()") + } + } + } + } + private func printGlobalSwiftThunkSources(_ printer: inout CodePrinter) throws { printHeader(&printer) @@ -154,7 +223,7 @@ extension JNISwift2JavaGenerator { case .actor, .class, .enum, .struct: printConcreteTypeThunks(&printer, type) case .protocol: - printProtocolThunks(&printer, type) + try printProtocolThunks(&printer, type) } } @@ -184,13 +253,18 @@ extension JNISwift2JavaGenerator { printer.println() } + printTypeMetadataAddressThunk(&printer, type) printer.println() printDestroyFunctionThunk(&printer, type) } - private func printProtocolThunks(_ printer: inout CodePrinter, _ type: ImportedNominalType) { - let protocolName = type.swiftNominal.name + private func printProtocolThunks(_ printer: inout CodePrinter, _ type: ImportedNominalType) throws { + guard let protocolWrapper = self.interfaceProtocolWrappers[type] else { + return + } + + try printSwiftInterfaceWrapper(&printer, protocolWrapper) } @@ -232,11 +306,19 @@ extension JNISwift2JavaGenerator { } private func renderEnumCaseCacheInit(_ enumCase: TranslatedEnumCase) -> String { - let nativeParametersClassName = "\(javaPackagePath)/\(enumCase.enumName)$\(enumCase.name)$$NativeParameters" + let nativeParametersClassName = "\(enumCase.enumName)$\(enumCase.name)$_NativeParameters" let methodSignature = MethodSignature(resultType: .void, parameterTypes: enumCase.parameterConversions.map(\.native.javaType)) - let methods = #"[.init(name: "", signature: "\#(methodSignature.mangledName)")]"# - return #"_JNIMethodIDCache(className: "\#(nativeParametersClassName)", methods: \#(methods))"# + return renderJNICacheInit(className: nativeParametersClassName, methods: [("", methodSignature)]) + } + + private func renderJNICacheInit(className: String, methods: [(String, MethodSignature)]) -> String { + let fullClassName = "\(javaPackagePath)/\(className)" + let methods = methods.map { name, signature in + #".init(name: "\#(name)", signature: "\#(signature.mangledName)")"# + }.joined(separator: ",\n") + + return #"_JNIMethodIDCache(className: "\#(fullClassName)", methods: [\#(methods)])"# } private func printEnumGetAsCaseThunk( @@ -287,6 +369,8 @@ extension JNISwift2JavaGenerator { return } + printSwiftFunctionHelperClasses(&printer, decl) + printCDecl( &printer, translatedDecl @@ -295,6 +379,96 @@ extension JNISwift2JavaGenerator { } } + + private func printSwiftFunctionHelperClasses( + _ printer: inout CodePrinter, + _ decl: ImportedFunc + ) { + let protocolParameters = decl.functionSignature.parameters.compactMap { parameter in + if let concreteType = parameter.type.typeIn( + genericParameters: decl.functionSignature.genericParameters, + genericRequirements: decl.functionSignature.genericRequirements + ) { + return (parameter, concreteType) + } + + switch parameter.type { + case .opaque(let protocolType), + .existential(let protocolType): + return (parameter, protocolType) + + default: + return nil + } + }.map { parameter, protocolType in + // We flatten any composite types + switch protocolType { + case .composite(let protocols): + return (parameter, protocols) + + default: + return (parameter, [protocolType]) + } + } + + // For each parameter that is a generic or a protocol, + // we generate a Swift class that conforms to all of those. + for (parameter, protocolTypes) in protocolParameters { + let protocolWrappers: [JavaInterfaceSwiftWrapper] = protocolTypes.compactMap { protocolType in + guard let importedType = self.asImportedNominalTypeDecl(protocolType), + let wrapper = self.interfaceProtocolWrappers[importedType] + else { + return nil + } + return wrapper + } + + // Make sure we can generate wrappers for all the protocols + // that the parameter requires + guard protocolWrappers.count == protocolTypes.count else { + // We cannot extract a wrapper for this class + // so it must only be passed in by JExtract instances + continue + } + + guard let parameterName = parameter.parameterName else { + // TODO: Throw + fatalError() + } + let swiftClassName = JNISwift2JavaGenerator.protocolParameterWrapperClassName( + methodName: decl.name, + parameterName: parameterName, + parentName: decl.parentType?.asNominalType?.nominalTypeDecl.qualifiedName ?? swiftModuleName + ) + let implementingProtocols = protocolWrappers.map(\.wrapperName).joined(separator: ", ") + + printer.printBraceBlock("final class \(swiftClassName): \(implementingProtocols)") { printer in + let variables: [(String, String)] = protocolWrappers.map { wrapper in + return (wrapper.javaInterfaceVariableName, wrapper.javaInterfaceName) + } + for (name, type) in variables { + printer.print("let \(name): \(type)") + } + printer.println() + let initializerParameters = variables.map { "\($0): \($1)" }.joined(separator: ", ") + + printer.printBraceBlock("init(\(initializerParameters))") { printer in + for (name, _) in variables { + printer.print("self.\(name) = \(name)") + } + } + } + } + } + + private func asImportedNominalTypeDecl(_ type: SwiftType) -> ImportedNominalType? { + self.analysis.importedTypes.first(where: ( { name, nominalType in + nominalType.swiftType == type + })).map { + $0.value + } + } + private func printFunctionDowncall( _ printer: inout CodePrinter, _ decl: ImportedFunc @@ -562,4 +736,44 @@ extension JNISwift2JavaGenerator { ) return newSelfParamName } + + static func protocolParameterWrapperClassName( + methodName: String, + parameterName: String, + parentName: String? + ) -> String { + let parent = if let parentName { + "\(parentName)_" + } else { + "" + } + return "_\(parent)\(methodName)_\(parameterName)_Wrapper" + } +} + +extension SwiftNominalTypeDeclaration { + private var safeProtocolName: String { + self.qualifiedName.replacingOccurrences(of: ".", with: "_") + } + + /// The name of the corresponding `@JavaInterface` of this type. + var javaInterfaceName: String { + "Java\(safeProtocolName)" + } + + var javaInterfaceSwiftProtocolWrapperName: String { + "SwiftJava\(safeProtocolName)Wrapper" + } + + var javaInterfaceVariableName: String { + "_\(javaInterfaceName.firstCharacterLowercased)Interface" + } + + var generatedJavaClassMacroName: String { + if let parent { + return "\(parent.generatedJavaClassMacroName).Java\(self.name)" + } + + return "Java\(self.name)" + } } diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift index 3b84cfb97..692b66b69 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift @@ -41,6 +41,7 @@ package class JNISwift2JavaGenerator: Swift2JavaGenerator { /// Cached Java translation result. 'nil' indicates failed translation. var translatedDecls: [ImportedFunc: TranslatedFunctionDecl] = [:] var translatedEnumCases: [ImportedEnumCase: TranslatedEnumCase] = [:] + var interfaceProtocolWrappers: [ImportedNominalType: JavaInterfaceSwiftWrapper] = [:] /// 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. @@ -78,6 +79,13 @@ package class JNISwift2JavaGenerator: Swift2JavaGenerator { } else { self.expectedOutputSwiftFiles = [] } + + if translator.config.enableJavaCallbacks ?? false { + // We translate all the protocol wrappers + // as we need them to know what protocols we can allow the user to implement themselves + // in Java. + self.interfaceProtocolWrappers = self.generateInterfaceWrappers(Array(self.analysis.importedTypes.values)) + } } func generate() throws { diff --git a/Sources/JExtractSwiftLib/JavaTypes/JavaType+JDK.swift b/Sources/JExtractSwiftLib/JavaTypes/JavaType+JDK.swift index 511bf8de6..e1aabd7fd 100644 --- a/Sources/JExtractSwiftLib/JavaTypes/JavaType+JDK.swift +++ b/Sources/JExtractSwiftLib/JavaTypes/JavaType+JDK.swift @@ -40,6 +40,12 @@ extension JavaType { .class(package: "java.lang", name: "Throwable") } + /// The description of the type java.lang.Object. + static var javaLangObject: JavaType { + .class(package: "java.lang", name: "Object") + } + + /// The description of the type java.util.concurrent.CompletableFuture static func completableFuture(_ T: JavaType) -> JavaType { .class(package: "java.util.concurrent", name: "CompletableFuture", typeParameters: [T.boxedType]) diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownTypeDecls.swift b/Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownTypeDecls.swift index 4a0cb9e8a..363663217 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownTypeDecls.swift +++ b/Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownTypeDecls.swift @@ -65,4 +65,18 @@ enum SwiftKnownTypeDeclKind: String, Hashable { return false } } + + /// Indicates whether this known type is translated by `wrap-java` + /// into the same type as `jextract`. + /// + /// This means we do not have to perform any mapping when passing + /// this type between jextract and wrap-java + var isDirectlyTranslatedToWrapJava: Bool { + switch self { + case .bool, .int, .uint, .int8, .uint8, .int16, .uint16, .int32, .uint32, .int64, .uint64, .float, .double, .string, .void: + return true + default: + return false + } + } } diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftType.swift b/Sources/JExtractSwiftLib/SwiftTypes/SwiftType.swift index b2f8d6eac..aeb88bfba 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftType.swift +++ b/Sources/JExtractSwiftLib/SwiftTypes/SwiftType.swift @@ -210,9 +210,13 @@ extension SwiftNominalType: CustomStringConvertible { extension SwiftNominalType { // TODO: Better way to detect Java wrapped classes. - var isJavaKitWrapper: Bool { + var isSwiftJavaWrapper: Bool { nominalTypeDecl.name.hasPrefix("Java") } + + var isProtocol: Bool { + nominalTypeDecl.kind == .protocol + } } extension SwiftType { diff --git a/Sources/JavaStdlib/JavaLangReflect/Constructor+Utilities.swift b/Sources/JavaStdlib/JavaLangReflect/Constructor+Utilities.swift index 8f57ffa51..4042ec766 100644 --- a/Sources/JavaStdlib/JavaLangReflect/Constructor+Utilities.swift +++ b/Sources/JavaStdlib/JavaLangReflect/Constructor+Utilities.swift @@ -13,11 +13,6 @@ //===----------------------------------------------------------------------===// extension Constructor { - /// Whether this is a 'public' constructor. - public var isPublic: Bool { - return (getModifiers() & 1) != 0 - } - /// Whether this is a 'native' constructor. public var isNative: Bool { return (getModifiers() & 256) != 0 diff --git a/Sources/JavaStdlib/JavaLangReflect/HasJavaModifiers.swift b/Sources/JavaStdlib/JavaLangReflect/HasJavaModifiers.swift new file mode 100644 index 000000000..fc10edfea --- /dev/null +++ b/Sources/JavaStdlib/JavaLangReflect/HasJavaModifiers.swift @@ -0,0 +1,47 @@ +//===----------------------------------------------------------------------===// +// +// 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 SwiftJava + +public protocol HasJavaModifiers { + func getModifiers() -> Int32 +} + +extension HasJavaModifiers { + /// Whether the modifiers contain 'public'. + public var isPublic: Bool { + return (getModifiers() & 0x00000001) != 0 + } + + /// Whether the modifiers contain 'private'. + public var isPrivate: Bool { + return (getModifiers() & 0x00000002) != 0 + } + + /// Whether the modifiers contain 'protected'. + public var isProtected: Bool { + return (getModifiers() & 0x00000004) != 0 + } + + /// Whether the modifiers is equivelant to 'package'.. + /// + /// The "default" access level in Java is 'package', it is signified by lack of a different access modifier. + public var isPackage: Bool { + return !isPublic && !isPrivate && !isProtected + } +} + +extension Constructor: HasJavaModifiers {} +extension JavaClass: HasJavaModifiers {} +extension Method: HasJavaModifiers {} diff --git a/Sources/JavaStdlib/JavaLangReflect/Method+Utilities.swift b/Sources/JavaStdlib/JavaLangReflect/Method+Utilities.swift index ecc11b507..00ca3d6bf 100644 --- a/Sources/JavaStdlib/JavaLangReflect/Method+Utilities.swift +++ b/Sources/JavaStdlib/JavaLangReflect/Method+Utilities.swift @@ -13,28 +13,6 @@ //===----------------------------------------------------------------------===// extension Method { - - /// Whether this is a 'public' method. - public var isPublic: Bool { - return (getModifiers() & 0x00000001) != 0 - } - - /// Whether this is a 'private' method. - public var isPrivate: Bool { - return (getModifiers() & 0x00000002) != 0 - } - - /// Whether this is a 'protected' method. - public var isProtected: Bool { - return (getModifiers() & 0x00000004) != 0 - } - - /// Whether this is a 'package' method. - /// - /// The "default" access level in Java is 'package', it is signified by lack of a different access modifier. - public var isPackage: Bool { - return !isPublic && !isPrivate && !isProtected - } /// Whether this is a 'static' method. public var isStatic: Bool { diff --git a/Sources/SwiftJava/JVM/JavaVirtualMachine.swift b/Sources/SwiftJava/JVM/JavaVirtualMachine.swift index 1c7936a34..bb574c8ad 100644 --- a/Sources/SwiftJava/JVM/JavaVirtualMachine.swift +++ b/Sources/SwiftJava/JVM/JavaVirtualMachine.swift @@ -350,4 +350,4 @@ extension JavaVirtualMachine { enum JavaKitError: Error { case classpathEntryNotFound(entry: String, classpath: [String]) } -} \ No newline at end of file +} diff --git a/Sources/SwiftJava/Macros.swift b/Sources/SwiftJava/Macros.swift index eb9c43745..4c0353661 100644 --- a/Sources/SwiftJava/Macros.swift +++ b/Sources/SwiftJava/Macros.swift @@ -141,6 +141,7 @@ public macro JavaStaticField(_ javaFieldName: String? = nil, isFinal: Bool = fal /// returning method signature, and then, convert the result to the expected `T` type on the Swift side. @attached(body) public macro JavaMethod( + _ javaMethodName: String? = nil, typeErasedResult: String? = nil ) = #externalMacro(module: "SwiftJavaMacros", type: "JavaMethodMacro") @@ -154,9 +155,7 @@ public macro JavaMethod( /// func sayHelloBack(_ i: Int32) -> Double /// ``` @attached(body) -public macro JavaStaticMethod( - typeErasedResult: String? = nil -) = #externalMacro(module: "SwiftJavaMacros", type: "JavaMethodMacro") +public macro JavaStaticMethod(_ javaMethodName: String? = nil) = #externalMacro(module: "SwiftJavaMacros", type: "JavaMethodMacro") /// Macro that marks extensions to specify that all of the @JavaMethod /// methods are implementations of Java methods spelled as `native`. diff --git a/Sources/SwiftJava/String+Extensions.swift b/Sources/SwiftJava/String+Extensions.swift index 0af0de107..b87e6a0c6 100644 --- a/Sources/SwiftJava/String+Extensions.swift +++ b/Sources/SwiftJava/String+Extensions.swift @@ -27,10 +27,14 @@ extension String { } extension String { - /// Replace all of the $'s for nested names with "." to turn a Java class - /// name into a Java canonical class name, + /// Convert a Java class name to its canonical name. + /// Replaces `$` with `.` for nested classes but preserves `$` at the start of identifiers. package var javaClassNameToCanonicalName: String { - return replacing("$", with: ".") + self.replacingOccurrences( + of: #"(?<=\w)\$"#, + with: ".", + options: .regularExpression + ) } /// Whether this is the name of an anonymous class. diff --git a/Sources/SwiftJavaConfigurationShared/Configuration.swift b/Sources/SwiftJavaConfigurationShared/Configuration.swift index 661cf9633..37d1e1e79 100644 --- a/Sources/SwiftJavaConfigurationShared/Configuration.swift +++ b/Sources/SwiftJavaConfigurationShared/Configuration.swift @@ -65,6 +65,10 @@ public struct Configuration: Codable { asyncFuncMode ?? .default } + public var enableJavaCallbacks: Bool? // FIXME: default it to false, but that plays not nice with Codable + + public var generatedJavaSourcesListFileOutput: String? + // ==== wrap-java --------------------------------------------------------- /// The Java class path that should be passed along to the swift-java tool. @@ -91,6 +95,8 @@ public struct Configuration: Codable { /// Exclude input Java types by their package prefix or exact match. public var filterExclude: [String]? + public var singleSwiftFileOutput: String? + // ==== dependencies --------------------------------------------------------- // Java dependencies we need to fetch for this target. @@ -162,7 +168,11 @@ public func readConfiguration(sourceDir: String, file: String = #fileID, line: U /// Configuration is expected to be "JSON-with-comments". /// Specifically "//" comments are allowed and will be trimmed before passing the rest of the config into a standard JSON parser. public func readConfiguration(configPath: URL, file: String = #fileID, line: UInt = #line) throws -> Configuration? { - guard let configData = try? Data(contentsOf: configPath) else { + let configData: Data + do { + configData = try Data(contentsOf: configPath) + } catch { + print("Failed to read SwiftJava configuration at '\(configPath.absoluteURL)', error: \(error)") return nil } diff --git a/Sources/SwiftJavaDocumentation/Documentation.docc/index.md b/Sources/SwiftJavaDocumentation/Documentation.docc/index.md index 460f396d5..4383727d9 100644 --- a/Sources/SwiftJavaDocumentation/Documentation.docc/index.md +++ b/Sources/SwiftJavaDocumentation/Documentation.docc/index.md @@ -20,7 +20,7 @@ Reasons why you might want to reach for Swift and Java interoperability include, - Reuse existing libraries which exist in one ecosystem, but don't have a direct equivalent in the other SwiftJava is offering several core libraries which support language interoperability: -- `JavaKit` (Swift -> Java) - JNI-based support library and Swift macros +- `SwiftJava` (Swift -> Java) - JNI-based support library and Swift macros - `SwiftKit` (Java -> Swift) - Support library for Java calling Swift code (either using JNI or FFM) - `swift-java` - command line tool; Supports source generation and also dependency management operations - Build tool integration - SwiftPM Plugin diff --git a/Sources/SwiftJavaMacros/JavaMethodMacro.swift b/Sources/SwiftJavaMacros/JavaMethodMacro.swift index f992ee760..099484528 100644 --- a/Sources/SwiftJavaMacros/JavaMethodMacro.swift +++ b/Sources/SwiftJavaMacros/JavaMethodMacro.swift @@ -50,15 +50,27 @@ extension JavaMethodMacro: BodyMacro { fatalError("not a function: \(declaration)") } + let funcName = + if case .argumentList(let arguments) = node.arguments, + let argument = arguments.first, + argument.label?.text != "typeErasedResult", + let stringLiteral = argument.expression.as(StringLiteralExprSyntax.self), + stringLiteral.segments.count == 1, + case let .stringSegment(funcNameSegment)? = stringLiteral.segments.first + { + funcNameSegment.content.text + } else { + funcDecl.name.text + } + let isStatic = node.attributeName.trimmedDescription == "JavaStaticMethod" - let funcName = funcDecl.name.text let params = funcDecl.signature.parameterClause.parameters let paramNames = params.map { param in param.parameterName?.text ?? "" }.joined(separator: ", ") let genericResultType: String? = if case let .argumentList(arguments) = node.arguments, - let firstElement = arguments.first, - let stringLiteral = firstElement.expression + let element = arguments.first(where: { $0.label?.text == "typeErasedResult" }), + let stringLiteral = element.expression .as(StringLiteralExprSyntax.self), stringLiteral.segments.count == 1, case let .stringSegment(wrapperName)? = stringLiteral.segments.first { diff --git a/Sources/SwiftJavaRuntimeSupport/JNIMethodIDCaches.swift b/Sources/SwiftJavaRuntimeSupport/JNIMethodIDCaches.swift index 16a9c899e..7be79a9fa 100644 --- a/Sources/SwiftJavaRuntimeSupport/JNIMethodIDCaches.swift +++ b/Sources/SwiftJavaRuntimeSupport/JNIMethodIDCaches.swift @@ -91,4 +91,33 @@ extension _JNIMethodIDCache { cache.methods[messageConstructor]! } } + + public enum JNISwiftInstance { + private static let memoryAddressMethod = Method( + name: "$memoryAddress", + signature: "()J" + ) + + private static let typeMetadataAddressMethod = Method( + name: "$typeMetadataAddress", + signature: "()J" + ) + + private static let cache = _JNIMethodIDCache( + className: "org/swift/swiftkit/core/JNISwiftInstance", + methods: [memoryAddressMethod, typeMetadataAddressMethod] + ) + + public static var `class`: jclass { + cache.javaClass + } + + public static var memoryAddress: jmethodID { + cache.methods[memoryAddressMethod]! + } + + public static var typeMetadataAddress: jmethodID { + cache.methods[typeMetadataAddressMethod]! + } + } } diff --git a/Sources/SwiftJavaRuntimeSupport/generated/JavaJNISwiftInstance.swift b/Sources/SwiftJavaRuntimeSupport/generated/JavaJNISwiftInstance.swift new file mode 100644 index 000000000..4040d0bcb --- /dev/null +++ b/Sources/SwiftJavaRuntimeSupport/generated/JavaJNISwiftInstance.swift @@ -0,0 +1,21 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +import SwiftJava + +@JavaInterface("org.swift.swiftkit.core.JNISwiftInstance") +public struct JavaJNISwiftInstance { + @JavaMethod("$memoryAddress") + public func memoryAddress() -> Int64 +} diff --git a/Sources/SwiftJavaTool/Commands/ConfigureCommand.swift b/Sources/SwiftJavaTool/Commands/ConfigureCommand.swift index 455cb962d..05bf3b8f3 100644 --- a/Sources/SwiftJavaTool/Commands/ConfigureCommand.swift +++ b/Sources/SwiftJavaTool/Commands/ConfigureCommand.swift @@ -59,8 +59,8 @@ extension SwiftJava { swiftModule } - @Argument(help: "The input file, which is either a swift-java configuration file or (if '-jar' was specified) a Jar file.") - var input: String? + @Option(help: "A prefix that will be added to the names of the Swift types") + var swiftTypePrefix: String? } } @@ -137,14 +137,19 @@ extension SwiftJava.ConfigureCommand { print("[debug][swift-java] Importing classpath entry: \(entry)") if entry.hasSuffix(".jar") { + print("[debug][swift-java] Importing classpath as JAR file: \(entry)") let jarFile = try JarFile(entry, false, environment: environment) try addJavaToSwiftMappings( to: &config, forJar: jarFile, environment: environment ) - } else if FileManager.default.fileExists(atPath: entry) { - log.warning("Currently unable handle directory classpath entries for config generation! Skipping: \(entry)") + } else if FileManager.default.fileExists(atPath: entry), let entryURL = URL(string: entry) { + print("[debug][swift-java] Importing classpath as directory: \(entryURL)") + try addJavaToSwiftMappings( + to: &config, + forDirectory: entryURL + ) } else { log.warning("Classpath entry does not exist, skipping: \(entry)") } @@ -162,62 +167,82 @@ extension SwiftJava.ConfigureCommand { ) } + mutating func addJavaToSwiftMappings( + to configuration: inout Configuration, + forDirectory url: Foundation.URL + ) throws { + let enumerator = FileManager.default.enumerator(atPath: url.path()) + + while let filePath = enumerator?.nextObject() as? String { + try addJavaToSwiftMappings(to: &configuration, fileName: filePath) + } + } + mutating func addJavaToSwiftMappings( to configuration: inout Configuration, forJar jarFile: JarFile, environment: JNIEnvironment ) throws { - let log = Self.log - for entry in jarFile.entries()! { - // We only look at class files in the Jar file. - guard entry.getName().hasSuffix(".class") else { - continue - } + try addJavaToSwiftMappings(to: &configuration, fileName: entry.getName()) + } + } - // Skip some "common" files we know that would be duplicated in every jar - guard !entry.getName().hasPrefix("META-INF") else { - continue - } - guard !entry.getName().hasSuffix("package-info") else { - continue - } - guard !entry.getName().hasSuffix("package-info.class") else { - continue - } + mutating func addJavaToSwiftMappings( + to configuration: inout Configuration, + fileName: String + ) throws { + // We only look at class files + guard fileName.hasSuffix(".class") else { + return + } - // If this is a local class, it cannot be mapped into Swift. - if entry.getName().isLocalJavaClass { - continue - } + // Skip some "common" files we know that would be duplicated in every jar + guard !fileName.hasPrefix("META-INF") else { + return + } + guard !fileName.hasSuffix("package-info") else { + return + } + guard !fileName.hasSuffix("package-info.class") else { + return + } - let javaCanonicalName = String(entry.getName().replacing("/", with: ".") - .dropLast(".class".count)) + // If this is a local class, it cannot be mapped into Swift. + if fileName.isLocalJavaClass { + return + } - guard SwiftJava.shouldImport(javaCanonicalName: javaCanonicalName, commonOptions: self.commonOptions) else { - log.info("Skip importing class: \(javaCanonicalName) due to include/exclude filters") - continue - } + let javaCanonicalName = String(fileName.replacing("/", with: ".") + .dropLast(".class".count)) - if configuration.classes?[javaCanonicalName] != nil { - // We never overwrite an existing class mapping configuration. - // E.g. the user may have configured a custom name for a type. - continue - } + guard SwiftJava.shouldImport(javaCanonicalName: javaCanonicalName, commonOptions: self.commonOptions) else { + log.info("Skip importing class: \(javaCanonicalName) due to include/exclude filters") + return + } - if configuration.classes == nil { - configuration.classes = [:] - } + if configuration.classes?[javaCanonicalName] != nil { + // We never overwrite an existing class mapping configuration. + // E.g. the user may have configured a custom name for a type. + return + } - if let configuredSwiftName = configuration.classes![javaCanonicalName] { - log.info("Java type '\(javaCanonicalName)' already configured as '\(configuredSwiftName)' Swift type.") - } else { - log.info("Configure Java type '\(javaCanonicalName)' as '\(javaCanonicalName.defaultSwiftNameForJavaClass.bold)' Swift type.") - } + if configuration.classes == nil { + configuration.classes = [:] + } - configuration.classes![javaCanonicalName] = - javaCanonicalName.defaultSwiftNameForJavaClass + var swiftName = javaCanonicalName.defaultSwiftNameForJavaClass + if let swiftTypePrefix { + swiftName = "\(swiftTypePrefix)\(swiftName)" } + + if let configuredSwiftName = configuration.classes![javaCanonicalName] { + log.info("Java type '\(javaCanonicalName)' already configured as '\(configuredSwiftName)' Swift type.") + } else { + log.info("Configure Java type '\(javaCanonicalName)' as '\(swiftName.bold)' Swift type.") + } + + configuration.classes![javaCanonicalName] = swiftName } } diff --git a/Sources/SwiftJavaTool/Commands/JExtractCommand.swift b/Sources/SwiftJavaTool/Commands/JExtractCommand.swift index b5c3a7bb9..775ab6040 100644 --- a/Sources/SwiftJavaTool/Commands/JExtractCommand.swift +++ b/Sources/SwiftJavaTool/Commands/JExtractCommand.swift @@ -81,6 +81,12 @@ extension SwiftJava { @Option(help: "The mode to use for extracting asynchronous Swift functions. By default async methods are extracted as Java functions returning CompletableFuture.") var asyncFuncMode: JExtractAsyncFuncMode? + + @Flag(help: "By enabling this mode, JExtract will generate Java code that allows you to implement Swift protocols using Java classes. This feature requires disabling the sandbox mode in SwiftPM. This only works in the 'jni' mode.") + var enableJavaCallbacks: Bool = false + + @Option(help: "If specified, JExtract will output to this file a list of paths to all generated Java source files") + var generatedJavaSourcesListFileOutput: String? } } @@ -96,10 +102,14 @@ extension SwiftJava.JExtractCommand { let writeEmptyFiles = CommandLine.arguments.contains("--write-empty-files") ? true : nil configure(&config.writeEmptyFiles, overrideWith: writeEmptyFiles) + let enableJavaCallbacks = CommandLine.arguments.contains("--enable-java-callbacks") ? true : nil + configure(&config.enableJavaCallbacks, overrideWith: enableJavaCallbacks) + configure(&config.unsignedNumbersMode, overrideWith: self.unsignedNumbersMode) configure(&config.minimumInputAccessLevelMode, overrideWith: self.minimumInputAccessLevelMode) configure(&config.memoryManagementMode, overrideWith: self.memoryManagementMode) configure(&config.asyncFuncMode, overrideWith: self.asyncFuncMode) + configure(&config.generatedJavaSourcesListFileOutput, overrideWith: self.generatedJavaSourcesListFileOutput) try checkModeCompatibility(config: config) @@ -126,11 +136,15 @@ extension SwiftJava.JExtractCommand { case .annotate: () // OK case .wrapGuava: - throw IllegalModeCombinationError("JNI mode does not support '\(JExtractUnsignedIntegerMode.wrapGuava)' Unsigned integer mode! \(Self.helpMessage)") + throw IllegalModeCombinationError("JNI mode does not support '\(JExtractUnsignedIntegerMode.wrapGuava)' Unsigned integer mode! \(Self.helpMessage())") } } else if config.effectiveMode == .ffm { guard config.effectiveMemoryManagementMode == .explicit else { - throw IllegalModeCombinationError("FFM mode does not support '\(self.memoryManagementMode)' memory management mode! \(Self.helpMessage)") + throw IllegalModeCombinationError("FFM mode does not support '\(self.memoryManagementMode ?? .default)' memory management mode! \(Self.helpMessage())") + } + + if let enableJavaCallbacks = config.enableJavaCallbacks, enableJavaCallbacks { + throw IllegalModeCombinationError("FFM mode does not support enabling Java callbacks! \(Self.helpMessage())") } } } diff --git a/Sources/SwiftJavaTool/Commands/WrapJavaCommand.swift b/Sources/SwiftJavaTool/Commands/WrapJavaCommand.swift index 51315102a..3e4e482b3 100644 --- a/Sources/SwiftJavaTool/Commands/WrapJavaCommand.swift +++ b/Sources/SwiftJavaTool/Commands/WrapJavaCommand.swift @@ -58,8 +58,8 @@ extension SwiftJava { @Option(help: "Match java package directory structure with generated Swift files") var swiftMatchPackageDirectoryStructure: Bool = false - @Argument(help: "Path to .jar file whose Java classes should be wrapped using Swift bindings") - var input: String + @Option(help: "If specified, a single Swift file will be generated containing all the generated code") + var singleSwiftFileOutput: String? } } @@ -69,6 +69,7 @@ extension SwiftJava.WrapJavaCommand { print("self.commonOptions.filterInclude = \(self.commonOptions.filterInclude)") configure(&config.filterInclude, append: self.commonOptions.filterInclude) configure(&config.filterExclude, append: self.commonOptions.filterExclude) + configure(&config.singleSwiftFileOutput, overrideWith: self.singleSwiftFileOutput) // Get base classpath configuration for this target and configuration var classpathSearchDirs = [self.effectiveSwiftModuleURL] @@ -78,7 +79,6 @@ extension SwiftJava.WrapJavaCommand { } else { print("[trace][swift-java] Cache directory: none") } - print("[trace][swift-java] INPUT: \(input)") var classpathEntries = self.configureCommandJVMClasspath( searchDirs: classpathSearchDirs, config: config, log: Self.log) @@ -151,21 +151,41 @@ extension SwiftJava.WrapJavaCommand { .getSystemClassLoader()! var javaClasses: [JavaClass] = [] for (javaClassName, _) in config.classes ?? [:] { + func remove() { + translator.translatedClasses.removeValue(forKey: javaClassName) + } + guard shouldImportJavaClass(javaClassName, config: config) else { + remove() continue } - log.info("Wrapping java type: \(javaClassName)") - guard let javaClass = try classLoader.loadClass(javaClassName) else { log.warning("Could not load Java class '\(javaClassName)', skipping.") + remove() + continue + } + + guard self.shouldExtract(javaClass: javaClass, config: config) else { + log.info("Skip Java type: \(javaClassName) (does not match minimum access level)") + remove() + continue + } + + guard !javaClass.isEnum() else { + log.info("Skip Java type: \(javaClassName) (enums do not currently work)") + remove() continue } + log.info("Wrapping java type: \(javaClassName)") + // Add this class to the list of classes we'll translate. javaClasses.append(javaClass) } + log.info("OK now we go to nested classes") + // Find all of the nested classes for each class, adding them to the list // of classes to be translated if they were already specified. var allClassesToVisit = javaClasses @@ -212,6 +232,16 @@ extension SwiftJava.WrapJavaCommand { return nil } + guard self.shouldExtract(javaClass: nestedClass, config: config) else { + log.info("Skip Java type: \(javaClassName) (does not match minimum access level)") + return nil + } + + guard !nestedClass.isEnum() else { + log.info("Skip Java type: \(javaClassName) (enums do not currently work)") + return nil + } + // Record this as a translated class. let swiftUnqualifiedName = javaClassName.javaClassNameToCanonicalName .defaultSwiftNameForJavaClass @@ -237,9 +267,13 @@ extension SwiftJava.WrapJavaCommand { try translator.validateClassConfiguration() // Translate all of the Java classes into Swift classes. - for javaClass in javaClasses { + + if let singleSwiftFileOutput = config.singleSwiftFileOutput { translator.startNewFile() - let swiftClassDecls = try translator.translateClass(javaClass) + + let swiftClassDecls = try javaClasses.flatMap { + try translator.translateClass($0) + } let importDecls = translator.getImportDecls() let swiftFileText = """ @@ -249,19 +283,53 @@ extension SwiftJava.WrapJavaCommand { """ - var generatedFileOutputDir = self.actualOutputDirectory - if self.swiftMatchPackageDirectoryStructure { - generatedFileOutputDir?.append(path: javaClass.getPackageName().replacing(".", with: "/")) - } - - let swiftFileName = try! translator.getSwiftTypeName(javaClass, preferValueTypes: false) - .swiftName.replacing(".", with: "+") + ".swift" try writeContents( swiftFileText, - outputDirectory: generatedFileOutputDir, - to: swiftFileName, - description: "Java class '\(javaClass.getName())' translation" + outputDirectory: self.actualOutputDirectory, + to: singleSwiftFileOutput, + description: "Java class translation" ) + } else { + for javaClass in javaClasses { + translator.startNewFile() + + let swiftClassDecls = try translator.translateClass(javaClass) + let importDecls = translator.getImportDecls() + + let swiftFileText = """ + // Auto-generated by Java-to-Swift wrapper generator. + \(importDecls.map { $0.description }.joined()) + \(swiftClassDecls.map { $0.description }.joined(separator: "\n")) + + """ + + var generatedFileOutputDir = self.actualOutputDirectory + if self.swiftMatchPackageDirectoryStructure { + generatedFileOutputDir?.append(path: javaClass.getPackageName().replacing(".", with: "/")) + } + + let swiftFileName = try translator.getSwiftTypeName(javaClass, preferValueTypes: false) + .swiftName.replacing(".", with: "+") + ".swift" + try writeContents( + swiftFileText, + outputDirectory: generatedFileOutputDir, + to: swiftFileName, + description: "Java class '\(javaClass.getName())' translation" + ) + } + } + } + + /// Determines whether a method should be extracted for translation. + /// Only look at public and protected methods here. + private func shouldExtract(javaClass: JavaClass, config: Configuration) -> Bool { + switch config.effectiveMinimumInputAccessLevelMode { + case .internal: + return javaClass.isPublic || javaClass.isProtected || javaClass.isPackage + case .package: + return javaClass.isPublic || javaClass.isProtected || javaClass.isPackage + case .public: + return javaClass.isPublic || javaClass.isProtected } } diff --git a/Sources/SwiftJavaTool/CommonOptions.swift b/Sources/SwiftJavaTool/CommonOptions.swift index c313202bc..4627381e0 100644 --- a/Sources/SwiftJavaTool/CommonOptions.swift +++ b/Sources/SwiftJavaTool/CommonOptions.swift @@ -67,6 +67,9 @@ extension SwiftJava { @Option(name: .long, help: "While scanning a classpath, skip types which match the filter prefix") var filterExclude: [String] = [] + + @Option(help: "A path to a custom swift-java.config to use") + var config: String? = nil } struct CommonJVMOptions: ParsableArguments { @@ -145,4 +148,4 @@ extension HasCommonJVMOptions { func makeJVM(classpathEntries: [String]) throws -> JavaVirtualMachine { try JavaVirtualMachine.shared(classpath: classpathEntries) } -} \ No newline at end of file +} diff --git a/Sources/SwiftJavaTool/SwiftJavaBaseAsyncParsableCommand.swift b/Sources/SwiftJavaTool/SwiftJavaBaseAsyncParsableCommand.swift index 9763b4b38..92818e43a 100644 --- a/Sources/SwiftJavaTool/SwiftJavaBaseAsyncParsableCommand.swift +++ b/Sources/SwiftJavaTool/SwiftJavaBaseAsyncParsableCommand.swift @@ -163,14 +163,24 @@ extension SwiftJavaBaseAsyncParsableCommand { func readInitialConfiguration(command: some SwiftJavaBaseAsyncParsableCommand) throws -> Configuration { var earlyConfig: Configuration? - if let moduleBaseDir { + if let configPath = commonOptions.config { + let configURL = URL(filePath: configPath, directoryHint: .notDirectory) + print("[debug][swift-java] Load config from passed in path: \(configURL)") + earlyConfig = try readConfiguration(configPath: configURL) + } else if let moduleBaseDir { print("[debug][swift-java] Load config from module base directory: \(moduleBaseDir.path)") earlyConfig = try readConfiguration(sourceDir: moduleBaseDir.path) } else if let inputSwift = commonOptions.inputSwift { print("[debug][swift-java] Load config from module swift input directory: \(inputSwift)") earlyConfig = try readConfiguration(sourceDir: inputSwift) } - var config = earlyConfig ?? Configuration() + var config: Configuration + if let earlyConfig { + config = earlyConfig + } else { + log.warning("[swift-java] Failed to load initial configuration. Proceeding with empty configuration.") + config = Configuration() + } // override configuration with options from command line config.logLevel = command.logLevel return config diff --git a/Sources/SwiftJavaToolLib/JavaClassTranslator.swift b/Sources/SwiftJavaToolLib/JavaClassTranslator.swift index 0f7aa45d6..ce780f904 100644 --- a/Sources/SwiftJavaToolLib/JavaClassTranslator.swift +++ b/Sources/SwiftJavaToolLib/JavaClassTranslator.swift @@ -228,6 +228,11 @@ struct JavaClassTranslator { continue } + guard method.getName().isValidSwiftFunctionName else { + log.warning("Skipping method \(method.getName()) because it is not a valid Swift function name") + continue + } + addMethod(method, isNative: false) } @@ -604,6 +609,15 @@ extension JavaClassTranslator { } } + // --- Parameter types + for parameter in method.getParameters() { + if let parameterizedType = parameter?.getParameterizedType() { + if parameterizedType.isEqualTo(typeParam.as(Type.self)) { + return true + } + } + } + return false } diff --git a/Sources/SwiftJavaToolLib/StringExtras.swift b/Sources/SwiftJavaToolLib/StringExtras.swift index e69f379c3..c3ac5390f 100644 --- a/Sources/SwiftJavaToolLib/StringExtras.swift +++ b/Sources/SwiftJavaToolLib/StringExtras.swift @@ -35,6 +35,11 @@ extension String { return "`\(self)`" } + /// Returns whether this is a valid Swift function name + var isValidSwiftFunctionName: Bool { + !self.starts(with: "$") + } + /// Replace all occurrences of one character in the string with another. public func replacing(_ character: Character, with replacement: Character) -> String { return replacingOccurrences(of: String(character), with: String(replacement)) diff --git a/Tests/JExtractSwiftTests/JNI/JNIEnumTests.swift b/Tests/JExtractSwiftTests/JNI/JNIEnumTests.swift index 38ef87895..2188bcd62 100644 --- a/Tests/JExtractSwiftTests/JNI/JNIEnumTests.swift +++ b/Tests/JExtractSwiftTests/JNI/JNIEnumTests.swift @@ -170,17 +170,17 @@ struct JNIEnumTests { """, """ public record First() implements Case { - record $NativeParameters() {} + record _NativeParameters() {} } """, """ public record Second(java.lang.String arg0) implements Case { - record $NativeParameters(java.lang.String arg0) {} + record _NativeParameters(java.lang.String arg0) {} } """, """ public record Third(long x, int y) implements Case { - record $NativeParameters(long x, int y) {} + record _NativeParameters(long x, int y) {} } """ ]) @@ -268,7 +268,7 @@ struct JNIEnumTests { if (getDiscriminator() != Discriminator.SECOND) { return Optional.empty(); } - Second.$NativeParameters $nativeParameters = MyEnum.$getAsSecond(this.$memoryAddress()); + Second._NativeParameters $nativeParameters = MyEnum.$getAsSecond(this.$memoryAddress()); return Optional.of(new Second($nativeParameters.arg0)); } """, @@ -277,7 +277,7 @@ struct JNIEnumTests { if (getDiscriminator() != Discriminator.THIRD) { return Optional.empty(); } - Third.$NativeParameters $nativeParameters = MyEnum.$getAsThird(this.$memoryAddress()); + Third._NativeParameters $nativeParameters = MyEnum.$getAsThird(this.$memoryAddress()); return Optional.of(new Third($nativeParameters.x, $nativeParameters.y)); } """ diff --git a/Tests/JExtractSwiftTests/JNI/JNIProtocolTests.swift b/Tests/JExtractSwiftTests/JNI/JNIProtocolTests.swift index c5302ca64..231c4d25d 100644 --- a/Tests/JExtractSwiftTests/JNI/JNIProtocolTests.swift +++ b/Tests/JExtractSwiftTests/JNI/JNIProtocolTests.swift @@ -12,16 +12,22 @@ // //===----------------------------------------------------------------------===// +import SwiftJavaConfigurationShared import JExtractSwiftLib import Testing @Suite struct JNIProtocolTests { + var config: Configuration { + var config = Configuration() + config.enableJavaCallbacks = true + return config + } + let source = """ public protocol SomeProtocol { - var x: Int64 { get set } - public func method() {} + public func withObject(c: SomeClass) -> SomeClass {} } public protocol B {} @@ -37,6 +43,7 @@ struct JNIProtocolTests { func generatesJavaInterface() throws { try assertOutput( input: source, + config: config, .jni, .java, detectChunkByInitialLines: 1, expectedChunks: [ @@ -50,18 +57,13 @@ struct JNIProtocolTests { import org.swift.swiftkit.core.util.*; """, """ - public interface SomeProtocol extends JNISwiftInstance { - ... + public interface SomeProtocol { + ... + public void method(); + ... + public SomeClass withObject(SomeClass c); + ... } - """, - """ - public long getX(); - """, - """ - public void setX(long newValue); - """, - """ - public void method(); """ ]) } @@ -70,6 +72,7 @@ struct JNIProtocolTests { func generatesJavaClassWithExtends() throws { try assertOutput( input: source, + config: config, .jni, .java, detectChunkByInitialLines: 1, expectedChunks: [ @@ -83,16 +86,17 @@ struct JNIProtocolTests { func takeProtocol_java() throws { try assertOutput( input: source, + config: config, .jni, .java, detectChunkByInitialLines: 1, expectedChunks: [ """ - public static <$T0 extends SomeProtocol, $T1 extends SomeProtocol> void takeProtocol($T0 x, $T1 y) { - SwiftModule.$takeProtocol(x.$memoryAddress(), x.$typeMetadataAddress(), y.$memoryAddress(), y.$typeMetadataAddress()); + public static <_T0 extends SomeProtocol, _T1 extends SomeProtocol> void takeProtocol(_T0 x, _T1 y) { + SwiftModule.$takeProtocol(x, y); } """, """ - private static native void $takeProtocol(long x, long x_typeMetadataAddress, long y, long y_typeMetadataAddress); + private static native void $takeProtocol(java.lang.Object x, java.lang.Object y); """ ]) } @@ -101,43 +105,50 @@ struct JNIProtocolTests { func takeProtocol_swift() throws { try assertOutput( input: source, + config: config, .jni, .swift, detectChunkByInitialLines: 1, expectedChunks: [ """ - @_cdecl("Java_com_example_swift_SwiftModule__00024takeProtocol__JJJJ") - func Java_com_example_swift_SwiftModule__00024takeProtocol__JJJJ(environment: UnsafeMutablePointer!, thisClass: jclass, x: jlong, x_typeMetadataAddress: jlong, y: jlong, y_typeMetadataAddress: jlong) { - guard let xTypeMetadataPointer$ = UnsafeRawPointer(bitPattern: Int(Int64(fromJNI: x_typeMetadataAddress, in: environment))) else { - fatalError("x_typeMetadataAddress memory address was null") - } - let xDynamicType$: Any.Type = unsafeBitCast(xTypeMetadataPointer$, to: Any.Type.self) - guard let xRawPointer$ = UnsafeMutableRawPointer(bitPattern: Int(Int64(fromJNI: x, in: environment))) else { - fatalError("x memory address was null") + final class _SwiftModule_takeGeneric_s_Wrapper: SwiftJavaSomeProtocolWrapper { + let _javaSomeProtocolInterface: JavaSomeProtocol + init(_javaSomeProtocolInterface: JavaSomeProtocol) { + self._javaSomeProtocolInterface = _javaSomeProtocolInterface } - #if hasFeature(ImplicitOpenExistentials) - let xExistential$ = xRawPointer$.load(as: xDynamicType$) as! any (SomeProtocol) - #else - func xDoLoad(_ ty: Ty.Type) -> any (SomeProtocol) { - xRawPointer$.load(as: ty) as! any (SomeProtocol) + } + """, + """ + @_cdecl("Java_com_example_swift_SwiftModule__00024takeProtocol__Ljava_lang_Object_2Ljava_lang_Object_2") + func Java_com_example_swift_SwiftModule__00024takeProtocol__Ljava_lang_Object_2Ljava_lang_Object_2(environment: UnsafeMutablePointer!, thisClass: jclass, x: jobject?, y: jobject?) { + let xswiftObject$: (SomeProtocol) + if environment.interface.IsInstanceOf(environment, x, _JNIMethodIDCache.JNISwiftInstance.class) != 0 { + ... + let xpointer$DynamicType$: Any.Type = unsafeBitCast(xpointer$TypeMetadataPointer$, to: Any.Type.self) + guard let xpointer$RawPointer$ = UnsafeMutableRawPointer(bitPattern: Int(Int64(fromJNI: xpointer$, in: environment))) else { + fatalError("xpointer$ memory address was null") + } + #if hasFeature(ImplicitOpenExistentials) + let xpointer$Existential$ = xpointer$RawPointer$.load(as: xpointer$DynamicType$) as! any (SomeProtocol) + #else + func xpointer$DoLoad(_ ty: Ty.Type) -> any (SomeProtocol) { + xpointer$RawPointer$.load(as: ty) as! any (SomeProtocol) + } + let xpointer$Existential$ = _openExistential(xpointer$DynamicType$, do: xpointer$DoLoad) + #endif + xswiftObject$ = xpointer$Existential$ } - let xExistential$ = _openExistential(xDynamicType$, do: xDoLoad) - #endif - guard let yTypeMetadataPointer$ = UnsafeRawPointer(bitPattern: Int(Int64(fromJNI: y_typeMetadataAddress, in: environment))) else { - fatalError("y_typeMetadataAddress memory address was null") + else { + xswiftObject$ = _SwiftModule_takeProtocol_x_Wrapper(_javaSomeProtocolInterface: JavaSomeProtocol(javaThis: x!, environment: environment)) } - let yDynamicType$: Any.Type = unsafeBitCast(yTypeMetadataPointer$, to: Any.Type.self) - guard let yRawPointer$ = UnsafeMutableRawPointer(bitPattern: Int(Int64(fromJNI: y, in: environment))) else { - fatalError("y memory address was null") + let yswiftObject$: (SomeProtocol) + if environment.interface.IsInstanceOf(environment, y, _JNIMethodIDCache.JNISwiftInstance.class) != 0 { + ... + yswiftObject$ = ypointer$Existential$ } - #if hasFeature(ImplicitOpenExistentials) - let yExistential$ = yRawPointer$.load(as: yDynamicType$) as! any (SomeProtocol) - #else - func yDoLoad(_ ty: Ty.Type) -> any (SomeProtocol) { - yRawPointer$.load(as: ty) as! any (SomeProtocol) + else { + yswiftObject$ = _SwiftModule_takeProtocol_y_Wrapper(_javaSomeProtocolInterface: JavaSomeProtocol(javaThis: y!, environment: environment)) } - let yExistential$ = _openExistential(yDynamicType$, do: yDoLoad) - #endif - SwiftModule.takeProtocol(x: xExistential$, y: yExistential$) + SwiftModule.takeProtocol(x: xswiftObject$, y: yswiftObject$) } """ ] @@ -148,16 +159,17 @@ struct JNIProtocolTests { func takeGeneric_java() throws { try assertOutput( input: source, + config: config, .jni, .java, detectChunkByInitialLines: 1, expectedChunks: [ """ public static void takeGeneric(S s) { - SwiftModule.$takeGeneric(s.$memoryAddress(), s.$typeMetadataAddress()); + SwiftModule.$takeGeneric(s); } """, """ - private static native void $takeGeneric(long s, long s_typeMetadataAddress); + private static native void $takeGeneric(java.lang.Object s); """ ]) } @@ -166,28 +178,30 @@ struct JNIProtocolTests { func takeGeneric_swift() throws { try assertOutput( input: source, + config: config, .jni, .swift, detectChunkByInitialLines: 1, expectedChunks: [ """ - @_cdecl("Java_com_example_swift_SwiftModule__00024takeGeneric__JJ") - func Java_com_example_swift_SwiftModule__00024takeGeneric__JJ(environment: UnsafeMutablePointer!, thisClass: jclass, s: jlong, s_typeMetadataAddress: jlong) { - guard let sTypeMetadataPointer$ = UnsafeRawPointer(bitPattern: Int(Int64(fromJNI: s_typeMetadataAddress, in: environment))) else { - fatalError("s_typeMetadataAddress memory address was null") + final class _SwiftModule_takeGeneric_s_Wrapper: SwiftJavaSomeProtocolWrapper { + let _javaSomeProtocolInterface: JavaSomeProtocol + init(_javaSomeProtocolInterface: JavaSomeProtocol) { + self._javaSomeProtocolInterface = _javaSomeProtocolInterface } - let sDynamicType$: Any.Type = unsafeBitCast(sTypeMetadataPointer$, to: Any.Type.self) - guard let sRawPointer$ = UnsafeMutableRawPointer(bitPattern: Int(Int64(fromJNI: s, in: environment))) else { - fatalError("s memory address was null") + } + """, + """ + @_cdecl("Java_com_example_swift_SwiftModule__00024takeGeneric__Ljava_lang_Object_2") + func Java_com_example_swift_SwiftModule__00024takeGeneric__Ljava_lang_Object_2(environment: UnsafeMutablePointer!, thisClass: jclass, s: jobject?) { + let sswiftObject$: (SomeProtocol) + if environment.interface.IsInstanceOf(environment, s, _JNIMethodIDCache.JNISwiftInstance.class) != 0 { + ... + sswiftObject$ = spointer$Existential$ } - #if hasFeature(ImplicitOpenExistentials) - let sExistential$ = sRawPointer$.load(as: sDynamicType$) as! any (SomeProtocol) - #else - func sDoLoad(_ ty: Ty.Type) -> any (SomeProtocol) { - sRawPointer$.load(as: ty) as! any (SomeProtocol) + else { + sswiftObject$ = _SwiftModule_takeGeneric_s_Wrapper(_javaSomeProtocolInterface: JavaSomeProtocol(javaThis: s!, environment: environment)) } - let sExistential$ = _openExistential(sDynamicType$, do: sDoLoad) - #endif - SwiftModule.takeGeneric(s: sExistential$) + SwiftModule.takeGeneric(s: sswiftObject$) } """ ] @@ -198,16 +212,17 @@ struct JNIProtocolTests { func takeComposite_java() throws { try assertOutput( input: source, + config: config, .jni, .java, detectChunkByInitialLines: 1, expectedChunks: [ """ - public static <$T0 extends SomeProtocol & B> void takeComposite($T0 x) { - SwiftModule.$takeComposite(x.$memoryAddress(), x.$typeMetadataAddress()); + public static <_T0 extends SomeProtocol & B> void takeComposite(_T0 x) { + SwiftModule.$takeComposite(x); } """, """ - private static native void $takeComposite(long x, long x_typeMetadataAddress); + private static native void $takeComposite(java.lang.Object x); """ ]) } @@ -216,28 +231,92 @@ struct JNIProtocolTests { func takeComposite_swift() throws { try assertOutput( input: source, + config: config, .jni, .swift, detectChunkByInitialLines: 1, expectedChunks: [ """ - @_cdecl("Java_com_example_swift_SwiftModule__00024takeComposite__JJ") - func Java_com_example_swift_SwiftModule__00024takeComposite__JJ(environment: UnsafeMutablePointer!, thisClass: jclass, x: jlong, x_typeMetadataAddress: jlong) { - guard let xTypeMetadataPointer$ = UnsafeRawPointer(bitPattern: Int(Int64(fromJNI: x_typeMetadataAddress, in: environment))) else { - fatalError("x_typeMetadataAddress memory address was null") + final class _SwiftModule_takeComposite_x_Wrapper: SwiftJavaSomeProtocolWrapper, SwiftJavaBWrapper { + let _javaSomeProtocolInterface: JavaSomeProtocol + let _javaBInterface: JavaB + init(_javaSomeProtocolInterface: JavaSomeProtocol, _javaBInterface: JavaB) { + self._javaSomeProtocolInterface = _javaSomeProtocolInterface + self._javaBInterface = _javaBInterface } - let xDynamicType$: Any.Type = unsafeBitCast(xTypeMetadataPointer$, to: Any.Type.self) - guard let xRawPointer$ = UnsafeMutableRawPointer(bitPattern: Int(Int64(fromJNI: x, in: environment))) else { - fatalError("x memory address was null") + } + """, + """ + @_cdecl("Java_com_example_swift_SwiftModule__00024takeComposite__Ljava_lang_Object_2") + func Java_com_example_swift_SwiftModule__00024takeComposite__Ljava_lang_Object_2(environment: UnsafeMutablePointer!, thisClass: jclass, x: jobject?) { + let xswiftObject$: (SomeProtocol & B) + if environment.interface.IsInstanceOf(environment, x, _JNIMethodIDCache.JNISwiftInstance.class) != 0 { + let xpointer$ = environment.interface.CallLongMethodA(environment, x, _JNIMethodIDCache.JNISwiftInstance.memoryAddress, []) + let xtypeMetadata$ = environment.interface.CallLongMethodA(environment, x, _JNIMethodIDCache.JNISwiftInstance.typeMetadataAddress, []) + guard let xpointer$TypeMetadataPointer$ = UnsafeRawPointer(bitPattern: Int(Int64(fromJNI: xtypeMetadata$, in: environment))) else { + fatalError("xtypeMetadata$ memory address was null") + } + let xpointer$DynamicType$: Any.Type = unsafeBitCast(xpointer$TypeMetadataPointer$, to: Any.Type.self) + guard let xpointer$RawPointer$ = UnsafeMutableRawPointer(bitPattern: Int(Int64(fromJNI: xpointer$, in: environment))) else { + fatalError("xpointer$ memory address was null") + } + #if hasFeature(ImplicitOpenExistentials) + let xpointer$Existential$ = xpointer$RawPointer$.load(as: xpointer$DynamicType$) as! any (SomeProtocol & B) + #else + func xpointer$DoLoad(_ ty: Ty.Type) -> any (SomeProtocol & B) { + xpointer$RawPointer$.load(as: ty) as! any (SomeProtocol & B) + } + let xpointer$Existential$ = _openExistential(xpointer$DynamicType$, do: xpointer$DoLoad) + #endif + xswiftObject$ = xpointer$Existential$ } - #if hasFeature(ImplicitOpenExistentials) - let xExistential$ = xRawPointer$.load(as: xDynamicType$) as! any (SomeProtocol & B) - #else - func xDoLoad(_ ty: Ty.Type) -> any (SomeProtocol & B) { - xRawPointer$.load(as: ty) as! any (SomeProtocol & B) + else { + xswiftObject$ = _SwiftModule_takeComposite_x_Wrapper(_javaSomeProtocolInterface: JavaSomeProtocol(javaThis: x!, environment: environment), _javaBInterface: JavaB(javaThis: x!, environment: environment)) } - let xExistential$ = _openExistential(xDynamicType$, do: xDoLoad) - #endif - SwiftModule.takeComposite(x: xExistential$) + SwiftModule.takeComposite(x: xswiftObject$) + } + """ + ] + ) + } + + @Test + func generatesProtocolWrappers() throws { + try assertOutput( + input: source, + config: config, + .jni, .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + protocol SwiftJavaSomeProtocolWrapper: SomeProtocol { + var _javaSomeProtocolInterface: JavaSomeProtocol { get } + } + """, + """ + extension SwiftJavaSomeProtocolWrapper { + public func method() { + _javaSomeProtocolInterface.method() + } + public func withObject(c: SomeClass) -> SomeClass { + let cClass = try! JavaClass(environment: JavaVirtualMachine.shared().environment()) + let cPointer = UnsafeMutablePointer.allocate(capacity: 1) + cPointer.initialize(to: c) + guard let unwrapped$ = _javaSomeProtocolInterface.withObject(cClass.wrapMemoryAddressUnsafe(Int64(Int(bitPattern: cPointer)))) else { + fatalError("Upcall to withObject unexpectedly returned nil") + } + let result$MemoryAddress$ = unwrapped$.as(JavaJNISwiftInstance.self)!.memoryAddress() + let result$Pointer = UnsafeMutablePointer(bitPattern: Int(result$MemoryAddress$))! + return result$Pointer.pointee + } + } + """, + """ + protocol SwiftJavaBWrapper: B { + var _javaBInterface: JavaB { get } + } + """, + """ + extension SwiftJavaBWrapper { } """ ] diff --git a/Tests/JExtractSwiftTests/MemoryManagementModeTests.swift b/Tests/JExtractSwiftTests/MemoryManagementModeTests.swift index 2228aad88..7e78434d1 100644 --- a/Tests/JExtractSwiftTests/MemoryManagementModeTests.swift +++ b/Tests/JExtractSwiftTests/MemoryManagementModeTests.swift @@ -99,7 +99,7 @@ struct MemoryManagementModeTests { } """, """ - public MyClass f(SwiftArena swiftArena$); + public MyClass f(); """ ] ) diff --git a/Tests/SwiftJavaToolLibTests/WrapJavaTests/GenericsWrapJavaTests.swift b/Tests/SwiftJavaToolLibTests/WrapJavaTests/GenericsWrapJavaTests.swift index cba7b38f5..ceb05df97 100644 --- a/Tests/SwiftJavaToolLibTests/WrapJavaTests/GenericsWrapJavaTests.swift +++ b/Tests/SwiftJavaToolLibTests/WrapJavaTests/GenericsWrapJavaTests.swift @@ -338,6 +338,45 @@ final class GenericsWrapJavaTests: XCTestCase { ) } + func test_wrapJava_genericMethodTypeErasure_customInterface_staticMethods() async throws { + let classpathURL = try await compileJava( + """ + package com.example; + + interface MyInterface {} + + final class Public { + public static void useInterface(T myInterface) { } + } + """) + + try assertWrapJavaOutput( + javaClassNames: [ + "com.example.MyInterface", + "com.example.Public" + ], + classpath: [classpathURL], + expectedChunks: [ + """ + @JavaInterface("com.example.MyInterface") + public struct MyInterface { + """, + """ + @JavaClass("com.example.Public") + open class Public: JavaObject { + """, + """ + extension JavaClass { + """, + """ + @JavaStaticMethod + public func useInterface(_ arg0: T?) + } + """ + ] + ) + } + // TODO: this should be improved some more, we need to generated a `: Map` on the Swift side func test_wrapJava_genericMethodTypeErasure_genericExtendsMap() async throws { let classpathURL = try await compileJava( @@ -370,4 +409,4 @@ final class GenericsWrapJavaTests: XCTestCase { ) } -} \ No newline at end of file +} From a33d709b28bb09b81cce248212f804e21b57f59d Mon Sep 17 00:00:00 2001 From: Konrad `ktoso` Malawski Date: Wed, 3 Dec 2025 12:44:21 +0900 Subject: [PATCH 09/14] test: add tests for @Unsigned in methods in jextract JNI mode (#475) --- .../JExtract/JExtractGenerationMode.swift | 2 +- .../UnsignedNumberTests.swift | 487 +++++++++++------- 2 files changed, 315 insertions(+), 174 deletions(-) diff --git a/Sources/SwiftJavaConfigurationShared/JExtract/JExtractGenerationMode.swift b/Sources/SwiftJavaConfigurationShared/JExtract/JExtractGenerationMode.swift index 1ad331da7..8e11d82b0 100644 --- a/Sources/SwiftJavaConfigurationShared/JExtract/JExtractGenerationMode.swift +++ b/Sources/SwiftJavaConfigurationShared/JExtract/JExtractGenerationMode.swift @@ -13,7 +13,7 @@ //===----------------------------------------------------------------------===// /// Determines which source generation mode JExtract should be using: JNI or Foreign Function and Memory. -public enum JExtractGenerationMode: String, Codable { +public enum JExtractGenerationMode: String, Sendable, Codable { /// Foreign Value and Memory API case ffm diff --git a/Tests/JExtractSwiftTests/UnsignedNumberTests.swift b/Tests/JExtractSwiftTests/UnsignedNumberTests.swift index 8109714cb..f3f784098 100644 --- a/Tests/JExtractSwiftTests/UnsignedNumberTests.swift +++ b/Tests/JExtractSwiftTests/UnsignedNumberTests.swift @@ -18,251 +18,392 @@ import Testing final class UnsignedNumberTests { - @Test("Import: UInt16 (char)") - func unsignedChar() throws { + @Test( + "Import: UInt16 (char)", + arguments: [ + ( + JExtractGenerationMode.ffm, + /* expected chunks */ + [ + """ + /** + * {@snippet lang=c : + * void swiftjava_SwiftModule_unsignedChar__(uint16_t arg) + * } + */ + private static class swiftjava_SwiftModule_unsignedChar__ { + private static final FunctionDescriptor DESC = FunctionDescriptor.ofVoid( + /* arg: */SwiftValueLayout.SWIFT_UINT16 + ); + """, + """ + public static void unsignedChar(@Unsigned char arg) { + swiftjava_SwiftModule_unsignedChar__.call(arg); + } + """, + ] + ), + ( + JExtractGenerationMode.jni, + /* expected chunks */ + [ + """ + public static void unsignedChar(@Unsigned char arg) { + SwiftModule.$unsignedChar(arg); + } + private static native void $unsignedChar(char arg); + """, + ] + ) + ]) + func unsignedChar(mode: JExtractGenerationMode, expectedChunks: [String]) throws { try assertOutput( input: "public func unsignedChar(_ arg: UInt16)", - .ffm, .java, + mode, .java, detectChunkByInitialLines: 2, - expectedChunks: [ - """ - /** - * {@snippet lang=c : - * void swiftjava_SwiftModule_unsignedChar__(uint16_t arg) - * } - */ - private static class swiftjava_SwiftModule_unsignedChar__ { - private static final FunctionDescriptor DESC = FunctionDescriptor.ofVoid( - /* arg: */SwiftValueLayout.SWIFT_UINT16 - ); - """, - """ - public static void unsignedChar(@Unsigned char arg) { - swiftjava_SwiftModule_unsignedChar__.call(arg); - } - """, - ] + expectedChunks: expectedChunks ) } - @Test("Import: UInt32 (wrap)") - func unsignedInt() throws { + @Test( + "Import: UInt32 (wrap)", + arguments: [ + ( + JExtractGenerationMode.ffm, + /* expected chunks */ + [ + """ + /** + * {@snippet lang=c : + * void swiftjava_SwiftModule_unsignedInt__(uint32_t arg) + * } + */ + private static class swiftjava_SwiftModule_unsignedInt__ { + private static final FunctionDescriptor DESC = FunctionDescriptor.ofVoid( + /* arg: */SwiftValueLayout.SWIFT_UINT32 + ); + """, + """ + public static void unsignedInt(com.google.common.primitives.UnsignedInteger arg) { + swiftjava_SwiftModule_unsignedInt__.call(UnsignedNumbers.toPrimitive(arg)); + } + """, + ] + ), + // JNI mode does not support the "wrap" mode + ]) + func unsignedInt_wrap(mode: JExtractGenerationMode, expectedChunks: [String]) throws { var config = Configuration() config.unsignedNumbersMode = .wrapGuava try assertOutput( input: "public func unsignedInt(_ arg: UInt32)", config: config, - .ffm, .java, + mode, .java, detectChunkByInitialLines: 2, - expectedChunks: [ - """ - /** - * {@snippet lang=c : - * void swiftjava_SwiftModule_unsignedInt__(uint32_t arg) - * } - */ - private static class swiftjava_SwiftModule_unsignedInt__ { - private static final FunctionDescriptor DESC = FunctionDescriptor.ofVoid( - /* arg: */SwiftValueLayout.SWIFT_UINT32 - ); - """, - """ - public static void unsignedInt(com.google.common.primitives.UnsignedInteger arg) { - swiftjava_SwiftModule_unsignedInt__.call(UnsignedNumbers.toPrimitive(arg)); - } - """, - ] + expectedChunks: expectedChunks ) } - @Test("Import: UInt32 (annotate)") - func unsignedIntAnnotate() throws { + @Test( + "Import: UInt32 (annotate)", + arguments: [ + ( + JExtractGenerationMode.ffm, + /* expected chunks */ + [ + """ + /** + * {@snippet lang=c : + * void swiftjava_SwiftModule_unsignedInt__(uint32_t arg) + * } + */ + private static class swiftjava_SwiftModule_unsignedInt__ { + private static final FunctionDescriptor DESC = FunctionDescriptor.ofVoid( + /* arg: */SwiftValueLayout.SWIFT_UINT32 + ); + """, + """ + public static void unsignedInt(@Unsigned int arg) { + swiftjava_SwiftModule_unsignedInt__.call(arg); + } + """, + ] + ), + ( + JExtractGenerationMode.jni, + /* expected chunks */ + [ + """ + public static void unsignedInt(@Unsigned int arg) { + SwiftModule.$unsignedInt(arg); + } + private static native void $unsignedInt(int arg); + """, + ] + ) + ]) + func unsignedIntAnnotate(mode: JExtractGenerationMode, expectedChunks: [String]) throws { var config = Configuration() config.unsignedNumbersMode = .annotate try assertOutput( input: "public func unsignedInt(_ arg: UInt32)", config: config, - .ffm, .java, + mode, .java, detectChunkByInitialLines: 2, - expectedChunks: [ - """ - /** - * {@snippet lang=c : - * void swiftjava_SwiftModule_unsignedInt__(uint32_t arg) - * } - */ - private static class swiftjava_SwiftModule_unsignedInt__ { - private static final FunctionDescriptor DESC = FunctionDescriptor.ofVoid( - /* arg: */SwiftValueLayout.SWIFT_UINT32 - ); - """, - """ - public static void unsignedInt(@Unsigned int arg) { - swiftjava_SwiftModule_unsignedInt__.call(arg); - } - """, - ] + expectedChunks: expectedChunks ) } - @Test("Import: return UInt32 (default)") - func returnUnsignedIntDefault() throws { + @Test( + "Import: return UInt32 (default)", + arguments: [ + ( + JExtractGenerationMode.ffm, + /* expected chunks */ + [ + """ + /** + * {@snippet lang=c : + * uint32_t swiftjava_SwiftModule_returnUnsignedInt(void) + * } + */ + private static class swiftjava_SwiftModule_returnUnsignedInt { + private static final FunctionDescriptor DESC = FunctionDescriptor.of( + /* -> */SwiftValueLayout.SWIFT_UINT32 + ); + """, + """ + @Unsigned + public static int returnUnsignedInt() { + return swiftjava_SwiftModule_returnUnsignedInt.call(); + } + """, + ] + ), + ( + JExtractGenerationMode.jni, + /* expected chunks */ + [ + """ + @Unsigned + public static int returnUnsignedInt() { + return SwiftModule.$returnUnsignedInt(); + } + private static native int $returnUnsignedInt(); + """, + ] + ) + ]) + func returnUnsignedIntDefault(mode: JExtractGenerationMode, expectedChunks: [String]) throws { let config = Configuration() try assertOutput( input: "public func returnUnsignedInt() -> UInt32", config: config, - .ffm, .java, + mode, .java, detectChunkByInitialLines: 2, - expectedChunks: [ - """ - /** - * {@snippet lang=c : - * uint32_t swiftjava_SwiftModule_returnUnsignedInt(void) - * } - */ - private static class swiftjava_SwiftModule_returnUnsignedInt { - private static final FunctionDescriptor DESC = FunctionDescriptor.of( - /* -> */SwiftValueLayout.SWIFT_UINT32 - ); - """, - """ - @Unsigned - public static int returnUnsignedInt() { - return swiftjava_SwiftModule_returnUnsignedInt.call(); - } - """, - ] + expectedChunks: expectedChunks ) } - @Test("Import: return UInt64 (wrap)") - func return_unsignedLongWrap() throws { + @Test( + "Import: return UInt64 (wrap)", + arguments: [ + ( + JExtractGenerationMode.ffm, + /* expected chunks */ + [ + """ + /** + * {@snippet lang=c : + * uint64_t swiftjava_SwiftModule_returnUnsignedLong(void) + * } + */ + private static class swiftjava_SwiftModule_returnUnsignedLong { + private static final FunctionDescriptor DESC = FunctionDescriptor.of( + /* -> */SwiftValueLayout.SWIFT_UINT64 + ); + """, + """ + public static com.google.common.primitives.UnsignedLong returnUnsignedLong() { + return com.google.common.primitives.UnsignedLong.fromLongBits(swiftjava_SwiftModule_returnUnsignedLong.call()); + } + """, + ] + ), + // JNI mode does not support "wrap" mode + ]) + func return_unsignedLongWrap(mode: JExtractGenerationMode, expectedChunks: [String]) throws { var config = Configuration() config.unsignedNumbersMode = .wrapGuava try assertOutput( input: "public func returnUnsignedLong() -> UInt64", config: config, - .ffm, .java, + mode, .java, detectChunkByInitialLines: 2, - expectedChunks: [ - """ - /** - * {@snippet lang=c : - * uint64_t swiftjava_SwiftModule_returnUnsignedLong(void) - * } - */ - private static class swiftjava_SwiftModule_returnUnsignedLong { - private static final FunctionDescriptor DESC = FunctionDescriptor.of( - /* -> */SwiftValueLayout.SWIFT_UINT64 - ); - """, - """ - public static com.google.common.primitives.UnsignedLong returnUnsignedLong() { - return com.google.common.primitives.UnsignedLong.fromLongBits(swiftjava_SwiftModule_returnUnsignedLong.call()); - } - """, - ] + expectedChunks: expectedChunks ) } - @Test("Import: return UInt64 (annotate)") - func return_unsignedLong_annotate() throws { + @Test( + "Import: return UInt64 (annotate)", + arguments: [ + ( + JExtractGenerationMode.ffm, + /* expected chunks */ + [ + """ + /** + * {@snippet lang=c : + * uint64_t swiftjava_SwiftModule_returnUnsignedLong(void) + * } + */ + private static class swiftjava_SwiftModule_returnUnsignedLong { + private static final FunctionDescriptor DESC = FunctionDescriptor.of( + /* -> */SwiftValueLayout.SWIFT_UINT64 + ); + """, + """ + @Unsigned + public static long returnUnsignedLong() { + return swiftjava_SwiftModule_returnUnsignedLong.call(); + } + """, + ] + ), + ( + JExtractGenerationMode.jni, + /* expected chunks */ + [ + """ + @Unsigned + public static long returnUnsignedLong() { + return SwiftModule.$returnUnsignedLong(); + } + private static native long $returnUnsignedLong(); + """, + ] + ) + ]) + func return_unsignedLong_annotate(mode: JExtractGenerationMode, expectedChunks: [String]) throws { var config = Configuration() config.unsignedNumbersMode = .annotate try assertOutput( input: "public func returnUnsignedLong() -> UInt64", config: config, - .ffm, .java, + mode, .java, detectChunkByInitialLines: 2, - expectedChunks: [ - """ - /** - * {@snippet lang=c : - * uint64_t swiftjava_SwiftModule_returnUnsignedLong(void) - * } - */ - private static class swiftjava_SwiftModule_returnUnsignedLong { - private static final FunctionDescriptor DESC = FunctionDescriptor.of( - /* -> */SwiftValueLayout.SWIFT_UINT64 - ); - """, - """ - @Unsigned - public static long returnUnsignedLong() { - return swiftjava_SwiftModule_returnUnsignedLong.call(); - } - """, - ] + expectedChunks: expectedChunks ) } - @Test("Import: take UInt64 (annotate)") - func take_unsignedLong_annotate() throws { + @Test( + "Import: take UInt64 (annotate)", + arguments: [ + ( + JExtractGenerationMode.ffm, + /* expected chunks */ + [ + """ + /** + * {@snippet lang=c : + * void swiftjava_SwiftModule_takeUnsignedLong_arg(uint64_t arg) + * } + */ + private static class swiftjava_SwiftModule_takeUnsignedLong_arg { + private static final FunctionDescriptor DESC = FunctionDescriptor.ofVoid( + /* arg: */SwiftValueLayout.SWIFT_UINT64 + ); + """, + """ + public static void takeUnsignedLong(@Unsigned long arg) { + swiftjava_SwiftModule_takeUnsignedLong_arg.call(arg); + } + """, + ] + ), + ( + JExtractGenerationMode.jni, + /* expected chunks */ + [ + """ + public static void takeUnsignedLong(@Unsigned long arg) { + SwiftModule.$takeUnsignedLong(arg); + } + private static native void $takeUnsignedLong(long arg); + """, + ] + ) + ]) + func take_unsignedLong_annotate(mode: JExtractGenerationMode, expectedChunks: [String]) throws { var config = Configuration() config.unsignedNumbersMode = .annotate try assertOutput( input: "public func takeUnsignedLong(arg: UInt64)", config: config, - .ffm, .java, + mode, .java, detectChunkByInitialLines: 2, - expectedChunks: [ - """ - /** - * {@snippet lang=c : - * void swiftjava_SwiftModule_takeUnsignedLong_arg(uint64_t arg) - * } - */ - private static class swiftjava_SwiftModule_takeUnsignedLong_arg { - private static final FunctionDescriptor DESC = FunctionDescriptor.ofVoid( - /* arg: */SwiftValueLayout.SWIFT_UINT64 - ); - """, - """ - public static void takeUnsignedLong(@Unsigned long arg) { - swiftjava_SwiftModule_takeUnsignedLong_arg.call(arg); - } - """, - ] + expectedChunks: expectedChunks ) } - @Test("Import: take UInt64 return UInt32 (annotate)") - func echo_unsignedLong_annotate() throws { + @Test( + "Import: take UInt64 return UInt32 (annotate)", + arguments: [ + ( + JExtractGenerationMode.ffm, + /* expected chunks */ + [ + """ + /** + * {@snippet lang=c : + * uint32_t swiftjava_SwiftModule_unsignedLong_first_second(uint64_t first, uint32_t second) + * } + */ + private static class swiftjava_SwiftModule_unsignedLong_first_second { + private static final FunctionDescriptor DESC = FunctionDescriptor.of( + /* -> */SwiftValueLayout.SWIFT_UINT32 + /* first: */SwiftValueLayout.SWIFT_UINT64 + /* second: */SwiftValueLayout.SWIFT_UINT32 + ); + """, + """ + @Unsigned + public static int unsignedLong(@Unsigned long first, @Unsigned int second) { + return swiftjava_SwiftModule_unsignedLong_first_second.call(first, second); + } + """, + ] + ), + ( + JExtractGenerationMode.jni, + /* expected chunks */ + [ + """ + @Unsigned + public static int unsignedLong(@Unsigned long first, @Unsigned int second) { + return SwiftModule.$unsignedLong(first, second); + } + private static native int $unsignedLong(long first, int second); + """, + ] + ), + ]) + func echo_unsignedLong_annotate(mode: JExtractGenerationMode, expectedChunks: [String]) throws { var config = Configuration() config.unsignedNumbersMode = .annotate try assertOutput( input: "public func unsignedLong(first: UInt64, second: UInt32) -> UInt32", config: config, - .ffm, .java, + mode, .java, detectChunkByInitialLines: 2, - expectedChunks: [ - """ - /** - * {@snippet lang=c : - * uint32_t swiftjava_SwiftModule_unsignedLong_first_second(uint64_t first, uint32_t second) - * } - */ - private static class swiftjava_SwiftModule_unsignedLong_first_second { - private static final FunctionDescriptor DESC = FunctionDescriptor.of( - /* -> */SwiftValueLayout.SWIFT_UINT32 - /* first: */SwiftValueLayout.SWIFT_UINT64 - /* second: */SwiftValueLayout.SWIFT_UINT32 - ); - """, - """ - @Unsigned - public static int unsignedLong(@Unsigned long first, @Unsigned int second) { - return swiftjava_SwiftModule_unsignedLong_first_second.call(first, second); - } - """, - ] + expectedChunks: expectedChunks ) } } From 670f640522e5094a3f7b94e02859b25253dc6c8d Mon Sep 17 00:00:00 2001 From: Konrad `ktoso` Malawski Date: Wed, 3 Dec 2025 16:53:24 +0900 Subject: [PATCH 10/14] Move some tests ouf of sample app into tests (#476) --- .../MySwiftLibrary/MySwiftLibrary.swift | 18 ++++++ .../com/example/swift/HelloJava2Swift.java | 4 +- .../com/example/swift/WithBufferTest.java | 60 +++++++++++++++++++ 3 files changed, 79 insertions(+), 3 deletions(-) create mode 100644 Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/WithBufferTest.java diff --git a/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftLibrary.swift b/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftLibrary.swift index cdd61c122..9929f888a 100644 --- a/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftLibrary.swift +++ b/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftLibrary.swift @@ -63,6 +63,24 @@ public func withBuffer(body: (UnsafeRawBufferPointer) -> Void) { body(globalBuffer) } +public func getArray() -> [UInt8] { + return [1, 2, 3] +} + +public func sumAllByteArrayElements(actuallyAnArray: UnsafeRawPointer, count: Int) -> Int { + let bufferPointer = UnsafeRawBufferPointer(start: actuallyAnArray, count: count) + let array = Array(bufferPointer) + return Int(array.reduce(0, { partialResult, element in partialResult + element })) +} + +public func sumAllByteArrayElements(array: [UInt8]) -> Int { + return Int(array.reduce(0, { partialResult, element in partialResult + element })) +} + +public func withArray(body: ([UInt8]) -> Void) { + body([1, 2, 3]) +} + public func globalReceiveSomeDataProtocol(data: some DataProtocol) -> Int { p(Array(data).description) return data.count diff --git a/Samples/SwiftJavaExtractFFMSampleApp/src/main/java/com/example/swift/HelloJava2Swift.java b/Samples/SwiftJavaExtractFFMSampleApp/src/main/java/com/example/swift/HelloJava2Swift.java index cecb12311..a13125478 100644 --- a/Samples/SwiftJavaExtractFFMSampleApp/src/main/java/com/example/swift/HelloJava2Swift.java +++ b/Samples/SwiftJavaExtractFFMSampleApp/src/main/java/com/example/swift/HelloJava2Swift.java @@ -52,9 +52,7 @@ static void examples() { CallTraces.trace("getGlobalBuffer().byteSize()=" + MySwiftLibrary.getGlobalBuffer().byteSize()); - MySwiftLibrary.withBuffer((buf) -> { - CallTraces.trace("withBuffer{$0.byteSize()}=" + buf.byteSize()); - }); + // Example of using an arena; MyClass.deinit is run at end of scope try (var arena = AllocatingSwiftArena.ofConfined()) { MySwiftClass obj = MySwiftClass.init(2222, 7777, arena); diff --git a/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/WithBufferTest.java b/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/WithBufferTest.java new file mode 100644 index 000000000..54206423c --- /dev/null +++ b/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/WithBufferTest.java @@ -0,0 +1,60 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +package com.example.swift; + +import org.junit.jupiter.api.Test; +import org.swift.swiftkit.core.*; +import org.swift.swiftkit.ffm.*; + +import static org.junit.jupiter.api.Assertions.*; + +import java.lang.foreign.ValueLayout; +import java.util.Arrays; +import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.IntStream; + +public class WithBufferTest { + @Test + void test_withBuffer() { + AtomicLong bufferSize = new AtomicLong(); + MySwiftLibrary.withBuffer((buf) -> { + CallTraces.trace("withBuffer{$0.byteSize()}=" + buf.byteSize()); + bufferSize.set(buf.byteSize()); + }); + + assertEquals(124, bufferSize.get()); + } + + @Test + void test_sumAllByteArrayElements_throughMemorySegment() { + byte[] bytes = new byte[124]; + Arrays.fill(bytes, (byte) 1); + + try (var arena = AllocatingSwiftArena.ofConfined()) { + // NOTE: We cannot use MemorySegment.ofArray because that creates a HEAP backed segment and therefore cannot pass into native: + // java.lang.IllegalArgumentException: Heap segment not allowed: MemorySegment{ kind: heap, heapBase: [B@5b6ec132, address: 0x0, byteSize: 124 } + // MemorySegment bytesSegment = MemorySegment.ofArray(bytes); // NO COPY (!) + // MySwiftLibrary.sumAllByteArrayElements(bytesSegment, bytes.length); + + var bytesCopy = arena.allocateFrom(ValueLayout.JAVA_BYTE, bytes); + var swiftSideSum = MySwiftLibrary.sumAllByteArrayElements(bytesCopy, bytes.length); + + System.out.println("swiftSideSum = " + swiftSideSum); + + int javaSideSum = IntStream.range(0, bytes.length).map(i -> bytes[i]).sum(); + assertEquals(javaSideSum, swiftSideSum); + } + } +} From 7519b4c2915566c4c80a3436d6b71a889fd56f10 Mon Sep 17 00:00:00 2001 From: Mads Odgaard Date: Wed, 3 Dec 2025 10:32:10 +0100 Subject: [PATCH 11/14] extract `CustomStringConvertible` as `toString()` and fix extensions (#473) * fix extensions * add runtime test * update docs * add `toString` and `toDebugString` * fix merge --- .../Sources/MySwiftLibrary/MySwiftClass.swift | 12 +++ .../com/example/swift/MySwiftClassTest.java | 16 ++++ ...t2JavaGenerator+JavaBindingsPrinting.swift | 26 +++++ ...ift2JavaGenerator+SwiftThunkPrinting.swift | 44 ++++++++- .../JExtractSwiftLib/Swift2JavaVisitor.swift | 9 +- .../SwiftTypes/SwiftTypeLookupContext.swift | 11 +++ .../Documentation.docc/SupportedFeatures.md | 2 +- .../JNI/JNIExtensionTests.swift | 65 +++++++++++++ .../JNI/JNIToStringTests.swift | 96 +++++++++++++++++++ 9 files changed, 276 insertions(+), 5 deletions(-) create mode 100644 Tests/JExtractSwiftTests/JNI/JNIExtensionTests.swift create mode 100644 Tests/JExtractSwiftTests/JNI/JNIToStringTests.swift diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftClass.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftClass.swift index e6aed287b..72f2fa357 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftClass.swift +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftClass.swift @@ -99,3 +99,15 @@ public class MySwiftClass { return self.x + other.longValue() } } + +extension MySwiftClass: CustomStringConvertible { + public var description: String { + "MySwiftClass(x: \(x), y: \(y))" + } +} + +extension MySwiftClass: CustomDebugStringConvertible { + public var debugDescription: String { + "debug: MySwiftClass(x: \(x), y: \(y))" + } +} diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/MySwiftClassTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/MySwiftClassTest.java index 860f1641c..a1f8a4529 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/MySwiftClassTest.java +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/MySwiftClassTest.java @@ -171,4 +171,20 @@ void getAsyncVariable() throws Exception { assertEquals(42, c1.getGetAsync().get()); } } + + @Test + void toStringTest() { + try (var arena = SwiftArena.ofConfined()) { + MySwiftClass c1 = MySwiftClass.init(20, 10, arena); + assertEquals("MySwiftClass(x: 20, y: 10)", c1.toString()); + } + } + + @Test + void toDebugStringTest() { + try (var arena = SwiftArena.ofConfined()) { + MySwiftClass c1 = MySwiftClass.init(20, 10, arena); + assertEquals("debug: MySwiftClass(x: 20, y: 10)", c1.toDebugString()); + } + } } \ No newline at end of file diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift index c492d439d..78cf7d418 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift @@ -260,12 +260,38 @@ extension JNISwift2JavaGenerator { printer.println() } + printToStringMethods(&printer, decl) + printer.println() + printTypeMetadataAddressFunction(&printer, decl) printer.println() printDestroyFunction(&printer, decl) } } + + private func printToStringMethods(_ printer: inout CodePrinter, _ decl: ImportedNominalType) { + printer.printBraceBlock("public String toString()") { printer in + printer.print( + """ + return $toString(this.$memoryAddress()); + """ + ) + } + printer.print("private static native java.lang.String $toString(long selfPointer);") + + printer.println() + + printer.printBraceBlock("public String toDebugString()") { printer in + printer.print( + """ + return $toDebugString(this.$memoryAddress()); + """ + ) + } + printer.print("private static native java.lang.String $toDebugString(long selfPointer);") + } + private func printHeader(_ printer: inout CodePrinter) { printer.print( """ diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift index 85fdc6404..13955430a 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift @@ -253,7 +253,7 @@ extension JNISwift2JavaGenerator { printer.println() } - + printToStringMethods(&printer, type) printTypeMetadataAddressThunk(&printer, type) printer.println() printDestroyFunctionThunk(&printer, type) @@ -267,6 +267,48 @@ extension JNISwift2JavaGenerator { try printSwiftInterfaceWrapper(&printer, protocolWrapper) } + private func printToStringMethods(_ printer: inout CodePrinter, _ type: ImportedNominalType) { + let selfPointerParam = JavaParameter(name: "selfPointer", type: .long) + let parentName = type.qualifiedName + + printCDecl( + &printer, + javaMethodName: "$toString", + parentName: type.swiftNominal.qualifiedName, + parameters: [ + selfPointerParam + ], + resultType: .javaLangString + ) { printer in + let selfVar = self.printSelfJLongToUnsafeMutablePointer(&printer, swiftParentName: parentName, selfPointerParam) + + printer.print( + """ + return String(describing: \(selfVar).pointee).getJNIValue(in: environment) + """ + ) + } + + printer.println() + + printCDecl( + &printer, + javaMethodName: "$toDebugString", + parentName: type.swiftNominal.qualifiedName, + parameters: [ + selfPointerParam + ], + resultType: .javaLangString + ) { printer in + let selfVar = self.printSelfJLongToUnsafeMutablePointer(&printer, swiftParentName: parentName, selfPointerParam) + + printer.print( + """ + return String(reflecting: \(selfVar).pointee).getJNIValue(in: environment) + """ + ) + } + } private func printEnumDiscriminator(_ printer: inout CodePrinter, _ type: ImportedNominalType) { let selfPointerParam = JavaParameter(name: "selfPointer", type: .long) diff --git a/Sources/JExtractSwiftLib/Swift2JavaVisitor.swift b/Sources/JExtractSwiftLib/Swift2JavaVisitor.swift index ab1ce32fe..cf99d11b6 100644 --- a/Sources/JExtractSwiftLib/Swift2JavaVisitor.swift +++ b/Sources/JExtractSwiftLib/Swift2JavaVisitor.swift @@ -109,6 +109,12 @@ final class Swift2JavaVisitor { guard let importedNominalType = translator.importedNominalType(node.extendedType) else { return } + + // Add any conforming protocols in the extension + importedNominalType.inheritedTypes += node.inheritanceClause?.inheritedTypes.compactMap { + try? SwiftType($0.type, lookupContext: translator.lookupContext) + } ?? [] + for memberItem in node.memberBlock.members { self.visit(decl: memberItem.decl, in: importedNominalType, sourceFilePath: sourceFilePath) } @@ -374,9 +380,6 @@ final class Swift2JavaVisitor { self.visit(decl: decl, in: imported, sourceFilePath: imported.sourceFilePath) } - // FIXME: why is this un-used - imported.variables.first?.signatureString - if !imported.initializers.contains(where: { $0.functionSignature.parameters.count == 1 && $0.functionSignature.parameters.first?.parameterName == "rawValue" diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftTypeLookupContext.swift b/Sources/JExtractSwiftLib/SwiftTypes/SwiftTypeLookupContext.swift index 33759a2cf..4489b4f65 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftTypeLookupContext.swift +++ b/Sources/JExtractSwiftLib/SwiftTypes/SwiftTypeLookupContext.swift @@ -105,6 +105,17 @@ class SwiftTypeLookupContext { typeDecl = try nominalTypeDeclaration(for: node, sourceFilePath: sourceFilePath) case .protocolDecl(let node): typeDecl = try nominalTypeDeclaration(for: node, sourceFilePath: sourceFilePath) + case .extensionDecl(let node): + // For extensions, we have to perform a unqualified lookup, + // as the extentedType is just the identifier of the type. + + guard case .identifierType(let id) = Syntax(node.extendedType).as(SyntaxEnum.self), + let lookupResult = try unqualifiedLookup(name: Identifier(id.name)!, from: node) + else { + throw TypeLookupError.notType(Syntax(node)) + } + + typeDecl = lookupResult case .typeAliasDecl: fatalError("typealias not implemented") case .associatedTypeDecl: diff --git a/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md b/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md index 849b7f01f..7161a26c9 100644 --- a/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md +++ b/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md @@ -92,7 +92,7 @@ SwiftJava's `swift-java jextract` tool automates generating Java bindings from S | Non-escaping closures with primitive arguments/results: `func callMe(maybe: (Int) -> (Double))` | ✅ | ✅ | | Non-escaping closures with object arguments/results: `func callMe(maybe: (JavaObj) -> (JavaObj))` | ❌ | ❌ | | `@escaping` closures: `func callMe(_: @escaping () -> ())` | ❌ | ❌ | -| Swift type extensions: `extension String { func uppercased() }` | 🟡 | 🟡 | +| Swift type extensions: `extension String { func uppercased() }` | ✅ | ✅ | | Swift macros (maybe) | ❌ | ❌ | | Result builders | ❌ | ❌ | | Automatic Reference Counting of class types / lifetime safety | ✅ | ✅ | diff --git a/Tests/JExtractSwiftTests/JNI/JNIExtensionTests.swift b/Tests/JExtractSwiftTests/JNI/JNIExtensionTests.swift new file mode 100644 index 000000000..e6cb10605 --- /dev/null +++ b/Tests/JExtractSwiftTests/JNI/JNIExtensionTests.swift @@ -0,0 +1,65 @@ +//===----------------------------------------------------------------------===// +// +// 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 JNIExtensionTests { + let interfaceFile = + """ + extension MyStruct { + public var variableInExtension: String { get } + public func methodInExtension() {} + } + + public protocol MyProtocol {} + public struct MyStruct {} + extension MyStruct: MyProtocol {} + """ + + @Test("Import extensions: Java methods") + func import_javaMethods() throws { + try assertOutput( + input: interfaceFile, + .jni, .java, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + public final class MyStruct implements JNISwiftInstance, MyProtocol { + ... + public void methodInExtension() { + ... + } + """ + ]) + } + + @Test("Import extensions: Computed variables") + func import_computedVariables() throws { + try assertOutput( + input: interfaceFile, + .jni, .java, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + public final class MyStruct implements JNISwiftInstance, MyProtocol { + ... + public java.lang.String getVariableInExtension() { + ... + } + """ + ]) + } +} diff --git a/Tests/JExtractSwiftTests/JNI/JNIToStringTests.swift b/Tests/JExtractSwiftTests/JNI/JNIToStringTests.swift new file mode 100644 index 000000000..1059002b6 --- /dev/null +++ b/Tests/JExtractSwiftTests/JNI/JNIToStringTests.swift @@ -0,0 +1,96 @@ +//===----------------------------------------------------------------------===// +// +// 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 +import SwiftJavaConfigurationShared + +@Suite +struct JNIToStringTests { + let source = + """ + public struct MyType {} + """ + + @Test("JNI toString (Java)") + func toString_java() throws { + try assertOutput( + input: source, + .jni, .java, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + public String toString() { + return $toString(this.$memoryAddress()); + } + """, + """ + private static native java.lang.String $toString(long selfPointer); + """ + ] + ) + } + + @Test("JNI toString (Swift)") + func toString_swift() throws { + try assertOutput( + input: source, + .jni, .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + @_cdecl("Java_com_example_swift_MyType__00024toString__J") + func Java_com_example_swift_MyType__00024toString__J(environment: UnsafeMutablePointer!, thisClass: jclass, selfPointer: jlong) -> jstring? { + ... + return String(describing: self$.pointee).getJNIValue(in: environment) + } + """, + ] + ) + } + + @Test("JNI toDebugString (Java)") + func toDebugString_java() throws { + try assertOutput( + input: source, + .jni, .java, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + public String toDebugString() { + return $toDebugString(this.$memoryAddress()); + } + """, + ] + ) + } + + @Test("JNI toDebugString (Swift)") + func toDebugString_swift() throws { + try assertOutput( + input: source, + .jni, .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + @_cdecl("Java_com_example_swift_MyType__00024toDebugString__J") + func Java_com_example_swift_MyType__00024toDebugString__J(environment: UnsafeMutablePointer!, thisClass: jclass, selfPointer: jlong) -> jstring? { + ... + return String(reflecting: self$.pointee).getJNIValue(in: environment) + } + """, + ] + ) + } +} From c6a56cd61bf11c88b003b8d34cb5ffb98db7a7eb Mon Sep 17 00:00:00 2001 From: Mads Odgaard Date: Wed, 3 Dec 2025 16:13:25 +0100 Subject: [PATCH 12/14] jextract: fix protocols that return java classes. (#479) * fix protocols * remove comments * comments * cleanup --- Package.swift | 1 + .../MySwiftLibrary/ConcreteProtocolAB.swift | 4 ++ .../Sources/MySwiftLibrary/ProtocolA.swift | 1 + .../example/swift/ProtocolCallbacksTest.java | 4 +- .../java/com/example/swift/ProtocolTest.java | 10 ++++- ...t2JavaGenerator+JavaBindingsPrinting.swift | 39 ++++++++++++------- .../Configuration.swift | 5 ++- .../JNI/JNIProtocolTests.swift | 34 ++++++++++++++-- .../MemoryManagementModeTests.swift | 2 +- 9 files changed, 78 insertions(+), 22 deletions(-) diff --git a/Package.swift b/Package.swift index d877ae696..2c56a2870 100644 --- a/Package.swift +++ b/Package.swift @@ -460,6 +460,7 @@ let package = Package( .product(name: "SwiftSyntax", package: "swift-syntax"), .product(name: "SwiftSyntaxBuilder", package: "swift-syntax"), .product(name: "ArgumentParser", package: "swift-argument-parser"), + .product(name: "OrderedCollections", package: "swift-collections"), "JavaTypes", "SwiftJavaShared", "SwiftJavaConfigurationShared", diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/ConcreteProtocolAB.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/ConcreteProtocolAB.swift index ed55d0398..d83fbb28d 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/ConcreteProtocolAB.swift +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/ConcreteProtocolAB.swift @@ -21,6 +21,10 @@ public class ConcreteProtocolAB: ProtocolA, ProtocolB { return "ConcreteProtocolAB" } + public func makeClass() -> MySwiftClass { + return MySwiftClass(x: 10, y: 50) + } + public init(constantA: Int64, constantB: Int64) { self.constantA = constantA self.constantB = constantB diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/ProtocolA.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/ProtocolA.swift index d5281b81e..6e19596f9 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/ProtocolA.swift +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/ProtocolA.swift @@ -17,6 +17,7 @@ public protocol ProtocolA { var mutable: Int64 { get set } func name() -> String + func makeClass() -> MySwiftClass } public func takeProtocol(_ proto1: any ProtocolA, _ proto2: some ProtocolA) -> Int64 { diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ProtocolCallbacksTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ProtocolCallbacksTest.java index e79fd4a3b..b4ebe8532 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ProtocolCallbacksTest.java +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ProtocolCallbacksTest.java @@ -74,7 +74,7 @@ public String withString(String input) { public void withVoid() {} @Override - public MySwiftClass withObject(MySwiftClass input) { + public MySwiftClass withObject(MySwiftClass input, SwiftArena swiftArena$) { return input; } @@ -84,7 +84,7 @@ public OptionalLong withOptionalInt64(OptionalLong input) { } @Override - public Optional withOptionalObject(Optional input) { + public Optional withOptionalObject(Optional input, SwiftArena swiftArena$) { return input; } } diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ProtocolTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ProtocolTest.java index f5d1ffcf7..b8159b8ad 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ProtocolTest.java +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ProtocolTest.java @@ -73,6 +73,14 @@ void protocolMethod() { } } + @Test + void protocolClassMethod() { + try (var arena = SwiftArena.ofConfined()) { + ProtocolA proto1 = ConcreteProtocolAB.init(10, 5, arena); + assertEquals(10, proto1.makeClass().getX()); + } + } + static class JavaStorage implements Storage { StorageItem item; @@ -81,7 +89,7 @@ static class JavaStorage implements Storage { } @Override - public StorageItem load() { + public StorageItem load(SwiftArena swiftArena$) { return item; } diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift index 78cf7d418..bfa2ff12d 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift @@ -144,17 +144,17 @@ extension JNISwift2JavaGenerator { printer.printBraceBlock("public interface \(decl.swiftNominal.name)\(extendsString)") { printer in for initializer in decl.initializers { - printFunctionDowncallMethods(&printer, initializer, skipMethodBody: true, skipArenas: true) + printFunctionDowncallMethods(&printer, initializer, skipMethodBody: true) printer.println() } for method in decl.methods { - printFunctionDowncallMethods(&printer, method, skipMethodBody: true, skipArenas: true) + printFunctionDowncallMethods(&printer, method, skipMethodBody: true) printer.println() } for variable in decl.variables { - printFunctionDowncallMethods(&printer, variable, skipMethodBody: true, skipArenas: true) + printFunctionDowncallMethods(&printer, variable, skipMethodBody: true) printer.println() } } @@ -420,7 +420,7 @@ extension JNISwift2JavaGenerator { printer.print("record _NativeParameters(\(nativeParameters.joined(separator: ", "))) {}") } - self.printJavaBindingWrapperMethod(&printer, translatedCase.getAsCaseFunction, skipMethodBody: false, skipArenas: false) + self.printJavaBindingWrapperMethod(&printer, translatedCase.getAsCaseFunction, skipMethodBody: false) printer.println() } } @@ -428,8 +428,7 @@ extension JNISwift2JavaGenerator { private func printFunctionDowncallMethods( _ printer: inout CodePrinter, _ decl: ImportedFunc, - skipMethodBody: Bool = false, - skipArenas: Bool = false + skipMethodBody: Bool = false ) { guard translatedDecl(for: decl) != nil else { // Failed to translate. Skip. @@ -440,7 +439,7 @@ extension JNISwift2JavaGenerator { printJavaBindingWrapperHelperClass(&printer, decl) - printJavaBindingWrapperMethod(&printer, decl, skipMethodBody: skipMethodBody, skipArenas: skipArenas) + printJavaBindingWrapperMethod(&printer, decl, skipMethodBody: skipMethodBody) } /// Print the helper type container for a user-facing Java API. @@ -486,21 +485,19 @@ extension JNISwift2JavaGenerator { private func printJavaBindingWrapperMethod( _ printer: inout CodePrinter, _ decl: ImportedFunc, - skipMethodBody: Bool, - skipArenas: Bool + skipMethodBody: Bool ) { guard let translatedDecl = translatedDecl(for: decl) else { fatalError("Decl was not translated, \(decl)") } - printJavaBindingWrapperMethod(&printer, translatedDecl, importedFunc: decl, skipMethodBody: skipMethodBody, skipArenas: skipArenas) + printJavaBindingWrapperMethod(&printer, translatedDecl, importedFunc: decl, skipMethodBody: skipMethodBody) } private func printJavaBindingWrapperMethod( _ printer: inout CodePrinter, _ translatedDecl: TranslatedFunctionDecl, importedFunc: ImportedFunc? = nil, - skipMethodBody: Bool, - skipArenas: Bool + skipMethodBody: Bool ) { var modifiers = ["public"] if translatedDecl.isStatic { @@ -531,14 +528,28 @@ extension JNISwift2JavaGenerator { let parametersStr = parameters.joined(separator: ", ") // Print default global arena variation + // If we have enabled javaCallbacks we must emit default + // arena methods for protocols, as this is what + // Swift will call into, when you call a interface from Swift. + let shouldGenerateGlobalArenaVariation: Bool + let isParentProtocol = importedFunc?.parentType?.asNominalType?.isProtocol ?? false + if config.effectiveMemoryManagementMode.requiresGlobalArena && translatedSignature.requiresSwiftArena { + shouldGenerateGlobalArenaVariation = true + } else if isParentProtocol, translatedSignature.requiresSwiftArena, config.effectiveEnableJavaCallbacks { + shouldGenerateGlobalArenaVariation = true + } else { + shouldGenerateGlobalArenaVariation = false + } + + if shouldGenerateGlobalArenaVariation { if let importedFunc { printDeclDocumentation(&printer, importedFunc) } var modifiers = modifiers // If we are a protocol, we emit this as default method - if importedFunc?.parentType?.asNominalTypeDeclaration?.kind == .protocol { + if isParentProtocol { modifiers.insert("default", at: 1) } @@ -555,7 +566,7 @@ extension JNISwift2JavaGenerator { printer.println() } - if translatedSignature.requiresSwiftArena, !skipArenas { + if translatedSignature.requiresSwiftArena { parameters.append("SwiftArena swiftArena$") } if let importedFunc { diff --git a/Sources/SwiftJavaConfigurationShared/Configuration.swift b/Sources/SwiftJavaConfigurationShared/Configuration.swift index 37d1e1e79..e0c40f1c5 100644 --- a/Sources/SwiftJavaConfigurationShared/Configuration.swift +++ b/Sources/SwiftJavaConfigurationShared/Configuration.swift @@ -65,7 +65,10 @@ public struct Configuration: Codable { asyncFuncMode ?? .default } - public var enableJavaCallbacks: Bool? // FIXME: default it to false, but that plays not nice with Codable + public var enableJavaCallbacks: Bool? + public var effectiveEnableJavaCallbacks: Bool { + enableJavaCallbacks ?? false + } public var generatedJavaSourcesListFileOutput: String? diff --git a/Tests/JExtractSwiftTests/JNI/JNIProtocolTests.swift b/Tests/JExtractSwiftTests/JNI/JNIProtocolTests.swift index 231c4d25d..3b47fd100 100644 --- a/Tests/JExtractSwiftTests/JNI/JNIProtocolTests.swift +++ b/Tests/JExtractSwiftTests/JNI/JNIProtocolTests.swift @@ -32,7 +32,9 @@ struct JNIProtocolTests { public protocol B {} - public class SomeClass: SomeProtocol {} + public class SomeClass: SomeProtocol { + public func makeClass() -> SomeClass {} + } public func takeProtocol(x: some SomeProtocol, y: any SomeProtocol) public func takeGeneric(s: S) @@ -61,7 +63,29 @@ struct JNIProtocolTests { ... public void method(); ... - public SomeClass withObject(SomeClass c); + public SomeClass withObject(SomeClass c, SwiftArena swiftArena$); + ... + } + """ + ]) + } + + @Test + func emitsDefault() throws { + try assertOutput( + input: source, + config: config, + .jni, .java, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + public interface SomeProtocol { + ... + public default SomeClass withObject(SomeClass c) { + return withObject(c, SwiftMemoryManagement.GLOBAL_SWIFT_JAVA_ARENA); + } + ... + public SomeClass withObject(SomeClass c, SwiftArena swiftArena$); ... } """ @@ -78,7 +102,11 @@ struct JNIProtocolTests { expectedChunks: [ """ public final class SomeClass implements JNISwiftInstance, SomeProtocol { - """ + ... + public SomeClass makeClass(SwiftArena swiftArena$) { + ... + } + """, ]) } diff --git a/Tests/JExtractSwiftTests/MemoryManagementModeTests.swift b/Tests/JExtractSwiftTests/MemoryManagementModeTests.swift index 7e78434d1..2228aad88 100644 --- a/Tests/JExtractSwiftTests/MemoryManagementModeTests.swift +++ b/Tests/JExtractSwiftTests/MemoryManagementModeTests.swift @@ -99,7 +99,7 @@ struct MemoryManagementModeTests { } """, """ - public MyClass f(); + public MyClass f(SwiftArena swiftArena$); """ ] ) From f45acdf8c1d2d78d49e1be9a034ea4e952ddd5f9 Mon Sep 17 00:00:00 2001 From: Mads Odgaard Date: Thu, 4 Dec 2025 14:03:29 +0100 Subject: [PATCH 13/14] fix test --- .../LibrarySubDirectory/SwiftTypeInSubDirectory.swift | 2 +- .../FFM/FFMSwift2JavaGenerator+SwiftThunkPrinting.swift | 2 -- Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator.swift | 4 ---- .../JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift | 1 - Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift | 4 ---- Sources/JExtractSwiftLib/Swift2Java.swift | 3 --- 6 files changed, 1 insertion(+), 15 deletions(-) diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/LibrarySubDirectory/SwiftTypeInSubDirectory.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/LibrarySubDirectory/SwiftTypeInSubDirectory.swift index 520c8c54e..96b200e83 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/LibrarySubDirectory/SwiftTypeInSubDirectory.swift +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/LibrarySubDirectory/SwiftTypeInSubDirectory.swift @@ -15,7 +15,7 @@ public final class SwiftTypeInSubDirectory { public init() {} - public func hello() -> Int { + public func hello() -> Int64 { 12 } } diff --git a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+SwiftThunkPrinting.swift b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+SwiftThunkPrinting.swift index ce70db0e8..09de17782 100644 --- a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+SwiftThunkPrinting.swift +++ b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+SwiftThunkPrinting.swift @@ -92,8 +92,6 @@ extension FFMSwift2JavaGenerator { javaPackagePath: nil, filename: filename) { log.info("Done writing Swift thunks to: \(outputFile.absoluteString)") - // log.info("REMOVE FROM: \(expectedOutputSwiftFileNames)") - // log.info("REMOVE FROM THE: \(filename)") self.expectedOutputSwiftFileNames.remove(filename) } } catch { diff --git a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator.swift b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator.swift index 332084503..a0f68ee00 100644 --- a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator.swift +++ b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator.swift @@ -64,10 +64,6 @@ package class FFMSwift2JavaGenerator: Swift2JavaGenerator { // It is sufficient to use file names only, since SwiftPM requires names to be unique within a module anyway. if translator.config.writeEmptyFiles ?? false { self.expectedOutputSwiftFileNames = Set(translator.inputs.compactMap { (input) -> String? in - // guard let filePathPart = input.path.split(separator: "/\(translator.swiftModuleName)/").last else { - // return nil - // } - // return String(filePathPart.replacing(".swift", with: "+SwiftJava.swift")) guard let fileName = input.path.split(separator: PATH_SEPARATOR).last else { return nil } diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift index da3936a7f..7964723c7 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift @@ -36,7 +36,6 @@ extension JNISwift2JavaGenerator { for expectedFileName in self.expectedOutputSwiftFileNames { logger.info("Write SwiftPM-'expected' empty file: \(expectedFileName.bold)") - var printer = CodePrinter() printer.print("// Empty file generated on purpose") _ = try printer.writeContents( diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift index 7f8a36b99..169d793d3 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift @@ -71,10 +71,6 @@ package class JNISwift2JavaGenerator: Swift2JavaGenerator { // It is sufficient to use file names only, since SwiftPM requires names to be unique within a module anyway. if translator.config.writeEmptyFiles ?? false { self.expectedOutputSwiftFileNames = Set(translator.inputs.compactMap { (input) -> String? in - // guard let filePathPart = input.path.split(separator: "/\(translator.swiftModuleName)/").last else { - // return nil - // } - // return String(filePathPart.replacing(".swift", with: "+SwiftJava.swift")) guard let fileName = input.path.split(separator: PATH_SEPARATOR).last else { return nil } diff --git a/Sources/JExtractSwiftLib/Swift2Java.swift b/Sources/JExtractSwiftLib/Swift2Java.swift index 43e168d57..30b6bd924 100644 --- a/Sources/JExtractSwiftLib/Swift2Java.swift +++ b/Sources/JExtractSwiftLib/Swift2Java.swift @@ -50,9 +50,6 @@ public struct SwiftToJava { log.info("Input paths = \(inputPaths)") let allFiles = collectAllFiles(suffix: ".swift", in: inputPaths, log: translator.log) - for f in allFiles { - log.warning("INPUT FILE: \(f) ->>>") - } // Register files to the translator. let fileManager = FileManager.default From 302391ea65d4b76813727b18b2a2cd134c29b11c Mon Sep 17 00:00:00 2001 From: Mads Odgaard Date: Thu, 4 Dec 2025 14:04:52 +0100 Subject: [PATCH 14/14] Revert "Merge branch 'main' into jextract-sub-diretories" This reverts commit 63124a0e5f34dc2d95b59a19d72efeff7333628b, reversing changes made to f45acdf8c1d2d78d49e1be9a034ea4e952ddd5f9. --- Package.swift | 4 - .../JExtractSwiftPlugin.swift | 199 +------ Plugins/SwiftJavaPlugin/SwiftJavaPlugin.swift | 4 +- README.md | 6 - .../JavaDependencySampleApp/ci-validate.sh | 11 +- Samples/JavaKitSampleApp/ci-validate.sh | 10 +- Samples/JavaProbablyPrime/ci-validate.sh | 10 +- .../MySwiftLibrary/MySwiftLibrary.swift | 18 - .../MySwiftLibrary/MySwiftStruct.swift | 22 - .../com/example/swift/HelloJava2Swift.java | 4 +- .../com/example/swift/WithBufferTest.java | 60 --- .../swift/swiftkitffm/MySwiftStructTest.java | 22 - .../MySwiftLibrary/CallbackProtcol.swift | 77 --- .../MySwiftLibrary/ConcreteProtocolAB.swift | 4 - .../Sources/MySwiftLibrary/MySwiftClass.swift | 12 - .../MySwiftLibrary/MySwiftStruct.swift | 22 - .../Sources/MySwiftLibrary/ProtocolA.swift | 1 - .../Sources/MySwiftLibrary/Storage.swift | 36 -- .../Sources/MySwiftLibrary/swift-java.config | 1 - .../SwiftJavaExtractJNISampleApp/build.gradle | 10 +- .../ci-validate.sh | 41 +- .../java/com/example/swift/AsyncTest.java | 33 +- .../com/example/swift/MySwiftClassTest.java | 20 +- .../com/example/swift/MySwiftStructTest.java | 22 - .../example/swift/ProtocolCallbacksTest.java | 117 ----- .../java/com/example/swift/ProtocolTest.java | 38 -- .../Convenience/JavaType+Extensions.swift | 2 +- .../Convenience/String+Extensions.swift | 2 +- .../Convenience/SwiftSyntax+Extensions.swift | 2 +- ...Swift2JavaGenerator+FunctionLowering.swift | 19 +- ...MSwift2JavaGenerator+JavaTranslation.swift | 4 +- Sources/JExtractSwiftLib/ImportedDecls.swift | 13 - ...Generator+InterfaceWrapperGeneration.swift | 342 ------------ ...t2JavaGenerator+JavaBindingsPrinting.swift | 109 +--- ...ISwift2JavaGenerator+JavaTranslation.swift | 134 ++--- ...wift2JavaGenerator+NativeTranslation.swift | 147 ++---- ...ift2JavaGenerator+SwiftThunkPrinting.swift | 295 +---------- .../JNI/JNISwift2JavaGenerator.swift | 8 - .../JavaTypes/JavaType+JDK.swift | 11 - .../JavaTypes/JavaType+SwiftKit.swift | 5 - .../JExtractSwiftLib/Swift2JavaVisitor.swift | 160 ++---- .../SwiftTypes/SwiftFunctionSignature.swift | 149 ++---- .../SwiftTypes/SwiftKnownTypeDecls.swift | 14 - .../SwiftTypes/SwiftType.swift | 6 +- .../SwiftTypes/SwiftTypeLookupContext.swift | 11 - .../JExtractSwiftLib/ThunkNameRegistry.swift | 4 +- .../Constructor+Utilities.swift | 5 + .../JavaLangReflect/HasJavaModifiers.swift | 47 -- .../JavaLangReflect/Method+Utilities.swift | 22 + Sources/SwiftJava/AnyJavaObject.swift | 2 +- .../SwiftJava/JVM/JavaVirtualMachine.swift | 2 +- .../SwiftJava/JavaObject+MethodCalls.swift | 50 +- Sources/SwiftJava/JavaObjectHolder.swift | 2 - Sources/SwiftJava/Macros.swift | 5 +- Sources/SwiftJava/String+Extensions.swift | 10 +- Sources/SwiftJava/generated/JavaThread.swift | 229 -------- Sources/SwiftJava/swift-java.config | 1 - .../Configuration.swift | 15 +- .../JExtract/JExtractAsyncFuncMode.swift | 2 +- .../JExtract/JExtractGenerationMode.swift | 2 +- .../Documentation.docc/SupportedFeatures.md | 4 +- .../Documentation.docc/index.md | 2 +- Sources/SwiftJavaMacros/JavaMethodMacro.swift | 18 +- ...thodIDCaches.swift => DefaultCaches.swift} | 60 +-- Sources/SwiftJavaRuntimeSupport/JNI.swift | 31 -- .../_JNIBoxedConversions.swift | 8 + .../_JNIMethodIDCache.swift | 31 +- .../generated/JavaJNISwiftInstance.swift | 21 - .../Commands/ConfigureCommand.swift | 115 ++--- .../Commands/JExtractCommand.swift | 18 +- .../Commands/WrapJavaCommand.swift | 102 +--- Sources/SwiftJavaTool/CommonOptions.swift | 5 +- .../SwiftJavaBaseAsyncParsableCommand.swift | 14 +- .../JavaClassTranslator.swift | 14 - Sources/SwiftJavaToolLib/StringExtras.swift | 5 - .../core/SimpleCompletableFuture.java | 223 -------- .../core/SimpleCompletableFutureTest.java | 185 ------- .../FFM/FFMSubscriptsTests.swift | 212 -------- .../JNI/JNIAsyncTests.swift | 92 +--- .../JExtractSwiftTests/JNI/JNIEnumTests.swift | 10 +- .../JNI/JNIExtensionTests.swift | 65 --- .../JNI/JNIProtocolTests.swift | 265 +++------- .../JNI/JNISubscriptsTests.swift | 155 ------ .../JNI/JNIToStringTests.swift | 96 ---- .../MemoryManagementModeTests.swift | 30 -- .../UnsignedNumberTests.swift | 487 +++++++----------- Tests/SwiftJavaTests/BasicRuntimeTests.swift | 11 - .../WrapJavaTests/GenericsWrapJavaTests.swift | 41 +- 88 files changed, 651 insertions(+), 4304 deletions(-) delete mode 100644 Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/WithBufferTest.java delete mode 100644 Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/CallbackProtcol.swift delete mode 100644 Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Storage.swift delete mode 100644 Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ProtocolCallbacksTest.java delete mode 100644 Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+InterfaceWrapperGeneration.swift delete mode 100644 Sources/JavaStdlib/JavaLangReflect/HasJavaModifiers.swift delete mode 100644 Sources/SwiftJava/generated/JavaThread.swift rename Sources/SwiftJavaRuntimeSupport/{JNIMethodIDCaches.swift => DefaultCaches.swift} (55%) delete mode 100644 Sources/SwiftJavaRuntimeSupport/JNI.swift delete mode 100644 Sources/SwiftJavaRuntimeSupport/generated/JavaJNISwiftInstance.swift delete mode 100644 SwiftKitCore/src/main/java/org/swift/swiftkit/core/SimpleCompletableFuture.java delete mode 100644 SwiftKitCore/src/test/java/org/swift/swiftkit/core/SimpleCompletableFutureTest.java delete mode 100644 Tests/JExtractSwiftTests/FFM/FFMSubscriptsTests.swift delete mode 100644 Tests/JExtractSwiftTests/JNI/JNIExtensionTests.swift delete mode 100644 Tests/JExtractSwiftTests/JNI/JNISubscriptsTests.swift delete mode 100644 Tests/JExtractSwiftTests/JNI/JNIToStringTests.swift diff --git a/Package.swift b/Package.swift index 2c56a2870..b4c202082 100644 --- a/Package.swift +++ b/Package.swift @@ -386,9 +386,6 @@ let package = Package( swiftSettings: [ .swiftLanguageMode(.v5), .unsafeFlags(["-I\(javaIncludePath)", "-I\(javaPlatformIncludePath)"], .when(platforms: [.macOS, .linux, .windows])) - ], - linkerSettings: [ - .linkedLibrary("log", .when(platforms: [.android])) ] ), @@ -460,7 +457,6 @@ let package = Package( .product(name: "SwiftSyntax", package: "swift-syntax"), .product(name: "SwiftSyntaxBuilder", package: "swift-syntax"), .product(name: "ArgumentParser", package: "swift-argument-parser"), - .product(name: "OrderedCollections", package: "swift-collections"), "JavaTypes", "SwiftJavaShared", "SwiftJavaConfigurationShared", diff --git a/Plugins/JExtractSwiftPlugin/JExtractSwiftPlugin.swift b/Plugins/JExtractSwiftPlugin/JExtractSwiftPlugin.swift index e000a6baa..0647e8c5f 100644 --- a/Plugins/JExtractSwiftPlugin/JExtractSwiftPlugin.swift +++ b/Plugins/JExtractSwiftPlugin/JExtractSwiftPlugin.swift @@ -25,12 +25,9 @@ struct JExtractSwiftBuildToolPlugin: SwiftJavaPluginProtocol, BuildToolPlugin { func createBuildCommands(context: PluginContext, target: Target) throws -> [Command] { let toolURL = try context.tool(named: "SwiftJavaTool").url - - var commands: [Command] = [] - + guard let sourceModule = target.sourceModule else { return [] } - // Note: Target doesn't have a directoryURL counterpart to directory, // so we cannot eliminate this deprecation warning. for dependency in target.dependencies { @@ -83,7 +80,7 @@ struct JExtractSwiftBuildToolPlugin: SwiftJavaPluginProtocol, BuildToolPlugin { let (moduleName, configFile) = moduleAndConfigFile return [ "--depends-on", - "\(moduleName)=\(configFile.path(percentEncoded: false))" + "\(configFile.path(percentEncoded: false))" ] } arguments += dependentConfigFilesArguments @@ -125,165 +122,15 @@ struct JExtractSwiftBuildToolPlugin: SwiftJavaPluginProtocol, BuildToolPlugin { print("[swift-java-plugin] Output swift files:\n - \(outputSwiftFiles.map({$0.absoluteString}).joined(separator: "\n - "))") - var jextractOutputFiles = outputSwiftFiles - - // If the developer has enabled java callbacks in the configuration (default is false) - // and we are running in JNI mode, we will run additional phases in this build plugin - // to generate Swift wrappers using wrap-java that can be used to callback to Java. - let shouldRunJavaCallbacksPhases = - if let configuration, - configuration.enableJavaCallbacks == true, - configuration.effectiveMode == .jni { - true - } else { - false - } - - // Extract list of all sources - let javaSourcesListFileName = "jextract-generated-sources.txt" - let javaSourcesFile = outputJavaDirectory.appending(path: javaSourcesListFileName) - if shouldRunJavaCallbacksPhases { - arguments += [ - "--generated-java-sources-list-file-output", javaSourcesListFileName - ] - jextractOutputFiles += [javaSourcesFile] - } - - commands += [ + return [ .buildCommand( displayName: "Generate Java wrappers for Swift types", executable: toolURL, arguments: arguments, inputFiles: [ configFile ] + swiftFiles, - outputFiles: jextractOutputFiles - ) - ] - - // If we do not need Java callbacks, we can skip the remaining steps. - guard shouldRunJavaCallbacksPhases else { - return commands - } - - // The URL of the compiled Java sources - let javaCompiledClassesURL = context.pluginWorkDirectoryURL - .appending(path: "compiled-java-output") - - // Build SwiftKitCore and get the classpath - // as the jextracted sources will depend on that - - guard let swiftJavaDirectory = findSwiftJavaDirectory(for: target) else { - fatalError("Unable to find the path to the swift-java sources, please file an issue.") - } - log("Found swift-java at \(swiftJavaDirectory)") - - let swiftKitCoreClassPath = swiftJavaDirectory.appending(path: "SwiftKitCore/build/classes/java/main") - - // We need to use a different gradle home, because - // this plugin might be run from inside another gradle task - // and that would cause conflicts. - let gradleUserHome = context.pluginWorkDirectoryURL.appending(path: "gradle-user-home") - - let GradleUserHome = "GRADLE_USER_HOME" - let gradleUserHomePath = gradleUserHome.path(percentEncoded: false) - log("Prepare command: :SwiftKitCore:build in \(GradleUserHome)=\(gradleUserHomePath)") - var gradlewEnvironment = ProcessInfo.processInfo.environment - gradlewEnvironment[GradleUserHome] = gradleUserHomePath - log("Forward environment: \(gradlewEnvironment)") - - let gradleExecutable = findExecutable(name: "gradle") ?? // try using installed 'gradle' if available in PATH - swiftJavaDirectory.appending(path: "gradlew") // fallback to calling ./gradlew if gradle is not installed - log("Detected 'gradle' executable (or gradlew fallback): \(gradleExecutable)") - - commands += [ - .buildCommand( - displayName: "Build SwiftKitCore using Gradle (Java)", - executable: gradleExecutable, - arguments: [ - ":SwiftKitCore:build", - "--project-dir", swiftJavaDirectory.path(percentEncoded: false), - "--gradle-user-home", gradleUserHomePath, - "--configure-on-demand", - "--no-daemon" - ], - environment: gradlewEnvironment, - inputFiles: [swiftJavaDirectory], - outputFiles: [swiftKitCoreClassPath] - ) - ] - - // Compile the jextracted sources - let javaHome = URL(filePath: findJavaHome()) - - commands += [ - .buildCommand( - displayName: "Build extracted Java sources", - executable: javaHome - .appending(path: "bin") - .appending(path: self.javacName), - arguments: [ - "@\(javaSourcesFile.path(percentEncoded: false))", - "-d", javaCompiledClassesURL.path(percentEncoded: false), - "-parameters", - "-classpath", swiftKitCoreClassPath.path(percentEncoded: false) - ], - inputFiles: [javaSourcesFile, swiftKitCoreClassPath], - outputFiles: [javaCompiledClassesURL] - ) - ] - - // Run `configure` to extract a swift-java config to use for wrap-java - let swiftJavaConfigURL = context.pluginWorkDirectoryURL.appending(path: "swift-java.config") - - commands += [ - .buildCommand( - displayName: "Output swift-java.config that contains all extracted Java sources", - executable: toolURL, - arguments: [ - "configure", - "--output-directory", context.pluginWorkDirectoryURL.path(percentEncoded: false), - "--cp", javaCompiledClassesURL.path(percentEncoded: false), - "--swift-module", sourceModule.name, - "--swift-type-prefix", "Java" - ], - inputFiles: [javaCompiledClassesURL], - outputFiles: [swiftJavaConfigURL] + outputFiles: outputSwiftFiles ) ] - - let singleSwiftFileOutputName = "WrapJavaGenerated.swift" - - // In the end we can run wrap-java on the previous inputs - var wrapJavaArguments = [ - "wrap-java", - "--swift-module", sourceModule.name, - "--output-directory", outputSwiftDirectory.path(percentEncoded: false), - "--config", swiftJavaConfigURL.path(percentEncoded: false), - "--cp", swiftKitCoreClassPath.path(percentEncoded: false), - "--single-swift-file-output", singleSwiftFileOutputName - ] - - // Add any dependent config files as arguments - wrapJavaArguments += dependentConfigFilesArguments - - commands += [ - .buildCommand( - displayName: "Wrap compiled Java sources using wrap-java", - executable: toolURL, - arguments: wrapJavaArguments, - inputFiles: [swiftJavaConfigURL, swiftKitCoreClassPath], - outputFiles: [outputSwiftDirectory.appending(path: singleSwiftFileOutputName)] - ) - ] - - return commands - } - - var javacName: String { -#if os(Windows) - "javac.exe" -#else - "javac" -#endif } /// Find the manifest files from other swift-java executions in any targets @@ -333,43 +180,5 @@ struct JExtractSwiftBuildToolPlugin: SwiftJavaPluginProtocol, BuildToolPlugin { return dependentConfigFiles } - - private func findSwiftJavaDirectory(for target: any Target) -> URL? { - for dependency in target.dependencies { - switch dependency { - case .target(let target): - continue - - case .product(let product): - guard let swiftJava = product.sourceModules.first(where: { $0.name == "SwiftJava" }) else { - return nil - } - - // We are inside Sources/SwiftJava - return swiftJava.directoryURL.deletingLastPathComponent().deletingLastPathComponent() - - @unknown default: - continue - } - } - - return nil - } } -func findExecutable(name: String) -> URL? { - let fileManager = FileManager.default - - guard let path = ProcessInfo.processInfo.environment["PATH"] else { - return nil - } - - for path in path.split(separator: ":") { - let fullURL = URL(fileURLWithPath: String(path)).appendingPathComponent(name) - if fileManager.isExecutableFile(atPath: fullURL.path) { - return fullURL - } - } - - return nil -} \ No newline at end of file diff --git a/Plugins/SwiftJavaPlugin/SwiftJavaPlugin.swift b/Plugins/SwiftJavaPlugin/SwiftJavaPlugin.swift index d7c773aa3..147bfc031 100644 --- a/Plugins/SwiftJavaPlugin/SwiftJavaPlugin.swift +++ b/Plugins/SwiftJavaPlugin/SwiftJavaPlugin.swift @@ -171,6 +171,8 @@ struct SwiftJavaBuildToolPlugin: SwiftJavaPluginProtocol, BuildToolPlugin { arguments += javaStdlibModules.flatMap { ["--depends-on", $0] } if !outputSwiftFiles.isEmpty { + arguments += [ configFile.path(percentEncoded: false) ] + let displayName = "Wrapping \(classes.count) Java classes in Swift target '\(sourceModule.name)'" log("Prepared: \(displayName)") @@ -268,4 +270,4 @@ func getExtractedJavaStdlibModules() -> [String] { } return url.lastPathComponent }.sorted() -} +} \ No newline at end of file diff --git a/README.md b/README.md index 7494366e2..abca3b9dd 100644 --- a/README.md +++ b/README.md @@ -34,12 +34,6 @@ Alternatively, you can use a JDK manager like [sdkman](https://sdkman.io/install $ export JAVA_HOME="$(sdk home java current)" ``` -E.g sdkman install command: - -```bash -sdk install java 25.0.1-amzn -``` - ## Self-publish supporting Java libraries Swift-java relies on supporting libraries that are under active development and not yet published to Maven Central. To use the project, you'll need to self-publish these libraries locally so your Java project can depend on them. diff --git a/Samples/JavaDependencySampleApp/ci-validate.sh b/Samples/JavaDependencySampleApp/ci-validate.sh index 1c3e2d552..feeb87675 100755 --- a/Samples/JavaDependencySampleApp/ci-validate.sh +++ b/Samples/JavaDependencySampleApp/ci-validate.sh @@ -3,16 +3,9 @@ set -e set -x -# WORKAROUND: prebuilts broken on Swift 6.2.1 and Linux and tests using macros https://github.com/swiftlang/swift-java/issues/418 -if [ "$(uname)" = "Darwin" ]; then - DISABLE_EXPERIMENTAL_PREBUILTS='' -else - DISABLE_EXPERIMENTAL_PREBUILTS='--disable-experimental-prebuilts' -fi - # invoke resolve as part of a build run swift build \ - $DISABLE_EXPERIMENTAL_PREBUILTS \ + --disable-experimental-prebuilts \ --disable-sandbox # explicitly invoke resolve without explicit path or dependency @@ -20,7 +13,7 @@ swift build \ # FIXME: until prebuilt swift-syntax isn't broken on 6.2 anymore: https://github.com/swiftlang/swift-java/issues/418 swift run \ - $DISABLE_EXPERIMENTAL_PREBUILTS \ + --disable-experimental-prebuilts \ swift-java resolve \ Sources/JavaCommonsCSV/swift-java.config \ --swift-module JavaCommonsCSV \ diff --git a/Samples/JavaKitSampleApp/ci-validate.sh b/Samples/JavaKitSampleApp/ci-validate.sh index 327baadf9..297f5c885 100755 --- a/Samples/JavaKitSampleApp/ci-validate.sh +++ b/Samples/JavaKitSampleApp/ci-validate.sh @@ -3,14 +3,8 @@ set -e set -x -# WORKAROUND: prebuilts broken on Swift 6.2.1 and Linux and tests using macros https://github.com/swiftlang/swift-java/issues/418 -if [ "$(uname)" = "Darwin" ]; then - DISABLE_EXPERIMENTAL_PREBUILTS='' -else - DISABLE_EXPERIMENTAL_PREBUILTS='--disable-experimental-prebuilts' -fi - -swift build $DISABLE_EXPERIMENTAL_PREBUILTS +swift build \ + --disable-experimental-prebuilts # FIXME: until prebuilt swift-syntax isn't broken on 6.2 anymore: https://github.com/swiftlang/swift-java/issues/418 "$JAVA_HOME/bin/java" \ -cp .build/plugins/outputs/javakitsampleapp/JavaKitExample/destination/JavaCompilerPlugin/Java \ diff --git a/Samples/JavaProbablyPrime/ci-validate.sh b/Samples/JavaProbablyPrime/ci-validate.sh index 202dcbabe..dc6249969 100755 --- a/Samples/JavaProbablyPrime/ci-validate.sh +++ b/Samples/JavaProbablyPrime/ci-validate.sh @@ -3,13 +3,7 @@ set -e set -x -# WORKAROUND: prebuilts broken on Swift 6.2.1 and Linux and tests using macros https://github.com/swiftlang/swift-java/issues/418 -if [ "$(uname)" = "Darwin" ]; then - DISABLE_EXPERIMENTAL_PREBUILTS='' -else - DISABLE_EXPERIMENTAL_PREBUILTS='--disable-experimental-prebuilts' -fi - +# FIXME: until prebuilt swift-syntax isn't broken on 6.2 anymore: https://github.com/swiftlang/swift-java/issues/418 swift run \ - $DISABLE_EXPERIMENTAL_PREBUILTS \ + --disable-experimental-prebuilts \ JavaProbablyPrime 1337 \ No newline at end of file diff --git a/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftLibrary.swift b/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftLibrary.swift index 9929f888a..cdd61c122 100644 --- a/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftLibrary.swift +++ b/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftLibrary.swift @@ -63,24 +63,6 @@ public func withBuffer(body: (UnsafeRawBufferPointer) -> Void) { body(globalBuffer) } -public func getArray() -> [UInt8] { - return [1, 2, 3] -} - -public func sumAllByteArrayElements(actuallyAnArray: UnsafeRawPointer, count: Int) -> Int { - let bufferPointer = UnsafeRawBufferPointer(start: actuallyAnArray, count: count) - let array = Array(bufferPointer) - return Int(array.reduce(0, { partialResult, element in partialResult + element })) -} - -public func sumAllByteArrayElements(array: [UInt8]) -> Int { - return Int(array.reduce(0, { partialResult, element in partialResult + element })) -} - -public func withArray(body: ([UInt8]) -> Void) { - body([1, 2, 3]) -} - public func globalReceiveSomeDataProtocol(data: some DataProtocol) -> Int { p(Array(data).description) return data.count diff --git a/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftStruct.swift b/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftStruct.swift index 5b5c2d322..363e06834 100644 --- a/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftStruct.swift +++ b/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftStruct.swift @@ -16,14 +16,10 @@ public struct MySwiftStruct { private var cap: Int private var len: Int - private var subscriptValue: Int - private var subscriptArray: [Int] public init(cap: Int, len: Int) { self.cap = cap self.len = len - self.subscriptValue = 0 - self.subscriptArray = [10, 20, 15, 75] } public func voidMethod() { @@ -65,22 +61,4 @@ public struct MySwiftStruct { public func makeRandomIntMethod() -> Int { return Int.random(in: 1..<256) } - - public func getSubscriptValue() -> Int { - return self.subscriptValue - } - - public func getSubscriptArrayValue(index: Int) -> Int { - return self.subscriptArray[index] - } - - public subscript() -> Int { - get { return subscriptValue } - set { subscriptValue = newValue } - } - - public subscript(index: Int) -> Int { - get { return subscriptArray[index] } - set { subscriptArray[index] = newValue } - } } diff --git a/Samples/SwiftJavaExtractFFMSampleApp/src/main/java/com/example/swift/HelloJava2Swift.java b/Samples/SwiftJavaExtractFFMSampleApp/src/main/java/com/example/swift/HelloJava2Swift.java index a13125478..cecb12311 100644 --- a/Samples/SwiftJavaExtractFFMSampleApp/src/main/java/com/example/swift/HelloJava2Swift.java +++ b/Samples/SwiftJavaExtractFFMSampleApp/src/main/java/com/example/swift/HelloJava2Swift.java @@ -52,7 +52,9 @@ static void examples() { CallTraces.trace("getGlobalBuffer().byteSize()=" + MySwiftLibrary.getGlobalBuffer().byteSize()); - + MySwiftLibrary.withBuffer((buf) -> { + CallTraces.trace("withBuffer{$0.byteSize()}=" + buf.byteSize()); + }); // Example of using an arena; MyClass.deinit is run at end of scope try (var arena = AllocatingSwiftArena.ofConfined()) { MySwiftClass obj = MySwiftClass.init(2222, 7777, arena); diff --git a/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/WithBufferTest.java b/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/WithBufferTest.java deleted file mode 100644 index 54206423c..000000000 --- a/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/com/example/swift/WithBufferTest.java +++ /dev/null @@ -1,60 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2025 Apple Inc. and the Swift.org project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of Swift.org project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -package com.example.swift; - -import org.junit.jupiter.api.Test; -import org.swift.swiftkit.core.*; -import org.swift.swiftkit.ffm.*; - -import static org.junit.jupiter.api.Assertions.*; - -import java.lang.foreign.ValueLayout; -import java.util.Arrays; -import java.util.concurrent.atomic.AtomicLong; -import java.util.stream.IntStream; - -public class WithBufferTest { - @Test - void test_withBuffer() { - AtomicLong bufferSize = new AtomicLong(); - MySwiftLibrary.withBuffer((buf) -> { - CallTraces.trace("withBuffer{$0.byteSize()}=" + buf.byteSize()); - bufferSize.set(buf.byteSize()); - }); - - assertEquals(124, bufferSize.get()); - } - - @Test - void test_sumAllByteArrayElements_throughMemorySegment() { - byte[] bytes = new byte[124]; - Arrays.fill(bytes, (byte) 1); - - try (var arena = AllocatingSwiftArena.ofConfined()) { - // NOTE: We cannot use MemorySegment.ofArray because that creates a HEAP backed segment and therefore cannot pass into native: - // java.lang.IllegalArgumentException: Heap segment not allowed: MemorySegment{ kind: heap, heapBase: [B@5b6ec132, address: 0x0, byteSize: 124 } - // MemorySegment bytesSegment = MemorySegment.ofArray(bytes); // NO COPY (!) - // MySwiftLibrary.sumAllByteArrayElements(bytesSegment, bytes.length); - - var bytesCopy = arena.allocateFrom(ValueLayout.JAVA_BYTE, bytes); - var swiftSideSum = MySwiftLibrary.sumAllByteArrayElements(bytesCopy, bytes.length); - - System.out.println("swiftSideSum = " + swiftSideSum); - - int javaSideSum = IntStream.range(0, bytes.length).map(i -> bytes[i]).sum(); - assertEquals(javaSideSum, swiftSideSum); - } - } -} diff --git a/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/org/swift/swiftkitffm/MySwiftStructTest.java b/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/org/swift/swiftkitffm/MySwiftStructTest.java index d904f7e82..6b994137c 100644 --- a/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/org/swift/swiftkitffm/MySwiftStructTest.java +++ b/Samples/SwiftJavaExtractFFMSampleApp/src/test/java/org/swift/swiftkitffm/MySwiftStructTest.java @@ -33,26 +33,4 @@ void create_struct() { assertEquals(len, struct.getLength()); } } - - @Test - void testSubscript() { - try (var arena = AllocatingSwiftArena.ofConfined()) { - MySwiftStruct s = MySwiftStruct.init(1337, 42, arena); - long currentValue = s.getSubscript(); - s.setSubscript(66); - assertEquals(0, currentValue); - assertEquals(66, s.getSubscriptValue()); - } - } - - @Test - void testSubscriptWithParams() { - try (var arena = AllocatingSwiftArena.ofConfined()) { - MySwiftStruct s = MySwiftStruct.init(1337, 42, arena); - long currentValue = s.getSubscript(1); - s.setSubscript(1, 66); - assertEquals(20, currentValue); - assertEquals(66, s.getSubscriptArrayValue(1)); - } - } } diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/CallbackProtcol.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/CallbackProtcol.swift deleted file mode 100644 index 985771bef..000000000 --- a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/CallbackProtcol.swift +++ /dev/null @@ -1,77 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2025 Apple Inc. and the Swift.org project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of Swift.org project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import SwiftJava - -public protocol CallbackProtocol { - func withBool(_ input: Bool) -> Bool - func withInt8(_ input: Int8) -> Int8 - func withUInt16(_ input: UInt16) -> UInt16 - func withInt16(_ input: Int16) -> Int16 - func withInt32(_ input: Int32) -> Int32 - func withInt64(_ input: Int64) -> Int64 - func withFloat(_ input: Float) -> Float - func withDouble(_ input: Double) -> Double - func withString(_ input: String) -> String - func withVoid() - func withObject(_ input: MySwiftClass) -> MySwiftClass - func withOptionalInt64(_ input: Int64?) -> Int64? - func withOptionalObject(_ input: MySwiftClass?) -> Optional -} - -public struct CallbackOutput { - public let bool: Bool - public let int8: Int8 - public let uint16: UInt16 - public let int16: Int16 - public let int32: Int32 - public let int64: Int64 - public let _float: Float - public let _double: Double - public let string: String - public let object: MySwiftClass - public let optionalInt64: Int64? - public let optionalObject: MySwiftClass? -} - -public func outputCallbacks( - _ callbacks: some CallbackProtocol, - bool: Bool, - int8: Int8, - uint16: UInt16, - int16: Int16, - int32: Int32, - int64: Int64, - _float: Float, - _double: Double, - string: String, - object: MySwiftClass, - optionalInt64: Int64?, - optionalObject: MySwiftClass? -) -> CallbackOutput { - return CallbackOutput( - bool: callbacks.withBool(bool), - int8: callbacks.withInt8(int8), - uint16: callbacks.withUInt16(uint16), - int16: callbacks.withInt16(int16), - int32: callbacks.withInt32(int32), - int64: callbacks.withInt64(int64), - _float: callbacks.withFloat(_float), - _double: callbacks.withDouble(_double), - string: callbacks.withString(string), - object: callbacks.withObject(object), - optionalInt64: callbacks.withOptionalInt64(optionalInt64), - optionalObject: callbacks.withOptionalObject(optionalObject) - ) -} diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/ConcreteProtocolAB.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/ConcreteProtocolAB.swift index d83fbb28d..ed55d0398 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/ConcreteProtocolAB.swift +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/ConcreteProtocolAB.swift @@ -21,10 +21,6 @@ public class ConcreteProtocolAB: ProtocolA, ProtocolB { return "ConcreteProtocolAB" } - public func makeClass() -> MySwiftClass { - return MySwiftClass(x: 10, y: 50) - } - public init(constantA: Int64, constantB: Int64) { self.constantA = constantA self.constantB = constantB diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftClass.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftClass.swift index 72f2fa357..e6aed287b 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftClass.swift +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftClass.swift @@ -99,15 +99,3 @@ public class MySwiftClass { return self.x + other.longValue() } } - -extension MySwiftClass: CustomStringConvertible { - public var description: String { - "MySwiftClass(x: \(x), y: \(y))" - } -} - -extension MySwiftClass: CustomDebugStringConvertible { - public var debugDescription: String { - "debug: MySwiftClass(x: \(x), y: \(y))" - } -} diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftStruct.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftStruct.swift index 34686f410..ddd77132d 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftStruct.swift +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftStruct.swift @@ -15,14 +15,10 @@ public struct MySwiftStruct { private var cap: Int64 public var len: Int64 - private var subscriptValue: Int64 - private var subscriptArray: [Int64] public init(cap: Int64, len: Int64) { self.cap = cap self.len = len - self.subscriptValue = 0 - self.subscriptArray = [10, 20, 15, 75] } public init?(doInit: Bool) { @@ -42,22 +38,4 @@ public struct MySwiftStruct { self.cap += value return self.cap } - - public func getSubscriptValue() -> Int64 { - return self.subscriptValue - } - - public func getSubscriptArrayValue(index: Int64) -> Int64 { - return self.subscriptArray[Int(index)] - } - - public subscript() -> Int64 { - get { return subscriptValue } - set { subscriptValue = newValue } - } - - public subscript(index: Int64) -> Int64 { - get { return subscriptArray[Int(index)] } - set { subscriptArray[Int(index)] = newValue } - } } diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/ProtocolA.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/ProtocolA.swift index 6e19596f9..d5281b81e 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/ProtocolA.swift +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/ProtocolA.swift @@ -17,7 +17,6 @@ public protocol ProtocolA { var mutable: Int64 { get set } func name() -> String - func makeClass() -> MySwiftClass } public func takeProtocol(_ proto1: any ProtocolA, _ proto2: some ProtocolA) -> Int64 { diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Storage.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Storage.swift deleted file mode 100644 index 488a78cc1..000000000 --- a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Storage.swift +++ /dev/null @@ -1,36 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2025 Apple Inc. and the Swift.org project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of Swift.org project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import SwiftJava - -public class StorageItem { - public let value: Int64 - - public init(value: Int64) { - self.value = value - } -} - -public protocol Storage { - func load() -> StorageItem - func save(_ item: StorageItem) -} - -public func saveWithStorage(_ item: StorageItem, s: any Storage) { - s.save(item); -} - -public func loadWithStorage(s: any Storage) -> StorageItem { - return s.load(); -} diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/swift-java.config b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/swift-java.config index 52143b7f7..3d6a12012 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/swift-java.config +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/swift-java.config @@ -1,6 +1,5 @@ { "javaPackage": "com.example.swift", "mode": "jni", - "enableJavaCallbacks": true, "logLevel": "debug" } diff --git a/Samples/SwiftJavaExtractJNISampleApp/build.gradle b/Samples/SwiftJavaExtractJNISampleApp/build.gradle index 7bb64c554..a8ce51d27 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/build.gradle +++ b/Samples/SwiftJavaExtractJNISampleApp/build.gradle @@ -101,7 +101,7 @@ def jextract = tasks.register("jextract", Exec) { } // FIXME: disable prebuilts until swift-syntax isn't broken on 6.2 anymore: https://github.com/swiftlang/swift-java/issues/418 - def cmdArgs = ["build", "--disable-experimental-prebuilts", "--disable-sandbox"] + def cmdArgs = ["build", "--disable-experimental-prebuilts"] // Check if the 'swiftSdk' project property was passed if (project.hasProperty('swiftSdk')) { @@ -209,11 +209,3 @@ jmh { "-Djextract.trace.downcalls=false" ] } - -task printGradleHome { - doLast { - println "Gradle Home: ${gradle.gradleHomeDir}" - println "Gradle Version: ${gradle.gradleVersion}" - println "Gradle User Home: ${gradle.gradleUserHomeDir}" - } -} \ No newline at end of file diff --git a/Samples/SwiftJavaExtractJNISampleApp/ci-validate.sh b/Samples/SwiftJavaExtractJNISampleApp/ci-validate.sh index d62a09102..ff5c32c80 100755 --- a/Samples/SwiftJavaExtractJNISampleApp/ci-validate.sh +++ b/Samples/SwiftJavaExtractJNISampleApp/ci-validate.sh @@ -3,44 +3,7 @@ set -x set -e -# WORKAROUND: prebuilts broken on Swift 6.2.1 and Linux and tests using macros https://github.com/swiftlang/swift-java/issues/418 -if [ "$(uname)" = "Darwin" ]; then - DISABLE_EXPERIMENTAL_PREBUILTS='' -else - DISABLE_EXPERIMENTAL_PREBUILTS='--disable-experimental-prebuilts' -fi - -if [[ "$(uname)" == "Darwin" && -n "$GITHUB_ACTION" ]]; then - # WORKAROUND: GitHub Actions on macOS issue with downloading gradle wrapper - # We seem to be hitting a problem when the swiftpm plugin, needs to execute gradle wrapper in a new gradle_user_home. - # Normally, this would just download gradle again and kick off a build, this seems to timeout *specifically* on - # github actions runners. - # - # It is not a sandbox problem, becuase the ./gradlew is run without sandboxing as we already execute - # the entire swift build with '--disable-sandbox' for other reasons. - # - # We cannot use the same gradle user home as the default one since we might make gradle think we're - # building the same project concurrently, which we kind of are, however only a limited subset in order - # to trigger wrap-java with those dependencies. - # - # TODO: this may use some further improvements so normal usage does not incur another wrapper download. - - ./gradlew -h # prime ~/.gradle/wrapper/dists/... - - # Worst part of workaround here; we make sure to pre-load the resolved gradle wrapper downloaded distribution - # to the "known" location the plugin will use for its local builds, which are done in order to compile SwiftKitCore. - # This build is only necessary in order to drive wrap-java on sources generated during the build itself - # which enables the "Implement Swift protocols in Java" feature of jextract/jni mode. - GRADLE_USER_HOME="$(pwd)/.build/plugins/outputs/swiftjavaextractjnisampleapp/MySwiftLibrary/destination/JExtractSwiftPlugin/gradle-user-home" - if [ -d "$HOME/.gradle" ] ; then - echo "COPY $HOME/.gradle to $GRADLE_USER_HOME" - mkdir -p "$GRADLE_USER_HOME" - cp -r "$HOME/.gradle/"* "$GRADLE_USER_HOME/" || true - fi -fi - -# FIXME: disable prebuilts until prebuilt swift-syntax isn't broken on 6.2 anymore: https://github.com/swiftlang/swift-java/issues/418 -swift build $DISABLE_EXPERIMENTAL_PREBUILTS --disable-sandbox +swift build --disable-experimental-prebuilts # FIXME: until prebuilt swift-syntax isn't broken on 6.2 anymore: https://github.com/swiftlang/swift-java/issues/418 ./gradlew run -./gradlew test +./gradlew test \ No newline at end of file diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/AsyncTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/AsyncTest.java index 400844fdc..5fe7c1310 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/AsyncTest.java +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/AsyncTest.java @@ -26,32 +26,31 @@ import java.util.OptionalLong; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; -import java.util.concurrent.Future; import static org.junit.jupiter.api.Assertions.*; public class AsyncTest { @Test - void asyncSum() throws Exception { - Future future = MySwiftLibrary.asyncSum(10, 12); + void asyncSum() { + CompletableFuture future = MySwiftLibrary.asyncSum(10, 12); - Long result = future.get(); + Long result = future.join(); assertEquals(22, result); } @Test - void asyncSleep() throws Exception { - Future future = MySwiftLibrary.asyncSleep(); - future.get(); + void asyncSleep() { + CompletableFuture future = MySwiftLibrary.asyncSleep(); + future.join(); } @Test - void asyncCopy() throws Exception { + void asyncCopy() { try (var arena = SwiftArena.ofConfined()) { MySwiftClass obj = MySwiftClass.init(10, 5, arena); - Future future = MySwiftLibrary.asyncCopy(obj, arena); + CompletableFuture future = MySwiftLibrary.asyncCopy(obj, arena); - MySwiftClass result = future.get(); + MySwiftClass result = future.join(); assertEquals(10, result.getX()); assertEquals(5, result.getY()); @@ -60,7 +59,7 @@ void asyncCopy() throws Exception { @Test void asyncThrows() { - Future future = MySwiftLibrary.asyncThrows(); + CompletableFuture future = MySwiftLibrary.asyncThrows(); ExecutionException ex = assertThrows(ExecutionException.class, future::get); @@ -71,14 +70,14 @@ void asyncThrows() { } @Test - void asyncOptional() throws Exception { - Future future = MySwiftLibrary.asyncOptional(42); - assertEquals(OptionalLong.of(42), future.get()); + void asyncOptional() { + CompletableFuture future = MySwiftLibrary.asyncOptional(42); + assertEquals(OptionalLong.of(42), future.join()); } @Test - void asyncString() throws Exception { - Future future = MySwiftLibrary.asyncString("hey"); - assertEquals("hey", future.get()); + void asyncString() { + CompletableFuture future = MySwiftLibrary.asyncString("hey"); + assertEquals("hey", future.join()); } } \ No newline at end of file diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/MySwiftClassTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/MySwiftClassTest.java index a1f8a4529..a58386299 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/MySwiftClassTest.java +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/MySwiftClassTest.java @@ -165,26 +165,10 @@ void addXWithJavaLong() { } @Test - void getAsyncVariable() throws Exception { + void getAsyncVariable() { try (var arena = SwiftArena.ofConfined()) { MySwiftClass c1 = MySwiftClass.init(20, 10, arena); - assertEquals(42, c1.getGetAsync().get()); - } - } - - @Test - void toStringTest() { - try (var arena = SwiftArena.ofConfined()) { - MySwiftClass c1 = MySwiftClass.init(20, 10, arena); - assertEquals("MySwiftClass(x: 20, y: 10)", c1.toString()); - } - } - - @Test - void toDebugStringTest() { - try (var arena = SwiftArena.ofConfined()) { - MySwiftClass c1 = MySwiftClass.init(20, 10, arena); - assertEquals("debug: MySwiftClass(x: 20, y: 10)", c1.toDebugString()); + assertEquals(42, c1.getGetAsync().join()); } } } \ No newline at end of file diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/MySwiftStructTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/MySwiftStructTest.java index 24b1fdbf9..e52e19591 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/MySwiftStructTest.java +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/MySwiftStructTest.java @@ -61,26 +61,4 @@ void increaseCap() { assertEquals(1347, s.getCapacity()); } } - - @Test - void testSubscript() { - try (var arena = SwiftArena.ofConfined()) { - MySwiftStruct s = MySwiftStruct.init(1337, 42, arena); - long currentValue = s.getSubscript(); - s.setSubscript(66); - assertEquals(0, currentValue); - assertEquals(66, s.getSubscriptValue()); - } - } - - @Test - void testSubscriptWithParams() { - try (var arena = SwiftArena.ofConfined()) { - MySwiftStruct s = MySwiftStruct.init(1337, 42, arena); - long currentValue = s.getSubscript(1); - s.setSubscript(1, 66); - assertEquals(20, currentValue); - assertEquals(66, s.getSubscriptArrayValue(1)); - } - } } \ No newline at end of file diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ProtocolCallbacksTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ProtocolCallbacksTest.java deleted file mode 100644 index b4ebe8532..000000000 --- a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ProtocolCallbacksTest.java +++ /dev/null @@ -1,117 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// 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.SwiftArena; -import org.swift.swiftkit.core.annotations.Unsigned; - -import java.util.Optional; -import java.util.OptionalLong; - -import static org.junit.jupiter.api.Assertions.*; - -public class ProtocolCallbacksTest { - static class JavaCallbacks implements CallbackProtocol { - @Override - public boolean withBool(boolean input) { - return input; - } - - @Override - public byte withInt8(byte input) { - return input; - } - - @Override - public @Unsigned char withUInt16(char input) { - return input; - } - - @Override - public short withInt16(short input) { - return input; - } - - @Override - public int withInt32(int input) { - return input; - } - - @Override - public long withInt64(long input) { - return input; - } - - @Override - public float withFloat(float input) { - return input; - } - - @Override - public double withDouble(double input) { - return input; - } - - @Override - public String withString(String input) { - return input; - } - - @Override - public void withVoid() {} - - @Override - public MySwiftClass withObject(MySwiftClass input, SwiftArena swiftArena$) { - return input; - } - - @Override - public OptionalLong withOptionalInt64(OptionalLong input) { - return input; - } - - @Override - public Optional withOptionalObject(Optional input, SwiftArena swiftArena$) { - return input; - } - } - - @Test - void primitiveCallbacks() { - try (var arena = SwiftArena.ofConfined()) { - JavaCallbacks callbacks = new JavaCallbacks(); - var object = MySwiftClass.init(5, 3, arena); - var optionalObject = Optional.of(MySwiftClass.init(10, 10, arena)); - var output = MySwiftLibrary.outputCallbacks(callbacks, true, (byte) 1, (char) 16, (short) 16, (int) 32, 64L, 1.34f, 1.34, "Hello from Java!", object, OptionalLong.empty(), optionalObject, arena); - - assertEquals(1, output.getInt8()); - assertEquals(16, output.getUint16()); - assertEquals(16, output.getInt16()); - assertEquals(32, output.getInt32()); - assertEquals(64, output.getInt64()); - assertEquals(1.34f, output.get_float()); - assertEquals(1.34, output.get_double()); - assertEquals("Hello from Java!", output.getString()); - assertFalse(output.getOptionalInt64().isPresent()); - assertEquals(5, output.getObject(arena).getX()); - assertEquals(3, output.getObject(arena).getY()); - - var optionalObjectOutput = output.getOptionalObject(arena); - assertTrue(optionalObjectOutput.isPresent()); - assertEquals(10, optionalObjectOutput.get().getX()); - } - } -} \ No newline at end of file diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ProtocolTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ProtocolTest.java index b8159b8ad..c095a42a4 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ProtocolTest.java +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ProtocolTest.java @@ -72,42 +72,4 @@ void protocolMethod() { assertEquals("ConcreteProtocolAB", proto1.name()); } } - - @Test - void protocolClassMethod() { - try (var arena = SwiftArena.ofConfined()) { - ProtocolA proto1 = ConcreteProtocolAB.init(10, 5, arena); - assertEquals(10, proto1.makeClass().getX()); - } - } - - static class JavaStorage implements Storage { - StorageItem item; - - JavaStorage(StorageItem item) { - this.item = item; - } - - @Override - public StorageItem load(SwiftArena swiftArena$) { - return item; - } - - @Override - public void save(StorageItem item) { - this.item = item; - } - } - - @Test - void useStorage() { - try (var arena = SwiftArena.ofConfined()) { - JavaStorage storage = new JavaStorage(null); - MySwiftLibrary.saveWithStorage(StorageItem.init(10, arena), storage); - assertEquals(10, MySwiftLibrary.loadWithStorage(storage, arena).getValue()); - MySwiftLibrary.saveWithStorage(StorageItem.init(7, arena), storage); - MySwiftLibrary.saveWithStorage(StorageItem.init(5, arena), storage); - assertEquals(5, MySwiftLibrary.loadWithStorage(storage, arena).getValue()); - } - } } \ No newline at end of file diff --git a/Sources/JExtractSwiftLib/Convenience/JavaType+Extensions.swift b/Sources/JExtractSwiftLib/Convenience/JavaType+Extensions.swift index 9f7a19cce..645e5aa48 100644 --- a/Sources/JExtractSwiftLib/Convenience/JavaType+Extensions.swift +++ b/Sources/JExtractSwiftLib/Convenience/JavaType+Extensions.swift @@ -102,7 +102,7 @@ extension JavaType { } } - /// Returns whether this type returns `JavaValue` from SwiftJava + /// Returns whether this type returns `JavaValue` from JavaKit var implementsJavaValue: Bool { return switch self { case .boolean, .byte, .char, .short, .int, .long, .float, .double, .void, .javaLangString: diff --git a/Sources/JExtractSwiftLib/Convenience/String+Extensions.swift b/Sources/JExtractSwiftLib/Convenience/String+Extensions.swift index 5641c7f23..82ce5c1c0 100644 --- a/Sources/JExtractSwiftLib/Convenience/String+Extensions.swift +++ b/Sources/JExtractSwiftLib/Convenience/String+Extensions.swift @@ -68,7 +68,7 @@ extension String { /// Looks up self as a SwiftJava wrapped class name and converts it /// into a `JavaType.class` if it exists in `lookupTable`. - func parseJavaClassFromSwiftJavaName(in lookupTable: [String: String]) -> JavaType? { + func parseJavaClassFromJavaKitName(in lookupTable: [String: String]) -> JavaType? { guard let canonicalJavaName = lookupTable[self] else { return nil } diff --git a/Sources/JExtractSwiftLib/Convenience/SwiftSyntax+Extensions.swift b/Sources/JExtractSwiftLib/Convenience/SwiftSyntax+Extensions.swift index c9f87b6dd..d3902aa4d 100644 --- a/Sources/JExtractSwiftLib/Convenience/SwiftSyntax+Extensions.swift +++ b/Sources/JExtractSwiftLib/Convenience/SwiftSyntax+Extensions.swift @@ -128,7 +128,7 @@ extension WithModifiersSyntax { } extension AttributeListSyntax.Element { - /// Whether this node has `SwiftJava` attributes. + /// Whether this node has `JavaKit` attributes. var isJava: Bool { guard case let .attribute(attr) = self else { // FIXME: Handle #if. diff --git a/Sources/JExtractSwiftLib/FFM/CDeclLowering/FFMSwift2JavaGenerator+FunctionLowering.swift b/Sources/JExtractSwiftLib/FFM/CDeclLowering/FFMSwift2JavaGenerator+FunctionLowering.swift index a7da370b3..832aff262 100644 --- a/Sources/JExtractSwiftLib/FFM/CDeclLowering/FFMSwift2JavaGenerator+FunctionLowering.swift +++ b/Sources/JExtractSwiftLib/FFM/CDeclLowering/FFMSwift2JavaGenerator+FunctionLowering.swift @@ -812,10 +812,11 @@ extension LoweredFunctionSignature { // Build callee expression. let callee: ExprSyntax = if let selfExpr { - switch apiKind { + if case .initializer = apiKind { // Don't bother to create explicit ${Self}.init expression. - case .initializer, .subscriptGetter, .subscriptSetter: selfExpr - default: ExprSyntax(MemberAccessExprSyntax(base: selfExpr, name: .identifier(swiftAPIName))) + selfExpr + } else { + ExprSyntax(MemberAccessExprSyntax(base: selfExpr, name: .identifier(swiftAPIName))) } } else { ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier(swiftAPIName))) @@ -844,18 +845,6 @@ extension LoweredFunctionSignature { case .enumCase: // This should not be called, but let's fatalError. fatalError("Enum cases are not supported with FFM.") - - case .subscriptGetter: - let parameters = paramExprs.map { $0.description }.joined(separator: ", ") - resultExpr = "\(callee)[\(raw: parameters)]" - case .subscriptSetter: - assert(paramExprs.count >= 1) - - var argumentsWithoutNewValue = paramExprs - let newValueArgument = argumentsWithoutNewValue.removeLast() - - let parameters = argumentsWithoutNewValue.map { $0.description }.joined(separator: ", ") - resultExpr = "\(callee)[\(raw: parameters)] = \(newValueArgument)" } // Lower the result. diff --git a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift index 76284b787..72da323c1 100644 --- a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift +++ b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift @@ -144,8 +144,8 @@ extension FFMSwift2JavaGenerator { // Name. let javaName = switch decl.apiKind { - case .getter, .subscriptGetter: decl.javaGetterName - case .setter, .subscriptSetter: decl.javaSetterName + case .getter: decl.javaGetterName + case .setter: decl.javaSetterName case .function, .initializer, .enumCase: decl.name } diff --git a/Sources/JExtractSwiftLib/ImportedDecls.swift b/Sources/JExtractSwiftLib/ImportedDecls.swift index aa4014d21..9ba1cd6f6 100644 --- a/Sources/JExtractSwiftLib/ImportedDecls.swift +++ b/Sources/JExtractSwiftLib/ImportedDecls.swift @@ -23,8 +23,6 @@ package enum SwiftAPIKind { case getter case setter case enumCase - case subscriptGetter - case subscriptSetter } /// Describes a Swift nominal type (e.g., a class, struct, enum) that has been @@ -181,8 +179,6 @@ public final class ImportedFunc: ImportedDecl, CustomStringConvertible { case .setter: "setter:" case .enumCase: "case:" case .function, .initializer: "" - case .subscriptGetter: "subscriptGetter:" - case .subscriptSetter: "subscriptSetter:" } let context = if let parentType { @@ -264,12 +260,3 @@ extension ImportedFunc { } } } - -extension ImportedNominalType: Hashable { - public func hash(into hasher: inout Hasher) { - hasher.combine(ObjectIdentifier(self)) - } - public static func == (lhs: ImportedNominalType, rhs: ImportedNominalType) -> Bool { - return lhs === rhs - } -} diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+InterfaceWrapperGeneration.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+InterfaceWrapperGeneration.swift deleted file mode 100644 index f1e7c6851..000000000 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+InterfaceWrapperGeneration.swift +++ /dev/null @@ -1,342 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2025 Apple Inc. and the Swift.org project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of Swift.org project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import JavaTypes -import SwiftJavaConfigurationShared -import SwiftSyntax - -extension JNISwift2JavaGenerator { - - func generateInterfaceWrappers( - _ types: [ImportedNominalType] - ) -> [ImportedNominalType: JavaInterfaceSwiftWrapper] { - var wrappers = [ImportedNominalType: JavaInterfaceSwiftWrapper]() - - for type in types { - do { - let translator = JavaInterfaceProtocolWrapperGenerator() - wrappers[type] = try translator.generate(for: type) - } catch { - self.logger.warning("Failed to generate protocol wrapper for: '\(type.swiftNominal.qualifiedName)'; \(error)") - } - } - - return wrappers - } - - /// A type that describes a Swift protocol - /// that uses an underlying wrap-java `@JavaInterface` - /// to make callbacks to Java from Swift using protocols. - struct JavaInterfaceSwiftWrapper { - let protocolType: SwiftNominalType - let functions: [Function] - let variables: [Variable] - - var wrapperName: String { - protocolType.nominalTypeDecl.javaInterfaceSwiftProtocolWrapperName - } - - var swiftName: String { - protocolType.nominalTypeDecl.qualifiedName - } - - var javaInterfaceVariableName: String { - protocolType.nominalTypeDecl.javaInterfaceVariableName - } - - var javaInterfaceName: String { - protocolType.nominalTypeDecl.javaInterfaceName - } - - struct Function { - let swiftFunctionName: String - let originalFunctionSignature: SwiftFunctionSignature - let swiftDecl: any DeclSyntaxProtocol - let parameterConversions: [UpcallConversionStep] - let resultConversion: UpcallConversionStep - } - - struct Variable { - let swiftDecl: any DeclSyntaxProtocol - let getter: Function - let setter: Function? - } - } - - - struct JavaInterfaceProtocolWrapperGenerator { - func generate(for type: ImportedNominalType) throws -> JavaInterfaceSwiftWrapper { - let functions = try type.methods.map { method in - try translate(function: method) - } - - // FIXME: Finish support for variables - if !type.variables.isEmpty { - throw JavaTranslationError.protocolVariablesNotSupported - } - - let variables = try Dictionary(grouping: type.variables, by: { $0.swiftDecl.id }).map { (id, funcs) in - precondition(funcs.count > 0 && funcs.count <= 2, "Variables must contain a getter and optionally a setter") - guard let getter = funcs.first(where: { $0.apiKind == .getter }) else { - fatalError("Getter not found for variable with imported funcs: \(funcs)") - } - let setter = funcs.first(where: { $0.apiKind == .setter }) - - return try self.translateVariable(getter: getter, setter: setter) - } - - return JavaInterfaceSwiftWrapper( - protocolType: SwiftNominalType(nominalTypeDecl: type.swiftNominal), - functions: functions, - variables: variables - ) - } - - private func translate(function: ImportedFunc) throws -> JavaInterfaceSwiftWrapper.Function { - let parameters = try function.functionSignature.parameters.map { - try self.translateParameter($0) - } - - let result = try translateResult(function.functionSignature.result, methodName: function.name) - - return JavaInterfaceSwiftWrapper.Function( - swiftFunctionName: function.name, - originalFunctionSignature: function.functionSignature, - swiftDecl: function.swiftDecl, - parameterConversions: parameters, - resultConversion: result - ) - } - - private func translateVariable(getter: ImportedFunc, setter: ImportedFunc?) throws -> JavaInterfaceSwiftWrapper.Variable { - return try JavaInterfaceSwiftWrapper.Variable( - swiftDecl: getter.swiftDecl, // they should be the same - getter: translate(function: getter), - setter: setter.map { try self.translate(function: $0) } - ) - } - - private func translateParameter(_ parameter: SwiftParameter) throws -> UpcallConversionStep { - try self.translateParameter(parameterName: parameter.parameterName!, type: parameter.type) - } - - private func translateParameter(parameterName: String, type: SwiftType) throws -> UpcallConversionStep { - - switch type { - case .nominal(let nominalType): - if let knownType = nominalType.nominalTypeDecl.knownTypeKind { - switch knownType { - case .optional: - guard let genericArgs = nominalType.genericArguments, genericArgs.count == 1 else { - throw JavaTranslationError.unsupportedSwiftType(type) - } - return try translateOptionalParameter( - name: parameterName, - wrappedType: genericArgs[0] - ) - - default: - guard knownType.isDirectlyTranslatedToWrapJava else { - throw JavaTranslationError.unsupportedSwiftType(type) - } - - return .placeholder - } - } - - // We assume this is then a JExtracted Swift class - return .toJavaWrapper( - .placeholder, - name: parameterName, - nominalType: nominalType - ) - - case .tuple([]): // void - return .placeholder - - case .optional(let wrappedType): - return try translateOptionalParameter( - name: parameterName, - wrappedType: wrappedType - ) - - case .genericParameter, .function, .metatype, .tuple, .existential, .opaque, .composite, .array: - throw JavaTranslationError.unsupportedSwiftType(type) - } - } - - private func translateOptionalParameter(name: String, wrappedType: SwiftType) throws -> UpcallConversionStep { - let wrappedConversion = try translateParameter(parameterName: name, type: wrappedType) - return .toJavaOptional(.map(.placeholder, body: wrappedConversion)) - } - - private func translateResult(_ result: SwiftResult, methodName: String) throws -> UpcallConversionStep { - try self.translateResult(type: result.type, methodName: methodName) - } - - private func translateResult( - type: SwiftType, - methodName: String, - allowNilForObjects: Bool = false - ) throws -> UpcallConversionStep { - switch type { - case .nominal(let nominalType): - if let knownType = nominalType.nominalTypeDecl.knownTypeKind { - switch knownType { - case .optional: - guard let genericArgs = nominalType.genericArguments, genericArgs.count == 1 else { - throw JavaTranslationError.unsupportedSwiftType(type) - } - return try self.translateOptionalResult( - wrappedType: genericArgs[0], - methodName: methodName - ) - - default: - guard knownType.isDirectlyTranslatedToWrapJava else { - throw JavaTranslationError.unsupportedSwiftType(type) - } - return .placeholder - } - } - - let inner: UpcallConversionStep = !allowNilForObjects ? - .unwrapOptional(.placeholder, message: "Upcall to \(methodName) unexpectedly returned nil") - : .placeholder - - // We assume this is then a JExtracted Swift class - return .toSwiftClass( - inner, - name: "result$", - nominalType: nominalType - ) - - case .tuple([]): // void - return .placeholder - - case .optional(let wrappedType): - return try self.translateOptionalResult(wrappedType: wrappedType, methodName: methodName) - - case .genericParameter, .function, .metatype, .tuple, .existential, .opaque, .composite, .array: - throw JavaTranslationError.unsupportedSwiftType(type) - } - } - - private func translateOptionalResult(wrappedType: SwiftType, methodName: String) throws -> UpcallConversionStep { - // The `fromJavaOptional` will handle the nullability - let wrappedConversion = try translateResult( - type: wrappedType, - methodName: methodName, - allowNilForObjects: true - ) - return .map(.fromJavaOptional(.placeholder), body: wrappedConversion) - } - } -} - - /// Describes how to convert values from and to wrap-java types - enum UpcallConversionStep { - case placeholder - - case constant(String) - - indirect case toJavaWrapper( - UpcallConversionStep, - name: String, - nominalType: SwiftNominalType - ) - - indirect case toSwiftClass( - UpcallConversionStep, - name: String, - nominalType: SwiftNominalType - ) - - indirect case unwrapOptional( - UpcallConversionStep, - message: String - ) - - indirect case toJavaOptional(UpcallConversionStep) - - indirect case fromJavaOptional(UpcallConversionStep) - - indirect case map(UpcallConversionStep, body: UpcallConversionStep) - - /// Returns the conversion string applied to the placeholder. - func render(_ printer: inout CodePrinter, _ placeholder: String) -> String { - switch self { - case .placeholder: - return placeholder - - case .constant(let constant): - return constant - - case .toJavaWrapper(let inner, let name, let nominalType): - let inner = inner.render(&printer, placeholder) - printer.print( - """ - let \(name)Class = try! JavaClass<\(nominalType.nominalTypeDecl.generatedJavaClassMacroName)>(environment: JavaVirtualMachine.shared().environment()) - let \(name)Pointer = UnsafeMutablePointer<\(nominalType.nominalTypeDecl.qualifiedName)>.allocate(capacity: 1) - \(name)Pointer.initialize(to: \(inner)) - """ - ) - - return "\(name)Class.wrapMemoryAddressUnsafe(Int64(Int(bitPattern: \(name)Pointer)))" - - case .toSwiftClass(let inner, let name, let nominalType): - let inner = inner.render(&printer, placeholder) - - // The wrap-java methods will return null - printer.print( - """ - let \(name)MemoryAddress$ = \(inner).as(JavaJNISwiftInstance.self)!.memoryAddress() - let \(name)Pointer = UnsafeMutablePointer<\(nominalType.nominalTypeDecl.qualifiedName)>(bitPattern: Int(\(name)MemoryAddress$))! - """ - ) - - return "\(name)Pointer.pointee" - - case .unwrapOptional(let inner, let message): - let inner = inner.render(&printer, placeholder) - - printer.print( - """ - guard let unwrapped$ = \(inner) else { - fatalError("\(message)") - } - """ - ) - - return "unwrapped$" - - case .toJavaOptional(let inner): - let inner = inner.render(&printer, placeholder) - return "\(inner).toJavaOptional()" - - case .fromJavaOptional(let inner): - let inner = inner.render(&printer, placeholder) - return "Optional(javaOptional: \(inner))" - - case .map(let inner, let body): - let inner = inner.render(&printer, placeholder) - var printer = CodePrinter() - printer.printBraceBlock("\(inner).map") { printer in - let body = body.render(&printer, "$0") - printer.print("return \(body)") - } - return printer.finalize() - } - } -} diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift index bfa2ff12d..07d23dc0f 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift @@ -12,9 +12,7 @@ // //===----------------------------------------------------------------------===// -import Foundation import JavaTypes -import OrderedCollections // MARK: Defaults @@ -42,8 +40,6 @@ extension JNISwift2JavaGenerator { package func writeExportedJavaSources(_ printer: inout CodePrinter) throws { let importedTypes = analysis.importedTypes.sorted(by: { (lhs, rhs) in lhs.key < rhs.key }) - var exportedFileNames: OrderedSet = [] - // Each parent type goes into its own file // any nested types are printed inside the body as `static class` for (_, ty) in importedTypes.filter({ _, type in type.parent == nil }) { @@ -56,7 +52,6 @@ extension JNISwift2JavaGenerator { javaPackagePath: javaPackagePath, filename: filename ) { - exportedFileNames.append(outputFile.path(percentEncoded: false)) logger.info("[swift-java] Generated: \(ty.swiftNominal.name.bold).java (at \(outputFile))") } } @@ -70,24 +65,10 @@ extension JNISwift2JavaGenerator { javaPackagePath: javaPackagePath, filename: filename ) { - exportedFileNames.append(outputFile.path(percentEncoded: false)) logger.info("[swift-java] Generated: \(self.swiftModuleName).java (at \(outputFile))") } - - // Write java sources list file - if let generatedJavaSourcesListFileOutput = config.generatedJavaSourcesListFileOutput, !exportedFileNames.isEmpty { - let outputPath = URL(fileURLWithPath: javaOutputDirectory).appending(path: generatedJavaSourcesListFileOutput) - try exportedFileNames.joined(separator: "\n").write( - to: outputPath, - atomically: true, - encoding: .utf8 - ) - logger.info("Generated file at \(outputPath)") - } } - - private func printModule(_ printer: inout CodePrinter) { printHeader(&printer) printPackage(&printer) @@ -132,29 +113,20 @@ extension JNISwift2JavaGenerator { } private func printProtocol(_ printer: inout CodePrinter, _ decl: ImportedNominalType) { - var extends = [String]() - - // If we cannot generate Swift wrappers - // that allows the user to implement the wrapped interface in Java - // then we require only JExtracted types can conform to this. - if !self.interfaceProtocolWrappers.keys.contains(decl) { - extends.append("JNISwiftInstance") - } - let extendsString = extends.isEmpty ? "" : " extends \(extends.joined(separator: ", "))" - - printer.printBraceBlock("public interface \(decl.swiftNominal.name)\(extendsString)") { printer in + let extends = ["JNISwiftInstance"] + printer.printBraceBlock("public interface \(decl.swiftNominal.name) extends \(extends.joined(separator: ", "))") { printer in for initializer in decl.initializers { - printFunctionDowncallMethods(&printer, initializer, skipMethodBody: true) + printFunctionDowncallMethods(&printer, initializer, signaturesOnly: true) printer.println() } for method in decl.methods { - printFunctionDowncallMethods(&printer, method, skipMethodBody: true) + printFunctionDowncallMethods(&printer, method, signaturesOnly: true) printer.println() } for variable in decl.variables { - printFunctionDowncallMethods(&printer, variable, skipMethodBody: true) + printFunctionDowncallMethods(&printer, variable, signaturesOnly: true) printer.println() } } @@ -212,10 +184,6 @@ extension JNISwift2JavaGenerator { public static \(decl.swiftNominal.name) wrapMemoryAddressUnsafe(long selfPointer, SwiftArena swiftArena) { return new \(decl.swiftNominal.name)(selfPointer, swiftArena); } - - public static \(decl.swiftNominal.name) wrapMemoryAddressUnsafe(long selfPointer) { - return new \(decl.swiftNominal.name)(selfPointer, SwiftMemoryManagement.GLOBAL_SWIFT_JAVA_ARENA); - } """ ) @@ -260,38 +228,12 @@ extension JNISwift2JavaGenerator { printer.println() } - printToStringMethods(&printer, decl) - printer.println() - printTypeMetadataAddressFunction(&printer, decl) printer.println() printDestroyFunction(&printer, decl) } } - - private func printToStringMethods(_ printer: inout CodePrinter, _ decl: ImportedNominalType) { - printer.printBraceBlock("public String toString()") { printer in - printer.print( - """ - return $toString(this.$memoryAddress()); - """ - ) - } - printer.print("private static native java.lang.String $toString(long selfPointer);") - - printer.println() - - printer.printBraceBlock("public String toDebugString()") { printer in - printer.print( - """ - return $toDebugString(this.$memoryAddress()); - """ - ) - } - printer.print("private static native java.lang.String $toDebugString(long selfPointer);") - } - private func printHeader(_ printer: inout CodePrinter) { printer.print( """ @@ -417,10 +359,10 @@ extension JNISwift2JavaGenerator { ["\(conversion.native.javaType) \(value.parameter.name)"] } - printer.print("record _NativeParameters(\(nativeParameters.joined(separator: ", "))) {}") + printer.print("record $NativeParameters(\(nativeParameters.joined(separator: ", "))) {}") } - self.printJavaBindingWrapperMethod(&printer, translatedCase.getAsCaseFunction, skipMethodBody: false) + self.printJavaBindingWrapperMethod(&printer, translatedCase.getAsCaseFunction, signaturesOnly: false) printer.println() } } @@ -428,7 +370,7 @@ extension JNISwift2JavaGenerator { private func printFunctionDowncallMethods( _ printer: inout CodePrinter, _ decl: ImportedFunc, - skipMethodBody: Bool = false + signaturesOnly: Bool = false ) { guard translatedDecl(for: decl) != nil else { // Failed to translate. Skip. @@ -439,7 +381,7 @@ extension JNISwift2JavaGenerator { printJavaBindingWrapperHelperClass(&printer, decl) - printJavaBindingWrapperMethod(&printer, decl, skipMethodBody: skipMethodBody) + printJavaBindingWrapperMethod(&printer, decl, signaturesOnly: signaturesOnly) } /// Print the helper type container for a user-facing Java API. @@ -482,22 +424,18 @@ extension JNISwift2JavaGenerator { ) } - private func printJavaBindingWrapperMethod( - _ printer: inout CodePrinter, - _ decl: ImportedFunc, - skipMethodBody: Bool - ) { + private func printJavaBindingWrapperMethod(_ printer: inout CodePrinter, _ decl: ImportedFunc, signaturesOnly: Bool) { guard let translatedDecl = translatedDecl(for: decl) else { fatalError("Decl was not translated, \(decl)") } - printJavaBindingWrapperMethod(&printer, translatedDecl, importedFunc: decl, skipMethodBody: skipMethodBody) + printJavaBindingWrapperMethod(&printer, translatedDecl, importedFunc: decl, signaturesOnly: signaturesOnly) } private func printJavaBindingWrapperMethod( _ printer: inout CodePrinter, _ translatedDecl: TranslatedFunctionDecl, importedFunc: ImportedFunc? = nil, - skipMethodBody: Bool + signaturesOnly: Bool ) { var modifiers = ["public"] if translatedDecl.isStatic { @@ -528,31 +466,10 @@ extension JNISwift2JavaGenerator { let parametersStr = parameters.joined(separator: ", ") // Print default global arena variation - // If we have enabled javaCallbacks we must emit default - // arena methods for protocols, as this is what - // Swift will call into, when you call a interface from Swift. - let shouldGenerateGlobalArenaVariation: Bool - let isParentProtocol = importedFunc?.parentType?.asNominalType?.isProtocol ?? false - if config.effectiveMemoryManagementMode.requiresGlobalArena && translatedSignature.requiresSwiftArena { - shouldGenerateGlobalArenaVariation = true - } else if isParentProtocol, translatedSignature.requiresSwiftArena, config.effectiveEnableJavaCallbacks { - shouldGenerateGlobalArenaVariation = true - } else { - shouldGenerateGlobalArenaVariation = false - } - - if shouldGenerateGlobalArenaVariation { if let importedFunc { printDeclDocumentation(&printer, importedFunc) } - var modifiers = modifiers - - // If we are a protocol, we emit this as default method - if isParentProtocol { - modifiers.insert("default", at: 1) - } - printer.printBraceBlock("\(annotationsStr)\(modifiers.joined(separator: " ")) \(resultType) \(translatedDecl.name)(\(parametersStr))\(throwsClause)") { printer in let globalArenaName = "SwiftMemoryManagement.GLOBAL_SWIFT_JAVA_ARENA" let arguments = translatedDecl.translatedFunctionSignature.parameters.map(\.parameter.name) + [globalArenaName] @@ -573,7 +490,7 @@ extension JNISwift2JavaGenerator { printDeclDocumentation(&printer, importedFunc) } let signature = "\(annotationsStr)\(modifiers.joined(separator: " ")) \(resultType) \(translatedDecl.name)(\(parameters.joined(separator: ", ")))\(throwsClause)" - if skipMethodBody { + if signaturesOnly { printer.print("\(signature);") } else { printer.printBraceBlock(signature) { printer in diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift index 553b3e10b..9c3cdbf1a 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift @@ -16,17 +16,6 @@ import JavaTypes import SwiftJavaConfigurationShared extension JNISwift2JavaGenerator { - var javaTranslator: JavaTranslation { - JavaTranslation( - config: config, - swiftModuleName: swiftModuleName, - javaPackage: self.javaPackage, - javaClassLookupTable: self.javaClassLookupTable, - knownTypes: SwiftKnownTypes(symbolTable: lookupContext.symbolTable), - protocolWrappers: self.interfaceProtocolWrappers - ) - } - func translatedDecl( for decl: ImportedFunc ) -> TranslatedFunctionDecl? { @@ -36,7 +25,14 @@ extension JNISwift2JavaGenerator { let translated: TranslatedFunctionDecl? do { - translated = try self.javaTranslator.translate(decl) + let translation = JavaTranslation( + config: config, + swiftModuleName: swiftModuleName, + javaPackage: self.javaPackage, + javaClassLookupTable: self.javaClassLookupTable, + knownTypes: SwiftKnownTypes(symbolTable: lookupContext.symbolTable) + ) + translated = try translation.translate(decl) } catch { self.logger.debug("Failed to translate: '\(decl.swiftDecl.qualifiedNameForDebug)'; \(error)") translated = nil @@ -60,8 +56,7 @@ extension JNISwift2JavaGenerator { swiftModuleName: swiftModuleName, javaPackage: self.javaPackage, javaClassLookupTable: self.javaClassLookupTable, - knownTypes: SwiftKnownTypes(symbolTable: lookupContext.symbolTable), - protocolWrappers: self.interfaceProtocolWrappers + knownTypes: SwiftKnownTypes(symbolTable: lookupContext.symbolTable) ) translated = try translation.translate(enumCase: decl) } catch { @@ -79,15 +74,13 @@ extension JNISwift2JavaGenerator { let javaPackage: String let javaClassLookupTable: JavaClassLookupTable var knownTypes: SwiftKnownTypes - let protocolWrappers: [ImportedNominalType: JavaInterfaceSwiftWrapper] func translate(enumCase: ImportedEnumCase) throws -> TranslatedEnumCase { let nativeTranslation = NativeJavaTranslation( config: self.config, javaPackage: self.javaPackage, javaClassLookupTable: self.javaClassLookupTable, - knownTypes: self.knownTypes, - protocolWrappers: self.protocolWrappers + knownTypes: self.knownTypes ) let methodName = "" // TODO: Used for closures, replace with better name? @@ -112,7 +105,7 @@ extension JNISwift2JavaGenerator { let caseName = enumCase.name.firstCharacterUppercased let enumName = enumCase.enumType.nominalTypeDecl.name - let nativeParametersType = JavaType.class(package: nil, name: "\(caseName)._NativeParameters") + 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: [ @@ -175,8 +168,7 @@ extension JNISwift2JavaGenerator { config: self.config, javaPackage: self.javaPackage, javaClassLookupTable: self.javaClassLookupTable, - knownTypes: self.knownTypes, - protocolWrappers: self.protocolWrappers + knownTypes: self.knownTypes ) // Types with no parent will be outputted inside a "module" class. @@ -184,8 +176,8 @@ extension JNISwift2JavaGenerator { // Name. let javaName = switch decl.apiKind { - case .getter, .subscriptGetter: decl.javaGetterName - case .setter, .subscriptSetter: decl.javaSetterName + case .getter: decl.javaGetterName + case .setter: decl.javaSetterName case .function, .initializer, .enumCase: decl.name } @@ -413,8 +405,8 @@ extension JNISwift2JavaGenerator { } } - if nominalType.isSwiftJavaWrapper { - guard let javaType = nominalTypeName.parseJavaClassFromSwiftJavaName(in: self.javaClassLookupTable) else { + if nominalType.isJavaKitWrapper { + guard let javaType = nominalTypeName.parseJavaClassFromJavaKitName(in: self.javaClassLookupTable) else { throw JavaTranslationError.wrappedJavaClassTranslationNotProvided(swiftType) } @@ -464,7 +456,7 @@ extension JNISwift2JavaGenerator { return try translateProtocolParameter( protocolType: proto, parameterName: parameterName, - javaGenericName: "_T\(parameterPosition)" + javaGenericName: "$T\(parameterPosition)" ) case .genericParameter(let generic): @@ -509,55 +501,40 @@ extension JNISwift2JavaGenerator { originalFunctionSignature: SwiftFunctionSignature, mode: JExtractAsyncFuncMode ) { - // Update translated function - let nativeFutureType: JavaType - let translatedFutureType: JavaType - let completeMethodID: String - let completeExceptionallyMethodID: String - switch mode { case .completableFuture: - nativeFutureType = .completableFuture(nativeFunctionSignature.result.javaType) - translatedFutureType = .completableFuture(translatedFunctionSignature.resultType.javaType) - completeMethodID = "_JNIMethodIDCache.CompletableFuture.complete" - completeExceptionallyMethodID = "_JNIMethodIDCache.CompletableFuture.completeExceptionally" - - case .legacyFuture: - nativeFutureType = .simpleCompletableFuture(nativeFunctionSignature.result.javaType) - translatedFutureType = .future(translatedFunctionSignature.resultType.javaType) - completeMethodID = "_JNIMethodIDCache.SimpleCompletableFuture.complete" - completeExceptionallyMethodID = "_JNIMethodIDCache.SimpleCompletableFuture.completeExceptionally" - } + // Update translated function - let futureOutParameter = OutParameter( - name: "future$", - type: nativeFutureType, - allocation: .new - ) + let nativeFutureType = JavaType.completableFuture(nativeFunctionSignature.result.javaType) + + let futureOutParameter = OutParameter( + name: "$future", + type: nativeFutureType, + allocation: .new + ) - let result = translatedFunctionSignature.resultType - translatedFunctionSignature.resultType = TranslatedResult( - javaType: translatedFutureType, - annotations: result.annotations, - outParameters: result.outParameters + [futureOutParameter], - conversion: .aggregate(variable: nil, [ - .print(.placeholder), // Make the downcall - .method(.constant("future$"), function: "thenApply", arguments: [ - .lambda(args: ["futureResult$"], body: .replacingPlaceholder(result.conversion, placeholder: "futureResult$")) + let result = translatedFunctionSignature.resultType + translatedFunctionSignature.resultType = TranslatedResult( + javaType: .completableFuture(translatedFunctionSignature.resultType.javaType), + annotations: result.annotations, + outParameters: result.outParameters + [futureOutParameter], + conversion: .aggregate(variable: nil, [ + .print(.placeholder), // Make the downcall + .method(.constant("$future"), function: "thenApply", arguments: [ + .lambda(args: ["futureResult$"], body: .replacingPlaceholder(result.conversion, placeholder: "futureResult$")) + ]) ]) - ]) - ) + ) - // Update native function - nativeFunctionSignature.result.conversion = .asyncCompleteFuture( - swiftFunctionResultType: originalFunctionSignature.result.type, - nativeFunctionSignature: nativeFunctionSignature, - isThrowing: originalFunctionSignature.isThrowing, - completeMethodID: completeMethodID, - completeExceptionallyMethodID: completeExceptionallyMethodID - ) - nativeFunctionSignature.result.javaType = .void - nativeFunctionSignature.result.outParameters.append(.init(name: "result_future", type: nativeFutureType)) + // Update native function + nativeFunctionSignature.result.conversion = .asyncCompleteFuture( + swiftFunctionResultType: originalFunctionSignature.result.type, + nativeFunctionSignature: nativeFunctionSignature, + isThrowing: originalFunctionSignature.isThrowing + ) + nativeFunctionSignature.result.javaType = .void + nativeFunctionSignature.result.outParameters.append(.init(name: "result_future", type: nativeFutureType)) + } } func translateProtocolParameter( @@ -593,14 +570,14 @@ extension JNISwift2JavaGenerator { } } - // We just pass down the jobject + // We assume this is a JExtract class. return TranslatedParameter( parameter: JavaParameter( name: parameterName, type: .generic(name: javaGenericName, extends: javaProtocolTypes), annotations: [] ), - conversion: .placeholder + conversion: .commaSeparated([.valueMemoryAddress(.placeholder), .typeMetadataAddress(.placeholder)]) ) } @@ -636,8 +613,8 @@ extension JNISwift2JavaGenerator { ) } - if nominalType.isSwiftJavaWrapper { - guard let javaType = nominalTypeName.parseJavaClassFromSwiftJavaName(in: self.javaClassLookupTable) else { + if nominalType.isJavaKitWrapper { + guard let javaType = nominalTypeName.parseJavaClassFromJavaKitName(in: self.javaClassLookupTable) else { throw JavaTranslationError.wrappedJavaClassTranslationNotProvided(swiftType) } @@ -711,7 +688,7 @@ extension JNISwift2JavaGenerator { } } - if nominalType.isSwiftJavaWrapper { + if nominalType.isJavaKitWrapper { throw JavaTranslationError.unsupportedSwiftType(swiftType) } @@ -797,7 +774,7 @@ extension JNISwift2JavaGenerator { } } - guard !nominalType.isSwiftJavaWrapper else { + guard !nominalType.isJavaKitWrapper else { throw JavaTranslationError.unsupportedSwiftType(swiftType) } @@ -844,7 +821,7 @@ extension JNISwift2JavaGenerator { ) } - guard !nominalType.isSwiftJavaWrapper else { + guard !nominalType.isJavaKitWrapper else { throw JavaTranslationError.unsupportedSwiftType(elementType) } @@ -893,7 +870,7 @@ extension JNISwift2JavaGenerator { ) } - guard !nominalType.isSwiftJavaWrapper else { + guard !nominalType.isJavaKitWrapper else { throw JavaTranslationError.unsupportedSwiftType(elementType) } @@ -973,7 +950,7 @@ extension JNISwift2JavaGenerator { /// Function signature of the native function that will be implemented by Swift let nativeFunctionSignature: NativeFunctionSignature - + /// Annotations to include on the Java function declaration var annotations: [JavaAnnotation] { self.translatedFunctionSignature.annotations @@ -1340,8 +1317,5 @@ extension JNISwift2JavaGenerator { /// The user has not supplied a mapping from `SwiftType` to /// a java class. case wrappedJavaClassTranslationNotProvided(SwiftType) - - // FIXME: Remove once we support protocol variables - case protocolVariablesNotSupported } } diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift index b744a33b4..1994dce04 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift @@ -22,7 +22,6 @@ extension JNISwift2JavaGenerator { let javaPackage: String let javaClassLookupTable: JavaClassLookupTable var knownTypes: SwiftKnownTypes - let protocolWrappers: [ImportedNominalType: JavaInterfaceSwiftWrapper] /// Translates a Swift function into the native JNI method signature. func translate( @@ -114,8 +113,8 @@ extension JNISwift2JavaGenerator { } } - if nominalType.isSwiftJavaWrapper { - guard let javaType = nominalTypeName.parseJavaClassFromSwiftJavaName(in: self.javaClassLookupTable) else { + if nominalType.isJavaKitWrapper { + guard let javaType = nominalTypeName.parseJavaClassFromJavaKitName(in: self.javaClassLookupTable) else { throw JavaTranslationError.wrappedJavaClassTranslationNotProvided(type) } @@ -123,7 +122,7 @@ extension JNISwift2JavaGenerator { parameters: [ JavaParameter(name: parameterName, type: javaType) ], - conversion: .initializeSwiftJavaWrapper( + conversion: .initializeJavaKitWrapper( .unwrapOptional( .placeholder, name: parameterName, @@ -182,18 +181,14 @@ extension JNISwift2JavaGenerator { case .opaque(let proto), .existential(let proto): return try translateProtocolParameter( protocolType: proto, - methodName: methodName, - parameterName: parameterName, - parentName: parentName + parameterName: parameterName ) case .genericParameter: if let concreteTy = type.typeIn(genericParameters: genericParameters, genericRequirements: genericRequirements) { return try translateProtocolParameter( protocolType: concreteTy, - methodName: methodName, - parameterName: parameterName, - parentName: parentName + parameterName: parameterName ) } @@ -212,23 +207,22 @@ extension JNISwift2JavaGenerator { func translateProtocolParameter( protocolType: SwiftType, - methodName: String, - parameterName: String, - parentName: String? + parameterName: String ) throws -> NativeParameter { switch protocolType { case .nominal(let nominalType): - return try translateProtocolParameter(protocolTypes: [nominalType], methodName: methodName, parameterName: parameterName, parentName: parentName) + let protocolName = nominalType.nominalTypeDecl.qualifiedName + return try translateProtocolParameter(protocolNames: [protocolName], parameterName: parameterName) case .composite(let types): - let protocolTypes = try types.map { - guard let nominalTypeName = $0.asNominalType else { + let protocolNames = try types.map { + guard let nominalTypeName = $0.asNominalType?.nominalTypeDecl.qualifiedName else { throw JavaTranslationError.unsupportedSwiftType($0) } return nominalTypeName } - return try translateProtocolParameter(protocolTypes: protocolTypes, methodName: methodName, parameterName: parameterName, parentName: parentName) + return try translateProtocolParameter(protocolNames: protocolNames, parameterName: parameterName) default: throw JavaTranslationError.unsupportedSwiftType(protocolType) @@ -236,30 +230,18 @@ extension JNISwift2JavaGenerator { } private func translateProtocolParameter( - protocolTypes: [SwiftNominalType], - methodName: String, - parameterName: String, - parentName: String? + protocolNames: [String], + parameterName: String ) throws -> NativeParameter { - // We allow Java implementations if we are able to generate the needed - // Swift wrappers for all the protocol types. - let allowsJavaImplementations = protocolTypes.allSatisfy { protocolType in - self.protocolWrappers.contains(where: { $0.value.protocolType == protocolType }) - } - return NativeParameter( parameters: [ - JavaParameter(name: parameterName, type: .javaLangObject) + JavaParameter(name: parameterName, type: .long), + JavaParameter(name: "\(parameterName)_typeMetadataAddress", type: .long) ], - conversion: .interfaceToSwiftObject( + conversion: .extractSwiftProtocolValue( .placeholder, - swiftWrapperClassName: JNISwift2JavaGenerator.protocolParameterWrapperClassName( - methodName: methodName, - parameterName: parameterName, - parentName: parentName - ), - protocolTypes: protocolTypes, - allowsJavaImplementations: allowsJavaImplementations + typeMetadataVariableName: .combinedName(component: "typeMetadataAddress"), + protocolNames: protocolNames ) ) } @@ -294,8 +276,8 @@ extension JNISwift2JavaGenerator { ) } - if nominalType.isSwiftJavaWrapper { - guard let javaType = nominalTypeName.parseJavaClassFromSwiftJavaName(in: self.javaClassLookupTable) else { + if nominalType.isJavaKitWrapper { + guard let javaType = nominalTypeName.parseJavaClassFromJavaKitName(in: self.javaClassLookupTable) else { throw JavaTranslationError.wrappedJavaClassTranslationNotProvided(swiftType) } @@ -303,7 +285,7 @@ extension JNISwift2JavaGenerator { parameters: [ JavaParameter(name: parameterName, type: javaType) ], - conversion: .optionalMap(.initializeSwiftJavaWrapper(.placeholder, wrapperName: nominalTypeName)) + conversion: .optionalMap(.initializeJavaKitWrapper(.placeholder, wrapperName: nominalTypeName)) ) } @@ -377,7 +359,7 @@ extension JNISwift2JavaGenerator { } } - guard !nominalType.isSwiftJavaWrapper else { + guard !nominalType.isJavaKitWrapper else { // TODO: Should be the same as above throw JavaTranslationError.unsupportedSwiftType(swiftType) } @@ -497,7 +479,7 @@ extension JNISwift2JavaGenerator { } } - if nominalType.isSwiftJavaWrapper { + if nominalType.isJavaKitWrapper { throw JavaTranslationError.unsupportedSwiftType(swiftResult.type) } @@ -544,7 +526,7 @@ extension JNISwift2JavaGenerator { ) } - guard !nominalType.isSwiftJavaWrapper else { + guard !nominalType.isJavaKitWrapper else { throw JavaTranslationError.unsupportedSwiftType(.array(elementType)) } @@ -592,7 +574,7 @@ extension JNISwift2JavaGenerator { ) } - guard !nominalType.isSwiftJavaWrapper else { + guard !nominalType.isJavaKitWrapper else { throw JavaTranslationError.unsupportedSwiftType(.array(elementType)) } @@ -663,13 +645,6 @@ extension JNISwift2JavaGenerator { /// `SwiftType(from: value, in: environment)` indirect case initFromJNI(NativeSwiftConversionStep, swiftType: SwiftType) - indirect case interfaceToSwiftObject( - NativeSwiftConversionStep, - swiftWrapperClassName: String, - protocolTypes: [SwiftNominalType], - allowsJavaImplementations: Bool - ) - indirect case extractSwiftProtocolValue( NativeSwiftConversionStep, typeMetadataVariableName: NativeSwiftConversionStep, @@ -693,7 +668,7 @@ extension JNISwift2JavaGenerator { indirect case closureLowering(parameters: [NativeParameter], result: NativeResult) - indirect case initializeSwiftJavaWrapper(NativeSwiftConversionStep, wrapperName: String) + indirect case initializeJavaKitWrapper(NativeSwiftConversionStep, wrapperName: String) indirect case optionalLowering(NativeSwiftConversionStep, discriminatorName: String, valueName: String) @@ -714,9 +689,7 @@ extension JNISwift2JavaGenerator { indirect case asyncCompleteFuture( swiftFunctionResultType: SwiftType, nativeFunctionSignature: NativeFunctionSignature, - isThrowing: Bool, - completeMethodID: String, - completeExceptionallyMethodID: String + isThrowing: Bool ) /// `{ (args) -> return body }` @@ -748,60 +721,6 @@ extension JNISwift2JavaGenerator { let inner = inner.render(&printer, placeholder) return "\(swiftType)(fromJNI: \(inner), in: environment)" - case .interfaceToSwiftObject( - let inner, - let swiftWrapperClassName, - let protocolTypes, - let allowsJavaImplementations - ): - let protocolNames = protocolTypes.map { $0.nominalTypeDecl.qualifiedName } - - let inner = inner.render(&printer, placeholder) - let variableName = "\(inner)swiftObject$" - let compositeProtocolName = "(\(protocolNames.joined(separator: " & ")))" - printer.print("let \(variableName): \(compositeProtocolName)") - - func printStandardJExtractBlock(_ printer: inout CodePrinter) { - let pointerVariableName = "\(inner)pointer$" - let typeMetadataVariableName = "\(inner)typeMetadata$" - printer.print( - """ - let \(pointerVariableName) = environment.interface.CallLongMethodA(environment, \(inner), _JNIMethodIDCache.JNISwiftInstance.memoryAddress, []) - let \(typeMetadataVariableName) = environment.interface.CallLongMethodA(environment, \(inner), _JNIMethodIDCache.JNISwiftInstance.typeMetadataAddress, []) - """ - ) - let existentialName = NativeSwiftConversionStep.extractSwiftProtocolValue( - .constant(pointerVariableName), - typeMetadataVariableName: .constant(typeMetadataVariableName), - protocolNames: protocolNames - ).render(&printer, placeholder) - - printer.print("\(variableName) = \(existentialName)") - } - - // If this protocol type supports being implemented by the user - // then we will check whether it is a JNI SwiftInstance type - // or if its a custom class implementing the interface. - if allowsJavaImplementations { - printer.printBraceBlock( - "if environment.interface.IsInstanceOf(environment, \(inner), _JNIMethodIDCache.JNISwiftInstance.class) != 0" - ) { printer in - printStandardJExtractBlock(&printer) - } - printer.printBraceBlock("else") { printer in - let arguments = protocolTypes.map { protocolType in - let nominalTypeDecl = protocolType.nominalTypeDecl - return "\(nominalTypeDecl.javaInterfaceVariableName): \(nominalTypeDecl.javaInterfaceName)(javaThis: \(inner)!, environment: environment)" - } - printer.print("\(variableName) = \(swiftWrapperClassName)(\(arguments.joined(separator: ", ")))") - } - } else { - printStandardJExtractBlock(&printer) - } - - - return variableName - case .extractSwiftProtocolValue(let inner, let typeMetadataVariableName, let protocolNames): let inner = inner.render(&printer, placeholder) let typeMetadataVariableName = typeMetadataVariableName.render(&printer, placeholder) @@ -918,7 +837,7 @@ extension JNISwift2JavaGenerator { return printer.finalize() - case .initializeSwiftJavaWrapper(let inner, let wrapperName): + case .initializeJavaKitWrapper(let inner, let wrapperName): let inner = inner.render(&printer, placeholder) return "\(wrapperName)(javaThis: \(inner), environment: environment)" @@ -1008,9 +927,7 @@ extension JNISwift2JavaGenerator { case .asyncCompleteFuture( let swiftFunctionResultType, let nativeFunctionSignature, - let isThrowing, - let completeMethodID, - let completeExceptionallyMethodID + let isThrowing ): var globalRefs: [String] = ["globalFuture"] @@ -1037,7 +954,7 @@ extension JNISwift2JavaGenerator { printer.print("environment = try! JavaVirtualMachine.shared().environment()") let inner = nativeFunctionSignature.result.conversion.render(&printer, "swiftResult$") if swiftFunctionResultType.isVoid { - printer.print("environment.interface.CallBooleanMethodA(environment, globalFuture, \(completeMethodID), [jvalue(l: nil)])") + printer.print("environment.interface.CallBooleanMethodA(environment, globalFuture, _JNIMethodIDCache.CompletableFuture.complete, [jvalue(l: nil)])") } else { let result: String if nativeFunctionSignature.result.javaType.requiresBoxing { @@ -1047,7 +964,7 @@ extension JNISwift2JavaGenerator { result = inner } - printer.print("environment.interface.CallBooleanMethodA(environment, globalFuture, \(completeMethodID), [jvalue(l: \(result))])") + printer.print("environment.interface.CallBooleanMethodA(environment, globalFuture, _JNIMethodIDCache.CompletableFuture.complete, [jvalue(l: \(result))])") } } @@ -1069,7 +986,7 @@ extension JNISwift2JavaGenerator { """ let catchEnvironment = try! JavaVirtualMachine.shared().environment() let exception = catchEnvironment.interface.NewObjectA(catchEnvironment, _JNIMethodIDCache.Exception.class, _JNIMethodIDCache.Exception.constructWithMessage, [String(describing: error).getJValue(in: catchEnvironment)]) - catchEnvironment.interface.CallBooleanMethodA(catchEnvironment, globalFuture, \(completeExceptionallyMethodID), [jvalue(l: exception)]) + catchEnvironment.interface.CallBooleanMethodA(catchEnvironment, globalFuture, _JNIMethodIDCache.CompletableFuture.completeExceptionally, [jvalue(l: exception)]) """ ) } diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift index 648c340be..7964723c7 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift @@ -115,80 +115,9 @@ extension JNISwift2JavaGenerator { } } - /// Prints the extension needed to make allow upcalls from Swift to Java for protocols - private func printSwiftInterfaceWrapper( - _ printer: inout CodePrinter, - _ translatedWrapper: JavaInterfaceSwiftWrapper - ) throws { - printer.printBraceBlock("protocol \(translatedWrapper.wrapperName): \(translatedWrapper.swiftName)") { printer in - printer.print("var \(translatedWrapper.javaInterfaceVariableName): \(translatedWrapper.javaInterfaceName) { get }") - } - printer.println() - printer.printBraceBlock("extension \(translatedWrapper.wrapperName)") { printer in - for function in translatedWrapper.functions { - printInterfaceWrapperFunctionImpl(&printer, function, inside: translatedWrapper) - printer.println() - } - - // FIXME: Add support for protocol variables https://github.com/swiftlang/swift-java/issues/457 -// for variable in translatedWrapper.variables { -// printerInterfaceWrapperVariable(&printer, variable, inside: translatedWrapper) -// printer.println() -// } - } - } - - private func printInterfaceWrapperFunctionImpl( - _ printer: inout CodePrinter, - _ function: JavaInterfaceSwiftWrapper.Function, - inside wrapper: JavaInterfaceSwiftWrapper - ) { - printer.printBraceBlock(function.swiftDecl.signatureString) { printer in - let upcallArguments = zip( - function.originalFunctionSignature.parameters, - function.parameterConversions - ).map { param, conversion in - // Wrap-java does not extract parameter names, so no labels - conversion.render(&printer, param.parameterName!) - } - - let javaUpcall = "\(wrapper.javaInterfaceVariableName).\(function.swiftFunctionName)(\(upcallArguments.joined(separator: ", ")))" - - let resultType = function.originalFunctionSignature.result.type - let result = function.resultConversion.render(&printer, javaUpcall) - if resultType.isVoid { - printer.print(result) - } else { - printer.print("return \(result)") - } - } - } - - private func printerInterfaceWrapperVariable( - _ printer: inout CodePrinter, - _ variable: JavaInterfaceSwiftWrapper.Variable, - inside wrapper: JavaInterfaceSwiftWrapper - ) { - // FIXME: Add support for variables. This won't get printed yet - // so we no need to worry about fatalErrors. - printer.printBraceBlock(variable.swiftDecl.signatureString) { printer in - printer.printBraceBlock("get") { printer in - printer.print("fatalError()") - } - - if let setter = variable.setter { - printer.printBraceBlock("set") { printer in - printer.print("fatalError()") - } - } - } - } - private func printGlobalSwiftThunkSources(_ printer: inout CodePrinter) throws { printHeader(&printer) - printJNIOnLoad(&printer) - for decl in analysis.importedGlobalFuncs { printSwiftFunctionThunk(&printer, decl) printer.println() @@ -200,18 +129,6 @@ extension JNISwift2JavaGenerator { } } - private func printJNIOnLoad(_ printer: inout CodePrinter) { - printer.print( - """ - @_cdecl("JNI_OnLoad") - func JNI_OnLoad(javaVM: JavaVMPointer, reserved: UnsafeMutableRawPointer) -> jint { - SwiftJavaRuntimeSupport._JNI_OnLoad(javaVM, reserved) - return JNI_VERSION_1_6 - } - """ - ) - } - private func printNominalTypeThunks(_ printer: inout CodePrinter, _ type: ImportedNominalType) throws { printHeader(&printer) @@ -222,7 +139,7 @@ extension JNISwift2JavaGenerator { case .actor, .class, .enum, .struct: printConcreteTypeThunks(&printer, type) case .protocol: - try printProtocolThunks(&printer, type) + printProtocolThunks(&printer, type) } } @@ -252,62 +169,15 @@ extension JNISwift2JavaGenerator { printer.println() } - printToStringMethods(&printer, type) printTypeMetadataAddressThunk(&printer, type) printer.println() printDestroyFunctionThunk(&printer, type) } - private func printProtocolThunks(_ printer: inout CodePrinter, _ type: ImportedNominalType) throws { - guard let protocolWrapper = self.interfaceProtocolWrappers[type] else { - return - } - - try printSwiftInterfaceWrapper(&printer, protocolWrapper) + private func printProtocolThunks(_ printer: inout CodePrinter, _ type: ImportedNominalType) { + let protocolName = type.swiftNominal.name } - private func printToStringMethods(_ printer: inout CodePrinter, _ type: ImportedNominalType) { - let selfPointerParam = JavaParameter(name: "selfPointer", type: .long) - let parentName = type.qualifiedName - - printCDecl( - &printer, - javaMethodName: "$toString", - parentName: type.swiftNominal.qualifiedName, - parameters: [ - selfPointerParam - ], - resultType: .javaLangString - ) { printer in - let selfVar = self.printSelfJLongToUnsafeMutablePointer(&printer, swiftParentName: parentName, selfPointerParam) - - printer.print( - """ - return String(describing: \(selfVar).pointee).getJNIValue(in: environment) - """ - ) - } - - printer.println() - - printCDecl( - &printer, - javaMethodName: "$toDebugString", - parentName: type.swiftNominal.qualifiedName, - parameters: [ - selfPointerParam - ], - resultType: .javaLangString - ) { printer in - let selfVar = self.printSelfJLongToUnsafeMutablePointer(&printer, swiftParentName: parentName, selfPointerParam) - - printer.print( - """ - return String(reflecting: \(selfVar).pointee).getJNIValue(in: environment) - """ - ) - } - } private func printEnumDiscriminator(_ printer: inout CodePrinter, _ type: ImportedNominalType) { let selfPointerParam = JavaParameter(name: "selfPointer", type: .long) @@ -347,19 +217,11 @@ extension JNISwift2JavaGenerator { } private func renderEnumCaseCacheInit(_ enumCase: TranslatedEnumCase) -> String { - let nativeParametersClassName = "\(enumCase.enumName)$\(enumCase.name)$_NativeParameters" + 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)")]"# - return renderJNICacheInit(className: nativeParametersClassName, methods: [("", methodSignature)]) - } - - private func renderJNICacheInit(className: String, methods: [(String, MethodSignature)]) -> String { - let fullClassName = "\(javaPackagePath)/\(className)" - let methods = methods.map { name, signature in - #".init(name: "\#(name)", signature: "\#(signature.mangledName)")"# - }.joined(separator: ",\n") - - return #"_JNIMethodIDCache(className: "\#(fullClassName)", methods: [\#(methods)])"# + return #"_JNIMethodIDCache(environment: try! JavaVirtualMachine.shared().environment(), className: "\#(nativeParametersClassName)", methods: \#(methods))"# } private func printEnumGetAsCaseThunk( @@ -410,8 +272,6 @@ extension JNISwift2JavaGenerator { return } - printSwiftFunctionHelperClasses(&printer, decl) - printCDecl( &printer, translatedDecl @@ -420,96 +280,6 @@ extension JNISwift2JavaGenerator { } } - - private func printSwiftFunctionHelperClasses( - _ printer: inout CodePrinter, - _ decl: ImportedFunc - ) { - let protocolParameters = decl.functionSignature.parameters.compactMap { parameter in - if let concreteType = parameter.type.typeIn( - genericParameters: decl.functionSignature.genericParameters, - genericRequirements: decl.functionSignature.genericRequirements - ) { - return (parameter, concreteType) - } - - switch parameter.type { - case .opaque(let protocolType), - .existential(let protocolType): - return (parameter, protocolType) - - default: - return nil - } - }.map { parameter, protocolType in - // We flatten any composite types - switch protocolType { - case .composite(let protocols): - return (parameter, protocols) - - default: - return (parameter, [protocolType]) - } - } - - // For each parameter that is a generic or a protocol, - // we generate a Swift class that conforms to all of those. - for (parameter, protocolTypes) in protocolParameters { - let protocolWrappers: [JavaInterfaceSwiftWrapper] = protocolTypes.compactMap { protocolType in - guard let importedType = self.asImportedNominalTypeDecl(protocolType), - let wrapper = self.interfaceProtocolWrappers[importedType] - else { - return nil - } - return wrapper - } - - // Make sure we can generate wrappers for all the protocols - // that the parameter requires - guard protocolWrappers.count == protocolTypes.count else { - // We cannot extract a wrapper for this class - // so it must only be passed in by JExtract instances - continue - } - - guard let parameterName = parameter.parameterName else { - // TODO: Throw - fatalError() - } - let swiftClassName = JNISwift2JavaGenerator.protocolParameterWrapperClassName( - methodName: decl.name, - parameterName: parameterName, - parentName: decl.parentType?.asNominalType?.nominalTypeDecl.qualifiedName ?? swiftModuleName - ) - let implementingProtocols = protocolWrappers.map(\.wrapperName).joined(separator: ", ") - - printer.printBraceBlock("final class \(swiftClassName): \(implementingProtocols)") { printer in - let variables: [(String, String)] = protocolWrappers.map { wrapper in - return (wrapper.javaInterfaceVariableName, wrapper.javaInterfaceName) - } - for (name, type) in variables { - printer.print("let \(name): \(type)") - } - printer.println() - let initializerParameters = variables.map { "\($0): \($1)" }.joined(separator: ", ") - - printer.printBraceBlock("init(\(initializerParameters))") { printer in - for (name, _) in variables { - printer.print("self.\(name) = \(name)") - } - } - } - } - } - - private func asImportedNominalTypeDecl(_ type: SwiftType) -> ImportedNominalType? { - self.analysis.importedTypes.first(where: ( { name, nominalType in - nominalType.swiftType == type - })).map { - $0.value - } - } - private func printFunctionDowncall( _ printer: inout CodePrinter, _ decl: ImportedFunc @@ -577,19 +347,6 @@ extension JNISwift2JavaGenerator { } result = "\(callee).\(decl.name) = \(newValueArgument)" - case .subscriptGetter: - let parameters = arguments.joined(separator: ", ") - result = "\(callee)[\(parameters)]" - case .subscriptSetter: - guard let newValueArgument = arguments.last else { - fatalError("Setter did not contain newValue parameter: \(decl)") - } - - var argumentsWithoutNewValue = arguments - argumentsWithoutNewValue.removeLast() - - let parameters = argumentsWithoutNewValue.joined(separator: ", ") - result = "\(callee)[\(parameters)] = \(newValueArgument)" } // Lower the result. @@ -777,44 +534,4 @@ extension JNISwift2JavaGenerator { ) return newSelfParamName } - - static func protocolParameterWrapperClassName( - methodName: String, - parameterName: String, - parentName: String? - ) -> String { - let parent = if let parentName { - "\(parentName)_" - } else { - "" - } - return "_\(parent)\(methodName)_\(parameterName)_Wrapper" - } -} - -extension SwiftNominalTypeDeclaration { - private var safeProtocolName: String { - self.qualifiedName.replacingOccurrences(of: ".", with: "_") - } - - /// The name of the corresponding `@JavaInterface` of this type. - var javaInterfaceName: String { - "Java\(safeProtocolName)" - } - - var javaInterfaceSwiftProtocolWrapperName: String { - "SwiftJava\(safeProtocolName)Wrapper" - } - - var javaInterfaceVariableName: String { - "_\(javaInterfaceName.firstCharacterLowercased)Interface" - } - - var generatedJavaClassMacroName: String { - if let parent { - return "\(parent.generatedJavaClassMacroName).Java\(self.name)" - } - - return "Java\(self.name)" - } } diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift index 6c7d91689..169d793d3 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift @@ -41,7 +41,6 @@ package class JNISwift2JavaGenerator: Swift2JavaGenerator { /// Cached Java translation result. 'nil' indicates failed translation. var translatedDecls: [ImportedFunc: TranslatedFunctionDecl] = [:] var translatedEnumCases: [ImportedEnumCase: TranslatedEnumCase] = [:] - var interfaceProtocolWrappers: [ImportedNominalType: JavaInterfaceSwiftWrapper] = [:] /// 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. @@ -85,13 +84,6 @@ package class JNISwift2JavaGenerator: Swift2JavaGenerator { } else { self.expectedOutputSwiftFileNames = [] } - - if translator.config.enableJavaCallbacks ?? false { - // We translate all the protocol wrappers - // as we need them to know what protocols we can allow the user to implement themselves - // in Java. - self.interfaceProtocolWrappers = self.generateInterfaceWrappers(Array(self.analysis.importedTypes.values)) - } } func generate() throws { diff --git a/Sources/JExtractSwiftLib/JavaTypes/JavaType+JDK.swift b/Sources/JExtractSwiftLib/JavaTypes/JavaType+JDK.swift index e1aabd7fd..5e5a72688 100644 --- a/Sources/JExtractSwiftLib/JavaTypes/JavaType+JDK.swift +++ b/Sources/JExtractSwiftLib/JavaTypes/JavaType+JDK.swift @@ -40,19 +40,8 @@ extension JavaType { .class(package: "java.lang", name: "Throwable") } - /// The description of the type java.lang.Object. - static var javaLangObject: JavaType { - .class(package: "java.lang", name: "Object") - } - - /// The description of the type java.util.concurrent.CompletableFuture static func completableFuture(_ T: JavaType) -> JavaType { .class(package: "java.util.concurrent", name: "CompletableFuture", typeParameters: [T.boxedType]) } - - /// The description of the type java.util.concurrent.Future - static func future(_ T: JavaType) -> JavaType { - .class(package: "java.util.concurrent", name: "Future", typeParameters: [T.boxedType]) - } } diff --git a/Sources/JExtractSwiftLib/JavaTypes/JavaType+SwiftKit.swift b/Sources/JExtractSwiftLib/JavaTypes/JavaType+SwiftKit.swift index 7319b2942..2ab9c0a22 100644 --- a/Sources/JExtractSwiftLib/JavaTypes/JavaType+SwiftKit.swift +++ b/Sources/JExtractSwiftLib/JavaTypes/JavaType+SwiftKit.swift @@ -85,9 +85,4 @@ extension JavaType { } } - /// The description of the type org.swift.swiftkit.core.SimpleCompletableFuture - static func simpleCompletableFuture(_ T: JavaType) -> JavaType { - .class(package: "org.swift.swiftkit.core", name: "SimpleCompletableFuture", typeParameters: [T.boxedType]) - } - } diff --git a/Sources/JExtractSwiftLib/Swift2JavaVisitor.swift b/Sources/JExtractSwiftLib/Swift2JavaVisitor.swift index cf99d11b6..247b26626 100644 --- a/Sources/JExtractSwiftLib/Swift2JavaVisitor.swift +++ b/Sources/JExtractSwiftLib/Swift2JavaVisitor.swift @@ -13,9 +13,9 @@ //===----------------------------------------------------------------------===// import Foundation -import SwiftJavaConfigurationShared import SwiftParser import SwiftSyntax +import SwiftJavaConfigurationShared final class Swift2JavaVisitor { let translator: Swift2JavaTranslator @@ -53,9 +53,9 @@ final class Swift2JavaVisitor { case .extensionDecl(let node): self.visit(extensionDecl: node, in: parent, sourceFilePath: sourceFilePath) case .typeAliasDecl: - break // TODO: Implement; https://github.com/swiftlang/swift-java/issues/338 + break // TODO: Implement; https://github.com/swiftlang/swift-java/issues/338 case .associatedTypeDecl: - break // TODO: Implement associated types + break // TODO: Implement associated types case .initializerDecl(let node): self.visit(initializerDecl: node, in: parent) @@ -63,8 +63,9 @@ final class Swift2JavaVisitor { self.visit(functionDecl: node, in: parent, sourceFilePath: sourceFilePath) case .variableDecl(let node): self.visit(variableDecl: node, in: parent, sourceFilePath: sourceFilePath) - case .subscriptDecl(let node): - self.visit(subscriptDecl: node, in: parent) + case .subscriptDecl: + // TODO: Implement subscripts + break case .enumCaseDecl(let node): self.visit(enumCaseDecl: node, in: parent) @@ -74,8 +75,7 @@ final class Swift2JavaVisitor { } func visit( - nominalDecl node: some DeclSyntaxProtocol & DeclGroupSyntax & NamedDeclSyntax - & WithAttributesSyntax & WithModifiersSyntax, + nominalDecl node: some DeclSyntaxProtocol & DeclGroupSyntax & NamedDeclSyntax & WithAttributesSyntax & WithModifiersSyntax, in parent: ImportedNominalType?, sourceFilePath: String ) { @@ -109,19 +109,13 @@ final class Swift2JavaVisitor { guard let importedNominalType = translator.importedNominalType(node.extendedType) else { return } - - // Add any conforming protocols in the extension - importedNominalType.inheritedTypes += node.inheritanceClause?.inheritedTypes.compactMap { - try? SwiftType($0.type, lookupContext: translator.lookupContext) - } ?? [] - for memberItem in node.memberBlock.members { self.visit(decl: memberItem.decl, in: importedNominalType, sourceFilePath: sourceFilePath) } } func visit( - functionDecl node: FunctionDeclSyntax, + functionDecl node: FunctionDeclSyntax, in typeContext: ImportedNominalType?, sourceFilePath: String ) { @@ -160,7 +154,7 @@ final class Swift2JavaVisitor { } func visit( - enumCaseDecl node: EnumCaseDeclSyntax, + enumCaseDecl node: EnumCaseDeclSyntax, in typeContext: ImportedNominalType? ) { guard let typeContext else { @@ -206,7 +200,7 @@ final class Swift2JavaVisitor { } func visit( - variableDecl node: VariableDeclSyntax, + variableDecl node: VariableDeclSyntax, in typeContext: ImportedNominalType?, sourceFilePath: String ) { @@ -222,21 +216,37 @@ final class Swift2JavaVisitor { self.log.debug("Import variable: \(node.kind) '\(node.qualifiedNameForDebug)'") + func importAccessor(kind: SwiftAPIKind) throws { + let signature = try SwiftFunctionSignature( + node, + isSet: kind == .setter, + enclosingType: typeContext?.swiftType, + lookupContext: translator.lookupContext + ) + + let imported = ImportedFunc( + module: translator.swiftModuleName, + swiftDecl: node, + name: varName, + apiKind: kind, + functionSignature: signature + ) + + log.debug("Record imported variable accessor \(kind == .getter ? "getter" : "setter"):\(node.qualifiedNameForDebug)") + if let typeContext { + typeContext.variables.append(imported) + } else { + translator.importedGlobalVariables.append(imported) + } + } + do { let supportedAccessors = node.supportedAccessorKinds(binding: binding) if supportedAccessors.contains(.get) { - try importAccessor( - from: DeclSyntax(node), - in: typeContext, - kind: .getter, - name: varName) + try importAccessor(kind: .getter) } if supportedAccessors.contains(.set) { - try importAccessor( - from: DeclSyntax(node), - in: typeContext, - kind: .setter, - name: varName) + try importAccessor(kind: .setter) } } catch { self.log.debug("Failed to import: \(node.qualifiedNameForDebug); \(error)") @@ -279,89 +289,10 @@ final class Swift2JavaVisitor { typeContext.initializers.append(imported) } - private func visit( - subscriptDecl node: SubscriptDeclSyntax, - in typeContext: ImportedNominalType?, - ) { - guard node.shouldExtract(config: config, log: log, in: typeContext) else { - return - } - - guard let accessorBlock = node.accessorBlock else { - return - } - - let name = "subscript" - let accessors = accessorBlock.supportedAccessorKinds() - - do { - if accessors.contains(.get) { - try importAccessor( - from: DeclSyntax(node), - in: typeContext, - kind: .subscriptGetter, - name: name) - } - if accessors.contains(.set) { - try importAccessor( - from: DeclSyntax(node), - in: typeContext, - kind: .subscriptSetter, - name: name) - } - } catch { - self.log.debug("Failed to import: \(node.qualifiedNameForDebug); \(error)") - } - } - - private func importAccessor( - from node: DeclSyntax, - in typeContext: ImportedNominalType?, - kind: SwiftAPIKind, - name: String - ) throws { - let signature: SwiftFunctionSignature - - switch node.as(DeclSyntaxEnum.self) { - case .variableDecl(let varNode): - signature = try SwiftFunctionSignature( - varNode, - isSet: kind == .setter, - enclosingType: typeContext?.swiftType, - lookupContext: translator.lookupContext) - case .subscriptDecl(let subscriptNode): - signature = try SwiftFunctionSignature( - subscriptNode, - isSet: kind == .subscriptSetter, - enclosingType: typeContext?.swiftType, - lookupContext: translator.lookupContext) - default: - log.warning("Not supported declaration type \(node.kind) while calling importAccessor!") - return - } - - let imported = ImportedFunc( - module: translator.swiftModuleName, - swiftDecl: node, - name: name, - apiKind: kind, - functionSignature: signature - ) - - log.debug( - "Record imported variable accessor \(kind == .getter || kind == .subscriptGetter ? "getter" : "setter"):\(node.qualifiedNameForDebug)" - ) - if let typeContext { - typeContext.variables.append(imported) - } else { - translator.importedGlobalVariables.append(imported) - } - } - private func synthesizeRawRepresentableConformance( enumDecl node: EnumDeclSyntax, in parent: ImportedNominalType? - ) { + ) { guard let imported = translator.importedNominalType(node, parent: parent) else { return } @@ -373,18 +304,15 @@ final class Swift2JavaVisitor { ), inheritanceType.isRawTypeCompatible { - if !imported.variables.contains(where: { - $0.name == "rawValue" && $0.functionSignature.result.type != inheritanceType - }) { + 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, sourceFilePath: imported.sourceFilePath) } - if !imported.initializers.contains(where: { - $0.functionSignature.parameters.count == 1 - && $0.functionSignature.parameters.first?.parameterName == "rawValue" - && $0.functionSignature.parameters.first?.type == inheritanceType - }) { + // FIXME: why is this un-used + 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, sourceFilePath: imported.sourceFilePath) } @@ -402,9 +330,7 @@ extension DeclSyntaxProtocol where Self: WithModifiersSyntax & WithAttributesSyn } guard meetsRequiredAccessLevel else { - log.debug( - "Skip import '\(self.qualifiedNameForDebug)': not at least \(config.effectiveMinimumInputAccessLevelMode)" - ) + log.debug("Skip import '\(self.qualifiedNameForDebug)': not at least \(config.effectiveMinimumInputAccessLevelMode)") return false } guard !attributes.contains(where: { $0.isJava }) else { diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftFunctionSignature.swift b/Sources/JExtractSwiftLib/SwiftTypes/SwiftFunctionSignature.swift index 804769e59..1f2fbd365 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftFunctionSignature.swift +++ b/Sources/JExtractSwiftLib/SwiftTypes/SwiftFunctionSignature.swift @@ -271,10 +271,31 @@ extension SwiftFunctionSignature { lookupContext: SwiftTypeLookupContext ) throws { - self.selfParameter = try Self.variableSelfParameter( - for: DeclSyntax(varNode), - enclosingType: enclosingType, - isSet: isSet) + // If this is a member of a type, so we will have a self parameter. Figure out the + // type and convention for the self parameter. + if let enclosingType { + var isStatic = false + for modifier in varNode.modifiers { + switch modifier.name.tokenKind { + case .keyword(.static): isStatic = true + case .keyword(.class): throw SwiftFunctionTranslationError.classMethod(modifier.name) + default: break + } + } + + if isStatic { + self.selfParameter = .staticMethod(enclosingType) + } else { + self.selfParameter = .instance( + SwiftParameter( + convention: isSet && !enclosingType.isReferenceType ? .inout : .byValue, + type: enclosingType + ) + ) + } + } else { + self.selfParameter = nil + } guard let binding = varNode.bindings.first, varNode.bindings.count == 1 else { throw SwiftFunctionTranslationError.multipleBindings(varNode) @@ -302,9 +323,7 @@ extension SwiftFunctionSignature { self.effectSpecifiers = effectSpecifiers ?? [] if isSet { - self.parameters = [ - SwiftParameter(convention: .byValue, parameterName: "newValue", type: valueType) - ] + self.parameters = [SwiftParameter(convention: .byValue, parameterName: "newValue", type: valueType)] self.result = .void } else { self.parameters = [] @@ -314,50 +333,6 @@ extension SwiftFunctionSignature { self.genericRequirements = [] } - init( - _ subscriptNode: SubscriptDeclSyntax, - isSet: Bool, - enclosingType: SwiftType?, - lookupContext: SwiftTypeLookupContext - ) throws { - self.selfParameter = try Self.variableSelfParameter( - for: DeclSyntax(subscriptNode), - enclosingType: enclosingType, - isSet: isSet) - - let valueType: SwiftType = try SwiftType(subscriptNode.returnClause.type, lookupContext: lookupContext) - var nodeParameters = try subscriptNode.parameterClause.parameters.map { param in - try SwiftParameter(param, lookupContext: lookupContext) - } - - var effectSpecifiers: [SwiftEffectSpecifier]? = nil - switch subscriptNode.accessorBlock?.accessors { - case .getter(let getter): - if let getter = getter.as(AccessorDeclSyntax.self) { - effectSpecifiers = try Self.effectSpecifiers(from: getter) - } - case .accessors(let accessors): - if let getter = accessors.first(where: { $0.accessorSpecifier.tokenKind == .keyword(.get) }) { - effectSpecifiers = try Self.effectSpecifiers(from: getter) - } - default: - break - } - - self.effectSpecifiers = effectSpecifiers ?? [] - - if isSet { - nodeParameters.append(SwiftParameter(convention: .byValue, parameterName: "newValue", type: valueType)) - self.result = .void - } else { - self.result = .init(convention: .direct, type: valueType) - } - - self.parameters = nodeParameters - self.genericParameters = [] - self.genericRequirements = [] - } - private static func effectSpecifiers(from decl: AccessorDeclSyntax) throws -> [SwiftEffectSpecifier] { var effectSpecifiers = [SwiftEffectSpecifier]() if decl.effectSpecifiers?.throwsClause != nil { @@ -368,80 +343,27 @@ extension SwiftFunctionSignature { } return effectSpecifiers } +} - private static func variableSelfParameter( - for decl: DeclSyntax, - enclosingType: SwiftType?, - isSet: Bool - ) throws -> SwiftSelfParameter? { - let modifiers: DeclModifierListSyntax? = - switch decl.as(DeclSyntaxEnum.self) { - case .variableDecl(let varDecl): varDecl.modifiers - case .subscriptDecl(let subscriptDecl): subscriptDecl.modifiers - default: nil - } - - guard let modifiers else { - return nil - } - - // If this is a member of a type, so we will have a self parameter. Figure out the - // type and convention for the self parameter. - if let enclosingType { - var isStatic = false - for modifier in modifiers { - switch modifier.name.tokenKind { - case .keyword(.static): isStatic = true - case .keyword(.class): throw SwiftFunctionTranslationError.classMethod(modifier.name) - default: break - } - } +extension VariableDeclSyntax { + struct SupportedAccessorKinds: OptionSet { + var rawValue: UInt8 - if isStatic { - return .staticMethod(enclosingType) - } else { - return .instance( - SwiftParameter( - convention: isSet && !enclosingType.isReferenceType ? .inout : .byValue, - type: enclosingType - ) - ) - } - } else { - return nil - } + static var get: Self = .init(rawValue: 1 << 0) + static var set: Self = .init(rawValue: 1 << 1) } -} -extension VariableDeclSyntax { /// Determine what operations (i.e. get and/or set) supported in this `VariableDeclSyntax` /// /// - Parameters: /// - binding the pattern binding in this declaration. - func supportedAccessorKinds(binding: PatternBindingSyntax) -> AccessorBlockSyntax.SupportedAccessorKinds { + func supportedAccessorKinds(binding: PatternBindingSyntax) -> SupportedAccessorKinds { if self.bindingSpecifier.tokenKind == .keyword(.let) { return [.get] } if let accessorBlock = binding.accessorBlock { - return accessorBlock.supportedAccessorKinds() - } - - return [.get, .set] - } -} - -extension AccessorBlockSyntax { - struct SupportedAccessorKinds: OptionSet { - var rawValue: UInt8 - - static var get: Self = .init(rawValue: 1 << 0) - static var set: Self = .init(rawValue: 1 << 1) - } - - /// Determine what operations (i.e. get and/or set) supported in this `AccessorBlockSyntax` - func supportedAccessorKinds() -> SupportedAccessorKinds { - switch self.accessors { + switch accessorBlock.accessors { case .getter: return [.get] case .accessors(let accessors): @@ -457,6 +379,9 @@ extension AccessorBlockSyntax { } return [.get] } + } + + return [.get, .set] } } diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownTypeDecls.swift b/Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownTypeDecls.swift index 363663217..4a0cb9e8a 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownTypeDecls.swift +++ b/Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownTypeDecls.swift @@ -65,18 +65,4 @@ enum SwiftKnownTypeDeclKind: String, Hashable { return false } } - - /// Indicates whether this known type is translated by `wrap-java` - /// into the same type as `jextract`. - /// - /// This means we do not have to perform any mapping when passing - /// this type between jextract and wrap-java - var isDirectlyTranslatedToWrapJava: Bool { - switch self { - case .bool, .int, .uint, .int8, .uint8, .int16, .uint16, .int32, .uint32, .int64, .uint64, .float, .double, .string, .void: - return true - default: - return false - } - } } diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftType.swift b/Sources/JExtractSwiftLib/SwiftTypes/SwiftType.swift index aeb88bfba..b2f8d6eac 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftType.swift +++ b/Sources/JExtractSwiftLib/SwiftTypes/SwiftType.swift @@ -210,13 +210,9 @@ extension SwiftNominalType: CustomStringConvertible { extension SwiftNominalType { // TODO: Better way to detect Java wrapped classes. - var isSwiftJavaWrapper: Bool { + var isJavaKitWrapper: Bool { nominalTypeDecl.name.hasPrefix("Java") } - - var isProtocol: Bool { - nominalTypeDecl.kind == .protocol - } } extension SwiftType { diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftTypeLookupContext.swift b/Sources/JExtractSwiftLib/SwiftTypes/SwiftTypeLookupContext.swift index 4489b4f65..33759a2cf 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftTypeLookupContext.swift +++ b/Sources/JExtractSwiftLib/SwiftTypes/SwiftTypeLookupContext.swift @@ -105,17 +105,6 @@ class SwiftTypeLookupContext { typeDecl = try nominalTypeDeclaration(for: node, sourceFilePath: sourceFilePath) case .protocolDecl(let node): typeDecl = try nominalTypeDeclaration(for: node, sourceFilePath: sourceFilePath) - case .extensionDecl(let node): - // For extensions, we have to perform a unqualified lookup, - // as the extentedType is just the identifier of the type. - - guard case .identifierType(let id) = Syntax(node.extendedType).as(SyntaxEnum.self), - let lookupResult = try unqualifiedLookup(name: Identifier(id.name)!, from: node) - else { - throw TypeLookupError.notType(Syntax(node)) - } - - typeDecl = lookupResult case .typeAliasDecl: fatalError("typealias not implemented") case .associatedTypeDecl: diff --git a/Sources/JExtractSwiftLib/ThunkNameRegistry.swift b/Sources/JExtractSwiftLib/ThunkNameRegistry.swift index da2e95c1b..3369ec629 100644 --- a/Sources/JExtractSwiftLib/ThunkNameRegistry.swift +++ b/Sources/JExtractSwiftLib/ThunkNameRegistry.swift @@ -32,9 +32,9 @@ package struct ThunkNameRegistry { let suffix: String switch decl.apiKind { - case .getter, .subscriptGetter: + case .getter: suffix = "$get" - case .setter, .subscriptSetter: + case .setter: suffix = "$set" default: suffix = decl.functionSignature.parameters diff --git a/Sources/JavaStdlib/JavaLangReflect/Constructor+Utilities.swift b/Sources/JavaStdlib/JavaLangReflect/Constructor+Utilities.swift index 4042ec766..8f57ffa51 100644 --- a/Sources/JavaStdlib/JavaLangReflect/Constructor+Utilities.swift +++ b/Sources/JavaStdlib/JavaLangReflect/Constructor+Utilities.swift @@ -13,6 +13,11 @@ //===----------------------------------------------------------------------===// extension Constructor { + /// Whether this is a 'public' constructor. + public var isPublic: Bool { + return (getModifiers() & 1) != 0 + } + /// Whether this is a 'native' constructor. public var isNative: Bool { return (getModifiers() & 256) != 0 diff --git a/Sources/JavaStdlib/JavaLangReflect/HasJavaModifiers.swift b/Sources/JavaStdlib/JavaLangReflect/HasJavaModifiers.swift deleted file mode 100644 index fc10edfea..000000000 --- a/Sources/JavaStdlib/JavaLangReflect/HasJavaModifiers.swift +++ /dev/null @@ -1,47 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2025 Apple Inc. and the Swift.org project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of Swift.org project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import SwiftJava - -public protocol HasJavaModifiers { - func getModifiers() -> Int32 -} - -extension HasJavaModifiers { - /// Whether the modifiers contain 'public'. - public var isPublic: Bool { - return (getModifiers() & 0x00000001) != 0 - } - - /// Whether the modifiers contain 'private'. - public var isPrivate: Bool { - return (getModifiers() & 0x00000002) != 0 - } - - /// Whether the modifiers contain 'protected'. - public var isProtected: Bool { - return (getModifiers() & 0x00000004) != 0 - } - - /// Whether the modifiers is equivelant to 'package'.. - /// - /// The "default" access level in Java is 'package', it is signified by lack of a different access modifier. - public var isPackage: Bool { - return !isPublic && !isPrivate && !isProtected - } -} - -extension Constructor: HasJavaModifiers {} -extension JavaClass: HasJavaModifiers {} -extension Method: HasJavaModifiers {} diff --git a/Sources/JavaStdlib/JavaLangReflect/Method+Utilities.swift b/Sources/JavaStdlib/JavaLangReflect/Method+Utilities.swift index 00ca3d6bf..ecc11b507 100644 --- a/Sources/JavaStdlib/JavaLangReflect/Method+Utilities.swift +++ b/Sources/JavaStdlib/JavaLangReflect/Method+Utilities.swift @@ -13,6 +13,28 @@ //===----------------------------------------------------------------------===// extension Method { + + /// Whether this is a 'public' method. + public var isPublic: Bool { + return (getModifiers() & 0x00000001) != 0 + } + + /// Whether this is a 'private' method. + public var isPrivate: Bool { + return (getModifiers() & 0x00000002) != 0 + } + + /// Whether this is a 'protected' method. + public var isProtected: Bool { + return (getModifiers() & 0x00000004) != 0 + } + + /// Whether this is a 'package' method. + /// + /// The "default" access level in Java is 'package', it is signified by lack of a different access modifier. + public var isPackage: Bool { + return !isPublic && !isPrivate && !isProtected + } /// Whether this is a 'static' method. public var isStatic: Bool { diff --git a/Sources/SwiftJava/AnyJavaObject.swift b/Sources/SwiftJava/AnyJavaObject.swift index 33a83159c..fe77bdbd4 100644 --- a/Sources/SwiftJava/AnyJavaObject.swift +++ b/Sources/SwiftJava/AnyJavaObject.swift @@ -66,7 +66,7 @@ extension AnyJavaObject { javaHolder.object } - /// Retrieve the environment in which this Java object was created. + /// Retrieve the environment in which this Java object resides. public var javaEnvironment: JNIEnvironment { javaHolder.environment } diff --git a/Sources/SwiftJava/JVM/JavaVirtualMachine.swift b/Sources/SwiftJava/JVM/JavaVirtualMachine.swift index bb574c8ad..1c7936a34 100644 --- a/Sources/SwiftJava/JVM/JavaVirtualMachine.swift +++ b/Sources/SwiftJava/JVM/JavaVirtualMachine.swift @@ -350,4 +350,4 @@ extension JavaVirtualMachine { enum JavaKitError: Error { case classpathEntryNotFound(entry: String, classpath: [String]) } -} +} \ No newline at end of file diff --git a/Sources/SwiftJava/JavaObject+MethodCalls.swift b/Sources/SwiftJava/JavaObject+MethodCalls.swift index 0626be23a..4880f7503 100644 --- a/Sources/SwiftJava/JavaObject+MethodCalls.swift +++ b/Sources/SwiftJava/JavaObject+MethodCalls.swift @@ -104,8 +104,7 @@ extension AnyJavaObject { resultType: Result.Type ) throws -> jmethodID { // Retrieve the Java class instance from the object. - let environment = try JavaVirtualMachine.shared().environment() - + let environment = javaEnvironment let thisClass = try environment.translatingJNIExceptions { environment.interface.GetObjectClass(environment, javaThis) }! @@ -116,7 +115,7 @@ extension AnyJavaObject { methodName: methodName, parameterTypes: repeat each parameterTypes, resultType: Result.javaType, - in: environment + in: javaEnvironment ) } } @@ -127,8 +126,7 @@ extension AnyJavaObject { parameterTypes: repeat (each Param).Type ) throws -> jmethodID { // Retrieve the Java class instance from the object. - let environment = try JavaVirtualMachine.shared().environment() - + let environment = javaEnvironment let thisClass = try environment.translatingJNIExceptions { environment.interface.GetObjectClass(environment, javaThis) }! @@ -139,7 +137,7 @@ extension AnyJavaObject { methodName: methodName, parameterTypes: repeat each parameterTypes, resultType: .void, - in: environment + in: javaEnvironment ) } } @@ -169,10 +167,8 @@ extension AnyJavaObject { method: jmethodID, args: repeat each Param ) throws -> Result { - let environment = try JavaVirtualMachine.shared().environment() - return try Self.javaMethodCall( - in: environment, + in: javaEnvironment, this: javaThis, method: method, args: repeat each args @@ -233,10 +229,8 @@ extension AnyJavaObject { method: jmethodID, args: repeat each Param ) throws { - let environment = try JavaVirtualMachine.shared().environment() - try Self.javaMethodCall( - in: environment, + in: javaEnvironment, this: javaThis, method: method, args: repeat each args @@ -282,7 +276,7 @@ extension AnyJavaObject { private func getJNIFieldID(_ fieldName: String, fieldType: FieldType.Type) -> jfieldID? where FieldType: ~Copyable { let this = javaThis - let environment = try! JavaVirtualMachine.shared().environment() + let environment = javaEnvironment // Retrieve the Java class instance from the object. let thisClass = environment.interface.GetObjectClass(environment, this)! @@ -295,19 +289,15 @@ extension AnyJavaObject { fieldType fieldType: FieldType.Type ) -> FieldType where FieldType: ~Copyable { get { - let environment = try! JavaVirtualMachine.shared().environment() - let fieldID = getJNIFieldID(fieldName, fieldType: fieldType)! - let jniMethod = FieldType.jniFieldGet(in: environment) - return FieldType(fromJNI: jniMethod(environment, javaThis, fieldID), in: environment) + let jniMethod = FieldType.jniFieldGet(in: javaEnvironment) + return FieldType(fromJNI: jniMethod(javaEnvironment, javaThis, fieldID), in: javaEnvironment) } nonmutating set { - let environment = try! JavaVirtualMachine.shared().environment() - let fieldID = getJNIFieldID(fieldName, fieldType: fieldType)! - let jniMethod = FieldType.jniFieldSet(in: environment) - jniMethod(environment, javaThis, fieldID, newValue.getJNIValue(in: environment)) + let jniMethod = FieldType.jniFieldSet(in: javaEnvironment) + jniMethod(javaEnvironment, javaThis, fieldID, newValue.getJNIValue(in: javaEnvironment)) } } } @@ -321,7 +311,7 @@ extension JavaClass { resultType: Result.Type ) throws -> Result { let thisClass = javaThis - let environment = try! JavaVirtualMachine.shared().environment() + let environment = javaEnvironment // Compute the method signature so we can find the right method, then look up the // method within the class. @@ -355,7 +345,7 @@ extension JavaClass { arguments: repeat each Param ) throws { let thisClass = javaThis - let environment = try JavaVirtualMachine.shared().environment() + let environment = javaEnvironment // Compute the method signature so we can find the right method, then look up the // method within the class. @@ -382,7 +372,7 @@ extension JavaClass { /// Retrieve the JNI field ID for a field with the given name and type. private func getJNIStaticFieldID(_ fieldName: String, fieldType: FieldType.Type) -> jfieldID? { - let environment = try! JavaVirtualMachine.shared().environment() + let environment = javaEnvironment return environment.interface.GetStaticFieldID(environment, javaThis, fieldName, FieldType.jniMangling) } @@ -392,19 +382,15 @@ extension JavaClass { fieldType fieldType: FieldType.Type ) -> FieldType { get { - let environment = try! JavaVirtualMachine.shared().environment() - let fieldID = getJNIStaticFieldID(fieldName, fieldType: fieldType)! - let jniMethod = FieldType.jniStaticFieldGet(in: environment) - return FieldType(fromJNI: jniMethod(environment, javaThis, fieldID), in: environment) + let jniMethod = FieldType.jniStaticFieldGet(in: javaEnvironment) + return FieldType(fromJNI: jniMethod(javaEnvironment, javaThis, fieldID), in: javaEnvironment) } set { - let environment = try! JavaVirtualMachine.shared().environment() - let fieldID = getJNIStaticFieldID(fieldName, fieldType: fieldType)! - let jniMethod = FieldType.jniStaticFieldSet(in: environment) - jniMethod(environment, javaThis, fieldID, newValue.getJNIValue(in: environment)) + let jniMethod = FieldType.jniStaticFieldSet(in: javaEnvironment) + jniMethod(javaEnvironment, javaThis, fieldID, newValue.getJNIValue(in: javaEnvironment)) } } } diff --git a/Sources/SwiftJava/JavaObjectHolder.swift b/Sources/SwiftJava/JavaObjectHolder.swift index b5e888351..5930da59b 100644 --- a/Sources/SwiftJava/JavaObjectHolder.swift +++ b/Sources/SwiftJava/JavaObjectHolder.swift @@ -32,8 +32,6 @@ public final class JavaObjectHolder { /// in Swift and the Java virtual machine is free to move or deallocate it. func forget() { if let object { - let environment = try! JavaVirtualMachine.shared().environment() - environment.interface.DeleteGlobalRef(environment, object) self.object = nil } diff --git a/Sources/SwiftJava/Macros.swift b/Sources/SwiftJava/Macros.swift index 4c0353661..eb9c43745 100644 --- a/Sources/SwiftJava/Macros.swift +++ b/Sources/SwiftJava/Macros.swift @@ -141,7 +141,6 @@ public macro JavaStaticField(_ javaFieldName: String? = nil, isFinal: Bool = fal /// returning method signature, and then, convert the result to the expected `T` type on the Swift side. @attached(body) public macro JavaMethod( - _ javaMethodName: String? = nil, typeErasedResult: String? = nil ) = #externalMacro(module: "SwiftJavaMacros", type: "JavaMethodMacro") @@ -155,7 +154,9 @@ public macro JavaMethod( /// func sayHelloBack(_ i: Int32) -> Double /// ``` @attached(body) -public macro JavaStaticMethod(_ javaMethodName: String? = nil) = #externalMacro(module: "SwiftJavaMacros", type: "JavaMethodMacro") +public macro JavaStaticMethod( + typeErasedResult: String? = nil +) = #externalMacro(module: "SwiftJavaMacros", type: "JavaMethodMacro") /// Macro that marks extensions to specify that all of the @JavaMethod /// methods are implementations of Java methods spelled as `native`. diff --git a/Sources/SwiftJava/String+Extensions.swift b/Sources/SwiftJava/String+Extensions.swift index b87e6a0c6..0af0de107 100644 --- a/Sources/SwiftJava/String+Extensions.swift +++ b/Sources/SwiftJava/String+Extensions.swift @@ -27,14 +27,10 @@ extension String { } extension String { - /// Convert a Java class name to its canonical name. - /// Replaces `$` with `.` for nested classes but preserves `$` at the start of identifiers. + /// Replace all of the $'s for nested names with "." to turn a Java class + /// name into a Java canonical class name, package var javaClassNameToCanonicalName: String { - self.replacingOccurrences( - of: #"(?<=\w)\$"#, - with: ".", - options: .regularExpression - ) + return replacing("$", with: ".") } /// Whether this is the name of an anonymous class. diff --git a/Sources/SwiftJava/generated/JavaThread.swift b/Sources/SwiftJava/generated/JavaThread.swift deleted file mode 100644 index c71e933b4..000000000 --- a/Sources/SwiftJava/generated/JavaThread.swift +++ /dev/null @@ -1,229 +0,0 @@ -// Auto-generated by Java-to-Swift wrapper generator. -import CSwiftJavaJNI - -@JavaClass("java.lang.Thread") -open class JavaThread: JavaObject { - @JavaMethod - @_nonoverride public convenience init(_ arg0: String, environment: JNIEnvironment? = nil) - - @JavaMethod - @_nonoverride public convenience init(environment: JNIEnvironment? = nil) - - @JavaMethod - open func getName() -> String - - @JavaMethod - open func run() - - @JavaMethod - open func interrupt() - - @JavaMethod - open override func toString() -> String - - @JavaMethod - open override func clone() throws -> JavaObject! - - @JavaMethod - open func join(_ arg0: Int64, _ arg1: Int32) throws - - @JavaMethod - open func join() throws - - @JavaMethod - open func join(_ arg0: Int64) throws - - @JavaMethod - open func setContextClassLoader(_ arg0: JavaClassLoader?) - - @JavaMethod - open func setPriority(_ arg0: Int32) - - @JavaMethod - open func setDaemon(_ arg0: Bool) - - @JavaMethod - open func start() - - @JavaMethod - open func getPriority() -> Int32 - - @JavaMethod - open func isDaemon() -> Bool - - @JavaMethod - open func getContextClassLoader() -> JavaClassLoader! - - @JavaMethod - open func isVirtual() -> Bool - - @JavaMethod - open func isAlive() -> Bool - - @JavaMethod - open func threadId() -> Int64 - - @JavaMethod - open func getUncaughtExceptionHandler() -> JavaThread.UncaughtExceptionHandler! - - @JavaMethod - open func stop() - - @JavaMethod - open func isInterrupted() -> Bool - - @JavaMethod - open func setName(_ arg0: String) - - @JavaMethod - open func checkAccess() - - @JavaMethod - open func getId() -> Int64 - - @JavaMethod - open func setUncaughtExceptionHandler(_ arg0: JavaThread.UncaughtExceptionHandler?) -} -extension JavaThread { - @JavaInterface("java.lang.Thread$Builder") - public struct Builder { - @JavaMethod - public func name(_ arg0: String) -> JavaThread.Builder! - - @JavaMethod - public func name(_ arg0: String, _ arg1: Int64) -> JavaThread.Builder! - - @JavaMethod - public func uncaughtExceptionHandler(_ arg0: JavaThread.UncaughtExceptionHandler?) -> JavaThread.Builder! - - @JavaMethod - public func inheritInheritableThreadLocals(_ arg0: Bool) -> JavaThread.Builder! - } -} -extension JavaThread.Builder { - @JavaInterface("java.lang.Thread$Builder$OfPlatform", extends: JavaThread.Builder.self) - public struct OfPlatform { - @JavaMethod - public func name(_ arg0: String, _ arg1: Int64) -> JavaThread.Builder! - - @JavaMethod - public func name(_ arg0: String, _ arg1: Int64) -> JavaThread.Builder.OfPlatform! - - @JavaMethod - public func name(_ arg0: String) -> JavaThread.Builder! - - @JavaMethod - public func name(_ arg0: String) -> JavaThread.Builder.OfPlatform! - - @JavaMethod - public func priority(_ arg0: Int32) -> JavaThread.Builder.OfPlatform! - - @JavaMethod - public func daemon() -> JavaThread.Builder.OfPlatform! - - @JavaMethod - public func daemon(_ arg0: Bool) -> JavaThread.Builder.OfPlatform! - - @JavaMethod - public func uncaughtExceptionHandler(_ arg0: JavaThread.UncaughtExceptionHandler?) -> JavaThread.Builder.OfPlatform! - - @JavaMethod - public func uncaughtExceptionHandler(_ arg0: JavaThread.UncaughtExceptionHandler?) -> JavaThread.Builder! - - @JavaMethod - public func stackSize(_ arg0: Int64) -> JavaThread.Builder.OfPlatform! - - @JavaMethod - public func inheritInheritableThreadLocals(_ arg0: Bool) -> JavaThread.Builder.OfPlatform! - - @JavaMethod - public func inheritInheritableThreadLocals(_ arg0: Bool) -> JavaThread.Builder! - } -} -extension JavaThread.Builder { - @JavaInterface("java.lang.Thread$Builder$OfVirtual", extends: JavaThread.Builder.self) - public struct OfVirtual { - @JavaMethod - public func name(_ arg0: String, _ arg1: Int64) -> JavaThread.Builder! - - @JavaMethod - public func name(_ arg0: String, _ arg1: Int64) -> JavaThread.Builder.OfVirtual! - - @JavaMethod - public func name(_ arg0: String) -> JavaThread.Builder! - - @JavaMethod - public func name(_ arg0: String) -> JavaThread.Builder.OfVirtual! - - @JavaMethod - public func uncaughtExceptionHandler(_ arg0: JavaThread.UncaughtExceptionHandler?) -> JavaThread.Builder! - - @JavaMethod - public func uncaughtExceptionHandler(_ arg0: JavaThread.UncaughtExceptionHandler?) -> JavaThread.Builder.OfVirtual! - - @JavaMethod - public func inheritInheritableThreadLocals(_ arg0: Bool) -> JavaThread.Builder.OfVirtual! - - @JavaMethod - public func inheritInheritableThreadLocals(_ arg0: Bool) -> JavaThread.Builder! - } -} -extension JavaThread { - @JavaInterface("java.lang.Thread$UncaughtExceptionHandler") - public struct UncaughtExceptionHandler { - @JavaMethod - public func uncaughtException(_ arg0: JavaThread?, _ arg1: Throwable?) - } -} -extension JavaClass { - @JavaStaticField(isFinal: true) - public var MIN_PRIORITY: Int32 - - @JavaStaticField(isFinal: true) - public var NORM_PRIORITY: Int32 - - @JavaStaticField(isFinal: true) - public var MAX_PRIORITY: Int32 - - @JavaStaticMethod - public func currentThread() -> JavaThread! - - @JavaStaticMethod - public func onSpinWait() - - @JavaStaticMethod - public func holdsLock(_ arg0: JavaObject?) -> Bool - - @JavaStaticMethod - public func interrupted() -> Bool - - @JavaStaticMethod - public func activeCount() -> Int32 - - @JavaStaticMethod - public func enumerate(_ arg0: [JavaThread?]) -> Int32 - - @JavaStaticMethod - public func yield() - - @JavaStaticMethod - public func sleep(_ arg0: Int64) throws - - @JavaStaticMethod - public func sleep(_ arg0: Int64, _ arg1: Int32) throws - - @JavaStaticMethod - public func ofPlatform() -> JavaThread.Builder.OfPlatform! - - @JavaStaticMethod - public func ofVirtual() -> JavaThread.Builder.OfVirtual! - - @JavaStaticMethod - public func dumpStack() - - @JavaStaticMethod - public func setDefaultUncaughtExceptionHandler(_ arg0: JavaThread.UncaughtExceptionHandler?) - - @JavaStaticMethod - public func getDefaultUncaughtExceptionHandler() -> JavaThread.UncaughtExceptionHandler! -} diff --git a/Sources/SwiftJava/swift-java.config b/Sources/SwiftJava/swift-java.config index d43096a73..d07ff1620 100644 --- a/Sources/SwiftJava/swift-java.config +++ b/Sources/SwiftJava/swift-java.config @@ -23,7 +23,6 @@ "java.lang.Void" : "JavaVoid", "java.lang.CharSequence": "CharSequence", "java.lang.Appendable": "Appendable", - "java.lang.Thread": "JavaThread", "java.util.Optional": "JavaOptional", "java.util.OptionalDouble": "JavaOptionalDouble", "java.util.OptionalInt": "JavaOptionalInt", diff --git a/Sources/SwiftJavaConfigurationShared/Configuration.swift b/Sources/SwiftJavaConfigurationShared/Configuration.swift index 6e3e2a52b..133b76fd9 100644 --- a/Sources/SwiftJavaConfigurationShared/Configuration.swift +++ b/Sources/SwiftJavaConfigurationShared/Configuration.swift @@ -65,13 +65,6 @@ public struct Configuration: Codable { asyncFuncMode ?? .default } - public var enableJavaCallbacks: Bool? - public var effectiveEnableJavaCallbacks: Bool { - enableJavaCallbacks ?? false - } - - public var generatedJavaSourcesListFileOutput: String? - // ==== wrap-java --------------------------------------------------------- /// The Java class path that should be passed along to the swift-java tool. @@ -98,8 +91,6 @@ public struct Configuration: Codable { /// Exclude input Java types by their package prefix or exact match. public var filterExclude: [String]? - public var singleSwiftFileOutput: String? - // ==== dependencies --------------------------------------------------------- // Java dependencies we need to fetch for this target. @@ -171,11 +162,7 @@ public func readConfiguration(sourceDir: String, file: String = #fileID, line: U /// Configuration is expected to be "JSON-with-comments". /// Specifically "//" comments are allowed and will be trimmed before passing the rest of the config into a standard JSON parser. public func readConfiguration(configPath: URL, file: String = #fileID, line: UInt = #line) throws -> Configuration? { - let configData: Data - do { - configData = try Data(contentsOf: configPath) - } catch { - print("Failed to read SwiftJava configuration at '\(configPath.absoluteURL)', error: \(error)") + guard let configData = try? Data(contentsOf: configPath) else { return nil } diff --git a/Sources/SwiftJavaConfigurationShared/JExtract/JExtractAsyncFuncMode.swift b/Sources/SwiftJavaConfigurationShared/JExtract/JExtractAsyncFuncMode.swift index d7fd84623..221649c52 100644 --- a/Sources/SwiftJavaConfigurationShared/JExtract/JExtractAsyncFuncMode.swift +++ b/Sources/SwiftJavaConfigurationShared/JExtract/JExtractAsyncFuncMode.swift @@ -23,7 +23,7 @@ public enum JExtractAsyncFuncMode: String, Codable { /// Android 23 and below. /// /// - Note: Prefer using the `completableFuture` mode instead, if possible. - case legacyFuture +// case future } extension JExtractAsyncFuncMode { diff --git a/Sources/SwiftJavaConfigurationShared/JExtract/JExtractGenerationMode.swift b/Sources/SwiftJavaConfigurationShared/JExtract/JExtractGenerationMode.swift index 8e11d82b0..1ad331da7 100644 --- a/Sources/SwiftJavaConfigurationShared/JExtract/JExtractGenerationMode.swift +++ b/Sources/SwiftJavaConfigurationShared/JExtract/JExtractGenerationMode.swift @@ -13,7 +13,7 @@ //===----------------------------------------------------------------------===// /// Determines which source generation mode JExtract should be using: JNI or Foreign Function and Memory. -public enum JExtractGenerationMode: String, Sendable, Codable { +public enum JExtractGenerationMode: String, Codable { /// Foreign Value and Memory API case ffm diff --git a/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md b/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md index 7161a26c9..7e8c66d3d 100644 --- a/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md +++ b/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md @@ -83,7 +83,7 @@ SwiftJava's `swift-java jextract` tool automates generating Java bindings from S | Ownership modifiers: `inout`, `borrowing`, `consuming` | ❌ | ❌ | | Default parameter values: `func p(name: String = "")` | ❌ | ❌ | | Operators: `+`, `-`, user defined | ❌ | ❌ | -| Subscripts: `subscript()` | ✅ | ✅ | +| Subscripts: `subscript()` | ❌ | ❌ | | Equatable | ❌ | ❌ | | Pointers: `UnsafeRawPointer`, UnsafeBufferPointer (?) | 🟡 | ❌ | | Nested types: `struct Hello { struct World {} }` | ❌ | ✅ | @@ -92,7 +92,7 @@ SwiftJava's `swift-java jextract` tool automates generating Java bindings from S | Non-escaping closures with primitive arguments/results: `func callMe(maybe: (Int) -> (Double))` | ✅ | ✅ | | Non-escaping closures with object arguments/results: `func callMe(maybe: (JavaObj) -> (JavaObj))` | ❌ | ❌ | | `@escaping` closures: `func callMe(_: @escaping () -> ())` | ❌ | ❌ | -| Swift type extensions: `extension String { func uppercased() }` | ✅ | ✅ | +| Swift type extensions: `extension String { func uppercased() }` | 🟡 | 🟡 | | Swift macros (maybe) | ❌ | ❌ | | Result builders | ❌ | ❌ | | Automatic Reference Counting of class types / lifetime safety | ✅ | ✅ | diff --git a/Sources/SwiftJavaDocumentation/Documentation.docc/index.md b/Sources/SwiftJavaDocumentation/Documentation.docc/index.md index 4383727d9..460f396d5 100644 --- a/Sources/SwiftJavaDocumentation/Documentation.docc/index.md +++ b/Sources/SwiftJavaDocumentation/Documentation.docc/index.md @@ -20,7 +20,7 @@ Reasons why you might want to reach for Swift and Java interoperability include, - Reuse existing libraries which exist in one ecosystem, but don't have a direct equivalent in the other SwiftJava is offering several core libraries which support language interoperability: -- `SwiftJava` (Swift -> Java) - JNI-based support library and Swift macros +- `JavaKit` (Swift -> Java) - JNI-based support library and Swift macros - `SwiftKit` (Java -> Swift) - Support library for Java calling Swift code (either using JNI or FFM) - `swift-java` - command line tool; Supports source generation and also dependency management operations - Build tool integration - SwiftPM Plugin diff --git a/Sources/SwiftJavaMacros/JavaMethodMacro.swift b/Sources/SwiftJavaMacros/JavaMethodMacro.swift index 099484528..f992ee760 100644 --- a/Sources/SwiftJavaMacros/JavaMethodMacro.swift +++ b/Sources/SwiftJavaMacros/JavaMethodMacro.swift @@ -50,27 +50,15 @@ extension JavaMethodMacro: BodyMacro { fatalError("not a function: \(declaration)") } - let funcName = - if case .argumentList(let arguments) = node.arguments, - let argument = arguments.first, - argument.label?.text != "typeErasedResult", - let stringLiteral = argument.expression.as(StringLiteralExprSyntax.self), - stringLiteral.segments.count == 1, - case let .stringSegment(funcNameSegment)? = stringLiteral.segments.first - { - funcNameSegment.content.text - } else { - funcDecl.name.text - } - let isStatic = node.attributeName.trimmedDescription == "JavaStaticMethod" + let funcName = funcDecl.name.text let params = funcDecl.signature.parameterClause.parameters let paramNames = params.map { param in param.parameterName?.text ?? "" }.joined(separator: ", ") let genericResultType: String? = if case let .argumentList(arguments) = node.arguments, - let element = arguments.first(where: { $0.label?.text == "typeErasedResult" }), - let stringLiteral = element.expression + let firstElement = arguments.first, + let stringLiteral = firstElement.expression .as(StringLiteralExprSyntax.self), stringLiteral.segments.count == 1, case let .stringSegment(wrapperName)? = stringLiteral.segments.first { diff --git a/Sources/SwiftJavaRuntimeSupport/JNIMethodIDCaches.swift b/Sources/SwiftJavaRuntimeSupport/DefaultCaches.swift similarity index 55% rename from Sources/SwiftJavaRuntimeSupport/JNIMethodIDCaches.swift rename to Sources/SwiftJavaRuntimeSupport/DefaultCaches.swift index 7be79a9fa..1c3079bc3 100644 --- a/Sources/SwiftJavaRuntimeSupport/JNIMethodIDCaches.swift +++ b/Sources/SwiftJavaRuntimeSupport/DefaultCaches.swift @@ -27,6 +27,7 @@ extension _JNIMethodIDCache { ) private static let cache = _JNIMethodIDCache( + environment: try! JavaVirtualMachine.shared().environment(), className: "java/util/concurrent/CompletableFuture", methods: [completeMethod, completeExceptionallyMethod] ) @@ -46,39 +47,11 @@ extension _JNIMethodIDCache { } } - public enum SimpleCompletableFuture { - private static let completeMethod = Method( - name: "complete", - signature: "(Ljava/lang/Object;)Z" - ) - - private static let completeExceptionallyMethod = Method( - name: "completeExceptionally", - signature: "(Ljava/lang/Throwable;)Z" - ) - - private static let cache = _JNIMethodIDCache( - className: "org/swift/swiftkit/core/SimpleCompletableFuture", - methods: [completeMethod, completeExceptionallyMethod] - ) - - public static var `class`: jclass { - cache.javaClass - } - - public static var complete: jmethodID { - cache.methods[completeMethod]! - } - - public static var completeExceptionally: jmethodID { - cache.methods[completeExceptionallyMethod]! - } - } - public enum Exception { private static let messageConstructor = Method(name: "", signature: "(Ljava/lang/String;)V") private static let cache = _JNIMethodIDCache( + environment: try! JavaVirtualMachine.shared().environment(), className: "java/lang/Exception", methods: [messageConstructor] ) @@ -91,33 +64,4 @@ extension _JNIMethodIDCache { cache.methods[messageConstructor]! } } - - public enum JNISwiftInstance { - private static let memoryAddressMethod = Method( - name: "$memoryAddress", - signature: "()J" - ) - - private static let typeMetadataAddressMethod = Method( - name: "$typeMetadataAddress", - signature: "()J" - ) - - private static let cache = _JNIMethodIDCache( - className: "org/swift/swiftkit/core/JNISwiftInstance", - methods: [memoryAddressMethod, typeMetadataAddressMethod] - ) - - public static var `class`: jclass { - cache.javaClass - } - - public static var memoryAddress: jmethodID { - cache.methods[memoryAddressMethod]! - } - - public static var typeMetadataAddress: jmethodID { - cache.methods[typeMetadataAddressMethod]! - } - } } diff --git a/Sources/SwiftJavaRuntimeSupport/JNI.swift b/Sources/SwiftJavaRuntimeSupport/JNI.swift deleted file mode 100644 index b6f0117df..000000000 --- a/Sources/SwiftJavaRuntimeSupport/JNI.swift +++ /dev/null @@ -1,31 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2025 Apple Inc. and the Swift.org project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of Swift.org project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import SwiftJava -import CSwiftJavaJNI - -final class JNI { - static var shared: JNI! - - let applicationClassLoader: JavaClassLoader - - init(fromVM javaVM: JavaVirtualMachine) { - self.applicationClassLoader = try! JavaClass(environment: javaVM.environment()).currentThread().getContextClassLoader() - } -} - -// Called by generated code, and not automatically by Java. -public func _JNI_OnLoad(_ javaVM: JavaVMPointer, _ reserved: UnsafeMutableRawPointer) { - JNI.shared = JNI(fromVM: JavaVirtualMachine(adoptingJVM: javaVM)) -} diff --git a/Sources/SwiftJavaRuntimeSupport/_JNIBoxedConversions.swift b/Sources/SwiftJavaRuntimeSupport/_JNIBoxedConversions.swift index ad4572caa..68d98ffcd 100644 --- a/Sources/SwiftJavaRuntimeSupport/_JNIBoxedConversions.swift +++ b/Sources/SwiftJavaRuntimeSupport/_JNIBoxedConversions.swift @@ -26,39 +26,47 @@ public enum _JNIBoxedConversions { private static let doubleMethod = _JNIMethodIDCache.Method(name: "valueOf", signature: "(D)Ljava/lang/Double;", isStatic: true) private static let booleanCache = _JNIMethodIDCache( + environment: try! JavaVirtualMachine.shared().environment(), className: "java/lang/Boolean", methods: [booleanMethod] ) private static let byteCache = _JNIMethodIDCache( + environment: try! JavaVirtualMachine.shared().environment(), className: "java/lang/Byte", methods: [byteMethod] ) private static let charCache = _JNIMethodIDCache( + environment: try! JavaVirtualMachine.shared().environment(), className: "java/lang/Character", methods: [charMethod] ) private static let shortCache = _JNIMethodIDCache( + environment: try! JavaVirtualMachine.shared().environment(), className: "java/lang/Short", methods: [shortMethod] ) private static let intCache = _JNIMethodIDCache( + environment: try! JavaVirtualMachine.shared().environment(), className: "java/lang/Integer", methods: [intMethod] ) private static let longCache = _JNIMethodIDCache( + environment: try! JavaVirtualMachine.shared().environment(), className: "java/lang/Long", methods: [longMethod] ) private static let floatCache = _JNIMethodIDCache( + environment: try! JavaVirtualMachine.shared().environment(), className: "java/lang/Float", methods: [floatMethod] ) private static let doubleCache = _JNIMethodIDCache( + environment: try! JavaVirtualMachine.shared().environment(), className: "java/lang/Double", methods: [doubleMethod] ) diff --git a/Sources/SwiftJavaRuntimeSupport/_JNIMethodIDCache.swift b/Sources/SwiftJavaRuntimeSupport/_JNIMethodIDCache.swift index fbadf1916..dd7eb5d13 100644 --- a/Sources/SwiftJavaRuntimeSupport/_JNIMethodIDCache.swift +++ b/Sources/SwiftJavaRuntimeSupport/_JNIMethodIDCache.swift @@ -39,33 +39,11 @@ public final class _JNIMethodIDCache: Sendable { self._class! } - /// An optional reference to a java object holder - /// if we cached this class through the class loader - /// This is to make sure that the underlying reference remains valid - nonisolated(unsafe) private let javaObjectHolder: JavaObjectHolder? - - public init(className: String, methods: [Method]) { - let environment = try! JavaVirtualMachine.shared().environment() - - let clazz: jobject - if let jniClass = environment.interface.FindClass(environment, className) { - clazz = environment.interface.NewGlobalRef(environment, jniClass)! - self.javaObjectHolder = nil - } else { - // Clear any ClassNotFound exceptions from FindClass - environment.interface.ExceptionClear(environment) - - if let javaClass = try? JNI.shared.applicationClassLoader.loadClass( - className.replacingOccurrences(of: "/", with: ".") - ) { - clazz = javaClass.javaThis - self.javaObjectHolder = javaClass.javaHolder - } else { - fatalError("Class \(className) could not be found!") - } + 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._class = clazz + self._class = environment.interface.NewGlobalRef(environment, clazz)! self.methods = methods.reduce(into: [:]) { (result, method) in if method.isStatic { if let methodID = environment.interface.GetStaticMethodID(environment, clazz, method.name, method.signature) { @@ -83,6 +61,7 @@ public final class _JNIMethodIDCache: Sendable { } } + public subscript(_ method: Method) -> jmethodID? { methods[method] } diff --git a/Sources/SwiftJavaRuntimeSupport/generated/JavaJNISwiftInstance.swift b/Sources/SwiftJavaRuntimeSupport/generated/JavaJNISwiftInstance.swift deleted file mode 100644 index 4040d0bcb..000000000 --- a/Sources/SwiftJavaRuntimeSupport/generated/JavaJNISwiftInstance.swift +++ /dev/null @@ -1,21 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// 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 -// -//===----------------------------------------------------------------------===// - -import SwiftJava - -@JavaInterface("org.swift.swiftkit.core.JNISwiftInstance") -public struct JavaJNISwiftInstance { - @JavaMethod("$memoryAddress") - public func memoryAddress() -> Int64 -} diff --git a/Sources/SwiftJavaTool/Commands/ConfigureCommand.swift b/Sources/SwiftJavaTool/Commands/ConfigureCommand.swift index 05bf3b8f3..455cb962d 100644 --- a/Sources/SwiftJavaTool/Commands/ConfigureCommand.swift +++ b/Sources/SwiftJavaTool/Commands/ConfigureCommand.swift @@ -59,8 +59,8 @@ extension SwiftJava { swiftModule } - @Option(help: "A prefix that will be added to the names of the Swift types") - var swiftTypePrefix: String? + @Argument(help: "The input file, which is either a swift-java configuration file or (if '-jar' was specified) a Jar file.") + var input: String? } } @@ -137,19 +137,14 @@ extension SwiftJava.ConfigureCommand { print("[debug][swift-java] Importing classpath entry: \(entry)") if entry.hasSuffix(".jar") { - print("[debug][swift-java] Importing classpath as JAR file: \(entry)") let jarFile = try JarFile(entry, false, environment: environment) try addJavaToSwiftMappings( to: &config, forJar: jarFile, environment: environment ) - } else if FileManager.default.fileExists(atPath: entry), let entryURL = URL(string: entry) { - print("[debug][swift-java] Importing classpath as directory: \(entryURL)") - try addJavaToSwiftMappings( - to: &config, - forDirectory: entryURL - ) + } else if FileManager.default.fileExists(atPath: entry) { + log.warning("Currently unable handle directory classpath entries for config generation! Skipping: \(entry)") } else { log.warning("Classpath entry does not exist, skipping: \(entry)") } @@ -167,82 +162,62 @@ extension SwiftJava.ConfigureCommand { ) } - mutating func addJavaToSwiftMappings( - to configuration: inout Configuration, - forDirectory url: Foundation.URL - ) throws { - let enumerator = FileManager.default.enumerator(atPath: url.path()) - - while let filePath = enumerator?.nextObject() as? String { - try addJavaToSwiftMappings(to: &configuration, fileName: filePath) - } - } - mutating func addJavaToSwiftMappings( to configuration: inout Configuration, forJar jarFile: JarFile, environment: JNIEnvironment ) throws { - for entry in jarFile.entries()! { - try addJavaToSwiftMappings(to: &configuration, fileName: entry.getName()) - } - } + let log = Self.log - mutating func addJavaToSwiftMappings( - to configuration: inout Configuration, - fileName: String - ) throws { - // We only look at class files - guard fileName.hasSuffix(".class") else { - return - } + for entry in jarFile.entries()! { + // We only look at class files in the Jar file. + guard entry.getName().hasSuffix(".class") else { + continue + } - // Skip some "common" files we know that would be duplicated in every jar - guard !fileName.hasPrefix("META-INF") else { - return - } - guard !fileName.hasSuffix("package-info") else { - return - } - guard !fileName.hasSuffix("package-info.class") else { - return - } + // Skip some "common" files we know that would be duplicated in every jar + guard !entry.getName().hasPrefix("META-INF") else { + continue + } + guard !entry.getName().hasSuffix("package-info") else { + continue + } + guard !entry.getName().hasSuffix("package-info.class") else { + continue + } - // If this is a local class, it cannot be mapped into Swift. - if fileName.isLocalJavaClass { - return - } + // If this is a local class, it cannot be mapped into Swift. + if entry.getName().isLocalJavaClass { + continue + } - let javaCanonicalName = String(fileName.replacing("/", with: ".") - .dropLast(".class".count)) + let javaCanonicalName = String(entry.getName().replacing("/", with: ".") + .dropLast(".class".count)) - guard SwiftJava.shouldImport(javaCanonicalName: javaCanonicalName, commonOptions: self.commonOptions) else { - log.info("Skip importing class: \(javaCanonicalName) due to include/exclude filters") - return - } + guard SwiftJava.shouldImport(javaCanonicalName: javaCanonicalName, commonOptions: self.commonOptions) else { + log.info("Skip importing class: \(javaCanonicalName) due to include/exclude filters") + continue + } - if configuration.classes?[javaCanonicalName] != nil { - // We never overwrite an existing class mapping configuration. - // E.g. the user may have configured a custom name for a type. - return - } + if configuration.classes?[javaCanonicalName] != nil { + // We never overwrite an existing class mapping configuration. + // E.g. the user may have configured a custom name for a type. + continue + } - if configuration.classes == nil { - configuration.classes = [:] - } + if configuration.classes == nil { + configuration.classes = [:] + } - var swiftName = javaCanonicalName.defaultSwiftNameForJavaClass - if let swiftTypePrefix { - swiftName = "\(swiftTypePrefix)\(swiftName)" - } + if let configuredSwiftName = configuration.classes![javaCanonicalName] { + log.info("Java type '\(javaCanonicalName)' already configured as '\(configuredSwiftName)' Swift type.") + } else { + log.info("Configure Java type '\(javaCanonicalName)' as '\(javaCanonicalName.defaultSwiftNameForJavaClass.bold)' Swift type.") + } - if let configuredSwiftName = configuration.classes![javaCanonicalName] { - log.info("Java type '\(javaCanonicalName)' already configured as '\(configuredSwiftName)' Swift type.") - } else { - log.info("Configure Java type '\(javaCanonicalName)' as '\(swiftName.bold)' Swift type.") + configuration.classes![javaCanonicalName] = + javaCanonicalName.defaultSwiftNameForJavaClass } - - configuration.classes![javaCanonicalName] = swiftName } } diff --git a/Sources/SwiftJavaTool/Commands/JExtractCommand.swift b/Sources/SwiftJavaTool/Commands/JExtractCommand.swift index 775ab6040..b5c3a7bb9 100644 --- a/Sources/SwiftJavaTool/Commands/JExtractCommand.swift +++ b/Sources/SwiftJavaTool/Commands/JExtractCommand.swift @@ -81,12 +81,6 @@ extension SwiftJava { @Option(help: "The mode to use for extracting asynchronous Swift functions. By default async methods are extracted as Java functions returning CompletableFuture.") var asyncFuncMode: JExtractAsyncFuncMode? - - @Flag(help: "By enabling this mode, JExtract will generate Java code that allows you to implement Swift protocols using Java classes. This feature requires disabling the sandbox mode in SwiftPM. This only works in the 'jni' mode.") - var enableJavaCallbacks: Bool = false - - @Option(help: "If specified, JExtract will output to this file a list of paths to all generated Java source files") - var generatedJavaSourcesListFileOutput: String? } } @@ -102,14 +96,10 @@ extension SwiftJava.JExtractCommand { let writeEmptyFiles = CommandLine.arguments.contains("--write-empty-files") ? true : nil configure(&config.writeEmptyFiles, overrideWith: writeEmptyFiles) - let enableJavaCallbacks = CommandLine.arguments.contains("--enable-java-callbacks") ? true : nil - configure(&config.enableJavaCallbacks, overrideWith: enableJavaCallbacks) - configure(&config.unsignedNumbersMode, overrideWith: self.unsignedNumbersMode) configure(&config.minimumInputAccessLevelMode, overrideWith: self.minimumInputAccessLevelMode) configure(&config.memoryManagementMode, overrideWith: self.memoryManagementMode) configure(&config.asyncFuncMode, overrideWith: self.asyncFuncMode) - configure(&config.generatedJavaSourcesListFileOutput, overrideWith: self.generatedJavaSourcesListFileOutput) try checkModeCompatibility(config: config) @@ -136,15 +126,11 @@ extension SwiftJava.JExtractCommand { case .annotate: () // OK case .wrapGuava: - throw IllegalModeCombinationError("JNI mode does not support '\(JExtractUnsignedIntegerMode.wrapGuava)' Unsigned integer mode! \(Self.helpMessage())") + throw IllegalModeCombinationError("JNI mode does not support '\(JExtractUnsignedIntegerMode.wrapGuava)' Unsigned integer mode! \(Self.helpMessage)") } } else if config.effectiveMode == .ffm { guard config.effectiveMemoryManagementMode == .explicit else { - throw IllegalModeCombinationError("FFM mode does not support '\(self.memoryManagementMode ?? .default)' memory management mode! \(Self.helpMessage())") - } - - if let enableJavaCallbacks = config.enableJavaCallbacks, enableJavaCallbacks { - throw IllegalModeCombinationError("FFM mode does not support enabling Java callbacks! \(Self.helpMessage())") + throw IllegalModeCombinationError("FFM mode does not support '\(self.memoryManagementMode)' memory management mode! \(Self.helpMessage)") } } } diff --git a/Sources/SwiftJavaTool/Commands/WrapJavaCommand.swift b/Sources/SwiftJavaTool/Commands/WrapJavaCommand.swift index 3e4e482b3..51315102a 100644 --- a/Sources/SwiftJavaTool/Commands/WrapJavaCommand.swift +++ b/Sources/SwiftJavaTool/Commands/WrapJavaCommand.swift @@ -58,8 +58,8 @@ extension SwiftJava { @Option(help: "Match java package directory structure with generated Swift files") var swiftMatchPackageDirectoryStructure: Bool = false - @Option(help: "If specified, a single Swift file will be generated containing all the generated code") - var singleSwiftFileOutput: String? + @Argument(help: "Path to .jar file whose Java classes should be wrapped using Swift bindings") + var input: String } } @@ -69,7 +69,6 @@ extension SwiftJava.WrapJavaCommand { print("self.commonOptions.filterInclude = \(self.commonOptions.filterInclude)") configure(&config.filterInclude, append: self.commonOptions.filterInclude) configure(&config.filterExclude, append: self.commonOptions.filterExclude) - configure(&config.singleSwiftFileOutput, overrideWith: self.singleSwiftFileOutput) // Get base classpath configuration for this target and configuration var classpathSearchDirs = [self.effectiveSwiftModuleURL] @@ -79,6 +78,7 @@ extension SwiftJava.WrapJavaCommand { } else { print("[trace][swift-java] Cache directory: none") } + print("[trace][swift-java] INPUT: \(input)") var classpathEntries = self.configureCommandJVMClasspath( searchDirs: classpathSearchDirs, config: config, log: Self.log) @@ -151,41 +151,21 @@ extension SwiftJava.WrapJavaCommand { .getSystemClassLoader()! var javaClasses: [JavaClass] = [] for (javaClassName, _) in config.classes ?? [:] { - func remove() { - translator.translatedClasses.removeValue(forKey: javaClassName) - } - guard shouldImportJavaClass(javaClassName, config: config) else { - remove() continue } + log.info("Wrapping java type: \(javaClassName)") + guard let javaClass = try classLoader.loadClass(javaClassName) else { log.warning("Could not load Java class '\(javaClassName)', skipping.") - remove() - continue - } - - guard self.shouldExtract(javaClass: javaClass, config: config) else { - log.info("Skip Java type: \(javaClassName) (does not match minimum access level)") - remove() - continue - } - - guard !javaClass.isEnum() else { - log.info("Skip Java type: \(javaClassName) (enums do not currently work)") - remove() continue } - log.info("Wrapping java type: \(javaClassName)") - // Add this class to the list of classes we'll translate. javaClasses.append(javaClass) } - log.info("OK now we go to nested classes") - // Find all of the nested classes for each class, adding them to the list // of classes to be translated if they were already specified. var allClassesToVisit = javaClasses @@ -232,16 +212,6 @@ extension SwiftJava.WrapJavaCommand { return nil } - guard self.shouldExtract(javaClass: nestedClass, config: config) else { - log.info("Skip Java type: \(javaClassName) (does not match minimum access level)") - return nil - } - - guard !nestedClass.isEnum() else { - log.info("Skip Java type: \(javaClassName) (enums do not currently work)") - return nil - } - // Record this as a translated class. let swiftUnqualifiedName = javaClassName.javaClassNameToCanonicalName .defaultSwiftNameForJavaClass @@ -267,13 +237,9 @@ extension SwiftJava.WrapJavaCommand { try translator.validateClassConfiguration() // Translate all of the Java classes into Swift classes. - - if let singleSwiftFileOutput = config.singleSwiftFileOutput { + for javaClass in javaClasses { translator.startNewFile() - - let swiftClassDecls = try javaClasses.flatMap { - try translator.translateClass($0) - } + let swiftClassDecls = try translator.translateClass(javaClass) let importDecls = translator.getImportDecls() let swiftFileText = """ @@ -283,53 +249,19 @@ extension SwiftJava.WrapJavaCommand { """ + var generatedFileOutputDir = self.actualOutputDirectory + if self.swiftMatchPackageDirectoryStructure { + generatedFileOutputDir?.append(path: javaClass.getPackageName().replacing(".", with: "/")) + } + + let swiftFileName = try! translator.getSwiftTypeName(javaClass, preferValueTypes: false) + .swiftName.replacing(".", with: "+") + ".swift" try writeContents( swiftFileText, - outputDirectory: self.actualOutputDirectory, - to: singleSwiftFileOutput, - description: "Java class translation" + outputDirectory: generatedFileOutputDir, + to: swiftFileName, + description: "Java class '\(javaClass.getName())' translation" ) - } else { - for javaClass in javaClasses { - translator.startNewFile() - - let swiftClassDecls = try translator.translateClass(javaClass) - let importDecls = translator.getImportDecls() - - let swiftFileText = """ - // Auto-generated by Java-to-Swift wrapper generator. - \(importDecls.map { $0.description }.joined()) - \(swiftClassDecls.map { $0.description }.joined(separator: "\n")) - - """ - - var generatedFileOutputDir = self.actualOutputDirectory - if self.swiftMatchPackageDirectoryStructure { - generatedFileOutputDir?.append(path: javaClass.getPackageName().replacing(".", with: "/")) - } - - let swiftFileName = try translator.getSwiftTypeName(javaClass, preferValueTypes: false) - .swiftName.replacing(".", with: "+") + ".swift" - try writeContents( - swiftFileText, - outputDirectory: generatedFileOutputDir, - to: swiftFileName, - description: "Java class '\(javaClass.getName())' translation" - ) - } - } - } - - /// Determines whether a method should be extracted for translation. - /// Only look at public and protected methods here. - private func shouldExtract(javaClass: JavaClass, config: Configuration) -> Bool { - switch config.effectiveMinimumInputAccessLevelMode { - case .internal: - return javaClass.isPublic || javaClass.isProtected || javaClass.isPackage - case .package: - return javaClass.isPublic || javaClass.isProtected || javaClass.isPackage - case .public: - return javaClass.isPublic || javaClass.isProtected } } diff --git a/Sources/SwiftJavaTool/CommonOptions.swift b/Sources/SwiftJavaTool/CommonOptions.swift index 4627381e0..c313202bc 100644 --- a/Sources/SwiftJavaTool/CommonOptions.swift +++ b/Sources/SwiftJavaTool/CommonOptions.swift @@ -67,9 +67,6 @@ extension SwiftJava { @Option(name: .long, help: "While scanning a classpath, skip types which match the filter prefix") var filterExclude: [String] = [] - - @Option(help: "A path to a custom swift-java.config to use") - var config: String? = nil } struct CommonJVMOptions: ParsableArguments { @@ -148,4 +145,4 @@ extension HasCommonJVMOptions { func makeJVM(classpathEntries: [String]) throws -> JavaVirtualMachine { try JavaVirtualMachine.shared(classpath: classpathEntries) } -} +} \ No newline at end of file diff --git a/Sources/SwiftJavaTool/SwiftJavaBaseAsyncParsableCommand.swift b/Sources/SwiftJavaTool/SwiftJavaBaseAsyncParsableCommand.swift index 92818e43a..9763b4b38 100644 --- a/Sources/SwiftJavaTool/SwiftJavaBaseAsyncParsableCommand.swift +++ b/Sources/SwiftJavaTool/SwiftJavaBaseAsyncParsableCommand.swift @@ -163,24 +163,14 @@ extension SwiftJavaBaseAsyncParsableCommand { func readInitialConfiguration(command: some SwiftJavaBaseAsyncParsableCommand) throws -> Configuration { var earlyConfig: Configuration? - if let configPath = commonOptions.config { - let configURL = URL(filePath: configPath, directoryHint: .notDirectory) - print("[debug][swift-java] Load config from passed in path: \(configURL)") - earlyConfig = try readConfiguration(configPath: configURL) - } else if let moduleBaseDir { + if let moduleBaseDir { print("[debug][swift-java] Load config from module base directory: \(moduleBaseDir.path)") earlyConfig = try readConfiguration(sourceDir: moduleBaseDir.path) } else if let inputSwift = commonOptions.inputSwift { print("[debug][swift-java] Load config from module swift input directory: \(inputSwift)") earlyConfig = try readConfiguration(sourceDir: inputSwift) } - var config: Configuration - if let earlyConfig { - config = earlyConfig - } else { - log.warning("[swift-java] Failed to load initial configuration. Proceeding with empty configuration.") - config = Configuration() - } + var config = earlyConfig ?? Configuration() // override configuration with options from command line config.logLevel = command.logLevel return config diff --git a/Sources/SwiftJavaToolLib/JavaClassTranslator.swift b/Sources/SwiftJavaToolLib/JavaClassTranslator.swift index ce780f904..0f7aa45d6 100644 --- a/Sources/SwiftJavaToolLib/JavaClassTranslator.swift +++ b/Sources/SwiftJavaToolLib/JavaClassTranslator.swift @@ -228,11 +228,6 @@ struct JavaClassTranslator { continue } - guard method.getName().isValidSwiftFunctionName else { - log.warning("Skipping method \(method.getName()) because it is not a valid Swift function name") - continue - } - addMethod(method, isNative: false) } @@ -609,15 +604,6 @@ extension JavaClassTranslator { } } - // --- Parameter types - for parameter in method.getParameters() { - if let parameterizedType = parameter?.getParameterizedType() { - if parameterizedType.isEqualTo(typeParam.as(Type.self)) { - return true - } - } - } - return false } diff --git a/Sources/SwiftJavaToolLib/StringExtras.swift b/Sources/SwiftJavaToolLib/StringExtras.swift index c3ac5390f..e69f379c3 100644 --- a/Sources/SwiftJavaToolLib/StringExtras.swift +++ b/Sources/SwiftJavaToolLib/StringExtras.swift @@ -35,11 +35,6 @@ extension String { return "`\(self)`" } - /// Returns whether this is a valid Swift function name - var isValidSwiftFunctionName: Bool { - !self.starts(with: "$") - } - /// Replace all occurrences of one character in the string with another. public func replacing(_ character: Character, with replacement: Character) -> String { return replacingOccurrences(of: String(character), with: String(replacement)) diff --git a/SwiftKitCore/src/main/java/org/swift/swiftkit/core/SimpleCompletableFuture.java b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/SimpleCompletableFuture.java deleted file mode 100644 index b92527236..000000000 --- a/SwiftKitCore/src/main/java/org/swift/swiftkit/core/SimpleCompletableFuture.java +++ /dev/null @@ -1,223 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2025 Apple Inc. and the Swift.org project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of Swift.org project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -package org.swift.swiftkit.core; - -import java.util.Deque; -import java.util.concurrent.*; -import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Function; - -/** - * A simple completable {@link Future} for platforms that do not support {@link java.util.concurrent.CompletableFuture}, - * e.g. before Java 8, and/or before Android 23. - *

- * Prefer using the {@link CompletableFuture} for bridging Swift asynchronous functions, i.e. use the {@code completableFuture} - * mode in {@code swift-java jextract}. - * - * @param The result type - */ -public final class SimpleCompletableFuture implements Future { - // Marker object used to indicate the Future has not yet been completed. - private static final Object PENDING = new Object(); - private static final Object NULL = new Object(); - private final AtomicReference result = new AtomicReference<>(PENDING); - - private final Deque callbacks = new ConcurrentLinkedDeque<>(); - - /** - * Wrapper type we use to indicate that a recorded result was a failure (recorded using {@link SimpleCompletableFuture#completeExceptionally(Throwable)}. - * Since no-one else can instantiate this type, we know for sure that a recorded CompletedExceptionally indicates a failure. - */ - static final class CompletedExceptionally { - private final Throwable exception; - - private CompletedExceptionally(Throwable exception) { - this.exception = exception; - } - } - - /** - * Returns a new future that, when this stage completes - * normally, is executed with this stage's result as the argument - * to the supplied function. - * - *

This method is analogous to - * {@link java.util.Optional#map Optional.map} and - * {@link java.util.stream.Stream#map Stream.map}. - * - * @return the new Future - */ - public Future thenApply(Function fn) { - SimpleCompletableFuture newFuture = new SimpleCompletableFuture<>(); - addCallback(() -> { - Object observed = this.result.get(); - if (observed instanceof CompletedExceptionally) { - newFuture.completeExceptionally(((CompletedExceptionally) observed).exception); - } else { - try { - // We're guaranteed that an observed result is of type T. - // noinspection unchecked - U newResult = fn.apply(observed == NULL ? null : (T) observed); - newFuture.complete(newResult); - } catch (Throwable t) { - newFuture.completeExceptionally(t); - } - } - }); - return newFuture; - } - - /** - * If not already completed, sets the value returned by {@link #get()} and - * related methods to the given value. - * - * @param value the result value - * @return {@code true} if this invocation caused this CompletableFuture - * to transition to a completed state, else {@code false} - */ - public boolean complete(T value) { - if (result.compareAndSet(PENDING, value == null ? NULL : value)) { - synchronized (result) { - result.notifyAll(); - } - runCallbacks(); - return true; - } - - return false; - } - - /** - * If not already completed, causes invocations of {@link #get()} - * and related methods to throw the given exception. - * - * @param ex the exception - * @return {@code true} if this invocation caused this CompletableFuture - * to transition to a completed state, else {@code false} - */ - public boolean completeExceptionally(Throwable ex) { - if (result.compareAndSet(PENDING, new CompletedExceptionally(ex))) { - synchronized (result) { - result.notifyAll(); - } - runCallbacks(); - return true; - } - - return false; - } - - private void runCallbacks() { - // This is a pretty naive implementation; even if we enter this by racing a thenApply, - // with a completion; we're using a concurrent deque so we won't happen to trigger a callback twice. - Runnable callback; - while ((callback = callbacks.pollFirst()) != null) { - callback.run(); - } - } - - @Override - public boolean cancel(boolean mayInterruptIfRunning) { - // TODO: If we're representing a Swift Task computation with this future, - // we could trigger a Task.cancel() from here - return false; - } - - @Override - public boolean isCancelled() { - return false; - } - - @Override - public boolean isDone() { - return this.result.get() != PENDING; - } - - @Override - public T get() throws InterruptedException, ExecutionException { - Object observed; - // If PENDING check fails immediately, we have no need to take the result lock at all. - while ((observed = result.get()) == PENDING) { - synchronized (result) { - if (result.get() == PENDING) { - result.wait(); - } - } - } - - return getReturn(observed); - } - - @Override - public T get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { - Object observed; - - // Fast path: are we already completed and don't need to do any waiting? - if ((observed = result.get()) != PENDING) { - return get(); - } - - long nanos = unit.toNanos(timeout); - synchronized (result) { - if (!isDone()) { - if (nanos <= 0) { - throw new TimeoutException(); - } - long deadline = System.nanoTime() + nanos; - while (!isDone()) { - nanos = deadline - System.nanoTime(); - if (nanos <= 0L) { - throw new TimeoutException(); - } - result.wait(nanos / 1000000, (int) (nanos % 1000000)); - } - } - } - - // Seems we broke out of the wait loop, let's trigger the 'get()' implementation - observed = result.get(); - if (observed == PENDING) { - throw new ExecutionException("Unexpectedly finished wait-loop while future was not completed, this is a bug.", null); - } - return getReturn(observed); - } - - private T getReturn(Object observed) throws ExecutionException { - if (observed instanceof CompletedExceptionally) { - // We observed a failure, unwrap and throw it - Throwable exception = ((CompletedExceptionally) observed).exception; - if (exception instanceof CancellationException) { - throw (CancellationException) exception; - } - throw new ExecutionException(exception); - } else if (observed == NULL) { - return null; - } else { - // We're guaranteed that we only allowed registering completions of type `T` - // noinspection unchecked - return (T) observed; - } - } - - private void addCallback(Runnable action) { - callbacks.add(action); - if (isDone()) { - // This may race, but we don't care since triggering the callbacks is going to be at-most-once - // by means of using the concurrent deque as our list of callbacks. - runCallbacks(); - } - } - -} \ No newline at end of file diff --git a/SwiftKitCore/src/test/java/org/swift/swiftkit/core/SimpleCompletableFutureTest.java b/SwiftKitCore/src/test/java/org/swift/swiftkit/core/SimpleCompletableFutureTest.java deleted file mode 100644 index b4bb98b3a..000000000 --- a/SwiftKitCore/src/test/java/org/swift/swiftkit/core/SimpleCompletableFutureTest.java +++ /dev/null @@ -1,185 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2025 Apple Inc. and the Swift.org project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of Swift.org project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -package org.swift.swiftkit.core; - -import org.junit.jupiter.api.Test; - -import java.util.Objects; -import java.util.concurrent.*; -import static org.junit.jupiter.api.Assertions.*; - -public class SimpleCompletableFutureTest { - - @Test - void testCompleteAndGet() throws ExecutionException, InterruptedException { - SimpleCompletableFuture future = new SimpleCompletableFuture<>(); - assertFalse(future.isDone()); - assertTrue(future.complete("test")); - assertTrue(future.isDone()); - assertEquals("test", future.get()); - } - - @Test - void testCompleteWithNullAndGet() throws ExecutionException, InterruptedException { - SimpleCompletableFuture future = new SimpleCompletableFuture<>(); - assertFalse(future.isDone()); - assertTrue(future.complete(null)); - assertTrue(future.isDone()); - assertNull(future.get()); - } - - @Test - void testCompleteExceptionallyAndGet() { - SimpleCompletableFuture future = new SimpleCompletableFuture<>(); - RuntimeException ex = new RuntimeException("Test Exception"); - assertTrue(future.completeExceptionally(ex)); - assertTrue(future.isDone()); - - ExecutionException thrown = assertThrows(ExecutionException.class, future::get); - assertEquals(ex, thrown.getCause()); - } - - @Test - void testGetWithTimeout_timesOut() { - SimpleCompletableFuture future = new SimpleCompletableFuture<>(); - assertThrows(TimeoutException.class, () -> future.get(10, TimeUnit.MILLISECONDS)); - } - - @Test - void testGetWithTimeout_completesInTime() throws ExecutionException, InterruptedException, TimeoutException { - SimpleCompletableFuture future = new SimpleCompletableFuture<>(); - future.complete("fast"); - assertEquals("fast", future.get(10, TimeUnit.MILLISECONDS)); - } - - @Test - void testGetWithTimeout_completesInTimeAfterWait() throws Exception { - SimpleCompletableFuture future = new SimpleCompletableFuture<>(); - Thread t = new Thread(() -> { - try { - Thread.sleep(50); - } catch (InterruptedException e) { - // ignore - } - future.complete("late"); - }); - t.start(); - assertEquals("late", future.get(200, TimeUnit.MILLISECONDS)); - } - - @Test - void testThenApply() throws ExecutionException, InterruptedException { - SimpleCompletableFuture future = new SimpleCompletableFuture<>(); - Future mapped = future.thenApply(String::length); - - future.complete("hello"); - - assertEquals(5, mapped.get()); - } - - @Test - void testThenApplyOnCompletedFuture() throws ExecutionException, InterruptedException { - SimpleCompletableFuture future = new SimpleCompletableFuture<>(); - future.complete("done"); - - Future mapped = future.thenApply(String::length); - - assertEquals(4, mapped.get()); - } - - @Test - void testThenApplyWithNull() throws ExecutionException, InterruptedException { - SimpleCompletableFuture future = new SimpleCompletableFuture<>(); - Future mapped = future.thenApply(Objects::isNull); - - future.complete(null); - - assertTrue(mapped.get()); - } - - @Test - void testThenApplyExceptionally() { - SimpleCompletableFuture future = new SimpleCompletableFuture<>(); - RuntimeException ex = new RuntimeException("Initial Exception"); - Future mapped = future.thenApply(String::length); - - future.completeExceptionally(ex); - - ExecutionException thrown = assertThrows(ExecutionException.class, mapped::get); - assertEquals(ex, thrown.getCause()); - } - - @Test - void testThenApplyTransformationThrows() { - SimpleCompletableFuture future = new SimpleCompletableFuture<>(); - RuntimeException ex = new RuntimeException("Transformation Exception"); - Future mapped = future.thenApply(s -> { - throw ex; - }); - - future.complete("hello"); - - ExecutionException thrown = assertThrows(ExecutionException.class, mapped::get); - assertEquals(ex, thrown.getCause()); - } - - @Test - void testCompleteTwice() throws ExecutionException, InterruptedException { - SimpleCompletableFuture future = new SimpleCompletableFuture<>(); - - assertTrue(future.complete("first")); - assertFalse(future.complete("second")); - - assertEquals("first", future.get()); - } - - @Test - void testCompleteThenCompleteExceptionally() throws ExecutionException, InterruptedException { - SimpleCompletableFuture future = new SimpleCompletableFuture<>(); - - assertTrue(future.complete("first")); - assertFalse(future.completeExceptionally(new RuntimeException("second"))); - - assertEquals("first", future.get()); - } - - @Test - void testCompleteExceptionallyThenComplete() { - SimpleCompletableFuture future = new SimpleCompletableFuture<>(); - RuntimeException ex = new RuntimeException("first"); - - assertTrue(future.completeExceptionally(ex)); - assertFalse(future.complete("second")); - - ExecutionException thrown = assertThrows(ExecutionException.class, future::get); - assertEquals(ex, thrown.getCause()); - } - - @Test - void testIsDone() { - SimpleCompletableFuture future = new SimpleCompletableFuture<>(); - assertFalse(future.isDone()); - future.complete("done"); - assertTrue(future.isDone()); - } - - @Test - void testIsDoneExceptionally() { - SimpleCompletableFuture future = new SimpleCompletableFuture<>(); - assertFalse(future.isDone()); - future.completeExceptionally(new RuntimeException()); - assertTrue(future.isDone()); - } -} diff --git a/Tests/JExtractSwiftTests/FFM/FFMSubscriptsTests.swift b/Tests/JExtractSwiftTests/FFM/FFMSubscriptsTests.swift deleted file mode 100644 index 89029dd70..000000000 --- a/Tests/JExtractSwiftTests/FFM/FFMSubscriptsTests.swift +++ /dev/null @@ -1,212 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2025 Apple Inc. and the Swift.org project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of Swift.org project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import JExtractSwiftLib -import Testing - -import JExtractSwiftLib -import Testing - -@Suite -struct FFMSubscriptsTests { - private let noParamsSubscriptSource = """ - public struct MyStruct { - private var testVariable: Double = 0 - - public subscript() -> Double { - get { return testVariable } - set { testVariable = newValue } - } - } - """ - - private let subscriptWithParamsSource = """ - public struct MyStruct { - private var testVariable: [Int32] = [] - - public subscript(index: Int32) -> Int32 { - get { return testVariable[Int(index)] } - set { testVariable[Int(index)] = newValue } - } - } - """ - - @Test("Test generation of JavaClass for subscript with no parameters") - func generatesJavaClassForNoParams() throws { - try assertOutput(input: noParamsSubscriptSource, .ffm, .java, expectedChunks: [ - """ - private static class swiftjava_SwiftModule_MyStruct_subscript$get { - private static final FunctionDescriptor DESC = FunctionDescriptor.of( - /* -> */SwiftValueLayout.SWIFT_DOUBLE, - /* self: */SwiftValueLayout.SWIFT_POINTER - ); - """, - """ - private static final MemorySegment ADDR = - SwiftModule.findOrThrow("swiftjava_SwiftModule_MyStruct_subscript$get"); - """, - """ - private static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); - public static double call(java.lang.foreign.MemorySegment self) { - try { - if (CallTraces.TRACE_DOWNCALLS) { - CallTraces.traceDowncall(self); - } - return (double) HANDLE.invokeExact(self); - } catch (Throwable ex$) { - throw new AssertionError("should not reach here", ex$); - } - } - """, - """ - public double getSubscript() { - $ensureAlive(); - return swiftjava_SwiftModule_MyStruct_subscript$get.call(this.$memorySegment()); - """, - ]) - try assertOutput(input: noParamsSubscriptSource, .ffm, .java, expectedChunks: [ - """ - private static class swiftjava_SwiftModule_MyStruct_subscript$set { - private static final FunctionDescriptor DESC = FunctionDescriptor.ofVoid( - /* newValue: */SwiftValueLayout.SWIFT_DOUBLE, - /* self: */SwiftValueLayout.SWIFT_POINTER - ); - private static final MemorySegment ADDR = - SwiftModule.findOrThrow("swiftjava_SwiftModule_MyStruct_subscript$set"); - private static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); - public static void call(double newValue, java.lang.foreign.MemorySegment self) { - try { - if (CallTraces.TRACE_DOWNCALLS) { - CallTraces.traceDowncall(newValue, self); - } - HANDLE.invokeExact(newValue, self); - } catch (Throwable ex$) { - throw new AssertionError("should not reach here", ex$); - } - } - """, - """ - public void setSubscript(double newValue) { - $ensureAlive(); - swiftjava_SwiftModule_MyStruct_subscript$set.call(newValue, this.$memorySegment()); - """ - ]) - } - - @Test("Test generation of JavaClass for subscript with parameters") - func generatesJavaClassForParameters() throws { - try assertOutput(input: subscriptWithParamsSource, .ffm, .java, expectedChunks: [ - """ - private static class swiftjava_SwiftModule_MyStruct_subscript$get { - private static final FunctionDescriptor DESC = FunctionDescriptor.of( - /* -> */SwiftValueLayout.SWIFT_INT32, - /* index: */SwiftValueLayout.SWIFT_INT32, - /* self: */SwiftValueLayout.SWIFT_POINTER - ); - private static final MemorySegment ADDR = - SwiftModule.findOrThrow("swiftjava_SwiftModule_MyStruct_subscript$get"); - private static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); - public static int call(int index, java.lang.foreign.MemorySegment self) { - try { - if (CallTraces.TRACE_DOWNCALLS) { - CallTraces.traceDowncall(index, self); - } - return (int) HANDLE.invokeExact(index, self); - } catch (Throwable ex$) { - throw new AssertionError("should not reach here", ex$); - } - } - """, - """ - public int getSubscript(int index) { - $ensureAlive(); - return swiftjava_SwiftModule_MyStruct_subscript$get.call(index, this.$memorySegment()); - """, - ]) - try assertOutput(input: subscriptWithParamsSource, .ffm, .java, expectedChunks: [ - """ - private static class swiftjava_SwiftModule_MyStruct_subscript$set { - private static final FunctionDescriptor DESC = FunctionDescriptor.ofVoid( - /* index: */SwiftValueLayout.SWIFT_INT32, - /* newValue: */SwiftValueLayout.SWIFT_INT32, - /* self: */SwiftValueLayout.SWIFT_POINTER - ); - private static final MemorySegment ADDR = - SwiftModule.findOrThrow("swiftjava_SwiftModule_MyStruct_subscript$set"); - private static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); - public static void call(int index, int newValue, java.lang.foreign.MemorySegment self) { - try { - if (CallTraces.TRACE_DOWNCALLS) { - CallTraces.traceDowncall(index, newValue, self); - } - HANDLE.invokeExact(index, newValue, self); - } catch (Throwable ex$) { - throw new AssertionError("should not reach here", ex$); - } - } - """, - """ - public void setSubscript(int index, int newValue) { - $ensureAlive(); - swiftjava_SwiftModule_MyStruct_subscript$set.call(index, newValue, this.$memorySegment()); - """, - ]) - } - - @Test("Test generation of Swift thunks for subscript without parameters") - func subscriptWithoutParamsMethodSwiftThunk() throws { - try assertOutput( - input: noParamsSubscriptSource, - .ffm, - .swift, - expectedChunks: [ - """ - @_cdecl("swiftjava_SwiftModule_MyStruct_subscript$get") - public func swiftjava_SwiftModule_MyStruct_subscript$get(_ self: UnsafeRawPointer) -> Double { - return self.assumingMemoryBound(to: MyStruct.self).pointee[] - } - """, - """ - @_cdecl("swiftjava_SwiftModule_MyStruct_subscript$set") - public func swiftjava_SwiftModule_MyStruct_subscript$set(_ newValue: Double, _ self: UnsafeMutableRawPointer) { - self.assumingMemoryBound(to: MyStruct.self).pointee[] = newValue - } - """ - ] - ) - } - - @Test("Test generation of Swift thunks for subscript with parameters") - func subscriptWithParamsMethodSwiftThunk() throws { - try assertOutput( - input: subscriptWithParamsSource, - .ffm, - .swift, - expectedChunks: [ - """ - @_cdecl("swiftjava_SwiftModule_MyStruct_subscript$get") - public func swiftjava_SwiftModule_MyStruct_subscript$get(_ index: Int32, _ self: UnsafeRawPointer) -> Int32 { - return self.assumingMemoryBound(to: MyStruct.self).pointee[index] - } - """, - """ - @_cdecl("swiftjava_SwiftModule_MyStruct_subscript$set") - public func swiftjava_SwiftModule_MyStruct_subscript$set(_ index: Int32, _ newValue: Int32, _ self: UnsafeMutableRawPointer) { - self.assumingMemoryBound(to: MyStruct.self).pointee[index] = newValue - } - """ - ] - ) - } -} diff --git a/Tests/JExtractSwiftTests/JNI/JNIAsyncTests.swift b/Tests/JExtractSwiftTests/JNI/JNIAsyncTests.swift index f246bd0f7..1930e601a 100644 --- a/Tests/JExtractSwiftTests/JNI/JNIAsyncTests.swift +++ b/Tests/JExtractSwiftTests/JNI/JNIAsyncTests.swift @@ -14,7 +14,6 @@ import JExtractSwiftLib import Testing -import SwiftJavaConfigurationShared @Suite struct JNIAsyncTests { @@ -34,9 +33,9 @@ struct JNIAsyncTests { * } */ public static java.util.concurrent.CompletableFuture asyncVoid() { - java.util.concurrent.CompletableFuture future$ = new java.util.concurrent.CompletableFuture(); - SwiftModule.$asyncVoid(future$); - return future$.thenApply((futureResult$) -> { + java.util.concurrent.CompletableFuture $future = new java.util.concurrent.CompletableFuture(); + SwiftModule.$asyncVoid($future); + return $future.thenApply((futureResult$) -> { return futureResult$; } ); @@ -108,9 +107,9 @@ struct JNIAsyncTests { * } */ public static java.util.concurrent.CompletableFuture async() { - java.util.concurrent.CompletableFuture future$ = new java.util.concurrent.CompletableFuture(); - SwiftModule.$async(future$); - return future$.thenApply((futureResult$) -> { + java.util.concurrent.CompletableFuture $future = new java.util.concurrent.CompletableFuture(); + SwiftModule.$async($future); + return $future.thenApply((futureResult$) -> { return futureResult$; } ); @@ -196,9 +195,9 @@ struct JNIAsyncTests { * } */ public static java.util.concurrent.CompletableFuture async(long i) { - java.util.concurrent.CompletableFuture future$ = new java.util.concurrent.CompletableFuture(); - SwiftModule.$async(i, future$); - return future$.thenApply((futureResult$) -> { + java.util.concurrent.CompletableFuture $future = new java.util.concurrent.CompletableFuture(); + SwiftModule.$async(i, $future); + return $future.thenApply((futureResult$) -> { return futureResult$; } ); @@ -277,9 +276,9 @@ struct JNIAsyncTests { * } */ public static java.util.concurrent.CompletableFuture async(MyClass c, SwiftArena swiftArena$) { - java.util.concurrent.CompletableFuture future$ = new java.util.concurrent.CompletableFuture(); - SwiftModule.$async(c.$memoryAddress(), future$); - return future$.thenApply((futureResult$) -> { + java.util.concurrent.CompletableFuture $future = new java.util.concurrent.CompletableFuture(); + SwiftModule.$async(c.$memoryAddress(), $future); + return $future.thenApply((futureResult$) -> { return MyClass.wrapMemoryAddressUnsafe(futureResult$, swiftArena$); } ); @@ -366,9 +365,9 @@ struct JNIAsyncTests { expectedChunks: [ """ public static java.util.concurrent.CompletableFuture async(java.lang.String s) { - java.util.concurrent.CompletableFuture future$ = new java.util.concurrent.CompletableFuture(); - SwiftModule.$async(s, future$); - return future$.thenApply((futureResult$) -> { + java.util.concurrent.CompletableFuture $future = new java.util.concurrent.CompletableFuture(); + SwiftModule.$async(s, $future); + return $future.thenApply((futureResult$) -> { return futureResult$; } ); @@ -409,65 +408,4 @@ struct JNIAsyncTests { ] ) } - - @Test("Import: (MyClass) async -> MyClass (Java, LegacyFuture)") - func legacyFuture_asyncMyClassToMyClass_java() throws { - var config = Configuration() - config.asyncFuncMode = .legacyFuture - - try assertOutput( - input: """ - class MyClass { } - - public func async(c: MyClass) async -> MyClass - """, - config: config, - .jni, .java, - detectChunkByInitialLines: 1, - expectedChunks: [ - """ - public static java.util.concurrent.Future async(MyClass c, SwiftArena swiftArena$) { - org.swift.swiftkit.core.SimpleCompletableFuture future$ = new org.swift.swiftkit.core.SimpleCompletableFuture(); - SwiftModule.$async(c.$memoryAddress(), future$); - return future$.thenApply((futureResult$) -> { - return MyClass.wrapMemoryAddressUnsafe(futureResult$, swiftArena$); - } - ); - } - """, - """ - private static native void $async(long c, org.swift.swiftkit.core.SimpleCompletableFuture result_future); - """, - ] - ) - } - - @Test("Import: (MyClass) async -> MyClass (Swift, LegacyFuture)") - func legacyFuture_asyncMyClassToMyClass_swift() throws { - var config = Configuration() - config.asyncFuncMode = .legacyFuture - - try assertOutput( - input: """ - class MyClass { } - - public func async(c: MyClass) async -> MyClass - """, - config: config, - .jni, .swift, - detectChunkByInitialLines: 1, - expectedChunks: [ - """ - @_cdecl("Java_com_example_swift_SwiftModule__00024async__JLorg_swift_swiftkit_core_SimpleCompletableFuture_2") - func Java_com_example_swift_SwiftModule__00024async__JLorg_swift_swiftkit_core_SimpleCompletableFuture_2(environment: UnsafeMutablePointer!, thisClass: jclass, c: jlong, result_future: jobject?) { - ... - var task: Task? = nil - ... - environment.interface.CallBooleanMethodA(environment, globalFuture, _JNIMethodIDCache.SimpleCompletableFuture.complete, [jvalue(l: boxedResult$)]) - ... - } - """ - ] - ) - } } diff --git a/Tests/JExtractSwiftTests/JNI/JNIEnumTests.swift b/Tests/JExtractSwiftTests/JNI/JNIEnumTests.swift index 2188bcd62..38ef87895 100644 --- a/Tests/JExtractSwiftTests/JNI/JNIEnumTests.swift +++ b/Tests/JExtractSwiftTests/JNI/JNIEnumTests.swift @@ -170,17 +170,17 @@ struct JNIEnumTests { """, """ public record First() implements Case { - record _NativeParameters() {} + record $NativeParameters() {} } """, """ public record Second(java.lang.String arg0) implements Case { - record _NativeParameters(java.lang.String arg0) {} + record $NativeParameters(java.lang.String arg0) {} } """, """ public record Third(long x, int y) implements Case { - record _NativeParameters(long x, int y) {} + record $NativeParameters(long x, int y) {} } """ ]) @@ -268,7 +268,7 @@ struct JNIEnumTests { if (getDiscriminator() != Discriminator.SECOND) { return Optional.empty(); } - Second._NativeParameters $nativeParameters = MyEnum.$getAsSecond(this.$memoryAddress()); + Second.$NativeParameters $nativeParameters = MyEnum.$getAsSecond(this.$memoryAddress()); return Optional.of(new Second($nativeParameters.arg0)); } """, @@ -277,7 +277,7 @@ struct JNIEnumTests { if (getDiscriminator() != Discriminator.THIRD) { return Optional.empty(); } - Third._NativeParameters $nativeParameters = MyEnum.$getAsThird(this.$memoryAddress()); + Third.$NativeParameters $nativeParameters = MyEnum.$getAsThird(this.$memoryAddress()); return Optional.of(new Third($nativeParameters.x, $nativeParameters.y)); } """ diff --git a/Tests/JExtractSwiftTests/JNI/JNIExtensionTests.swift b/Tests/JExtractSwiftTests/JNI/JNIExtensionTests.swift deleted file mode 100644 index e6cb10605..000000000 --- a/Tests/JExtractSwiftTests/JNI/JNIExtensionTests.swift +++ /dev/null @@ -1,65 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2025 Apple Inc. and the Swift.org project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of Swift.org project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import JExtractSwiftLib -import Testing - -@Suite -struct JNIExtensionTests { - let interfaceFile = - """ - extension MyStruct { - public var variableInExtension: String { get } - public func methodInExtension() {} - } - - public protocol MyProtocol {} - public struct MyStruct {} - extension MyStruct: MyProtocol {} - """ - - @Test("Import extensions: Java methods") - func import_javaMethods() throws { - try assertOutput( - input: interfaceFile, - .jni, .java, - detectChunkByInitialLines: 1, - expectedChunks: [ - """ - public final class MyStruct implements JNISwiftInstance, MyProtocol { - ... - public void methodInExtension() { - ... - } - """ - ]) - } - - @Test("Import extensions: Computed variables") - func import_computedVariables() throws { - try assertOutput( - input: interfaceFile, - .jni, .java, - detectChunkByInitialLines: 1, - expectedChunks: [ - """ - public final class MyStruct implements JNISwiftInstance, MyProtocol { - ... - public java.lang.String getVariableInExtension() { - ... - } - """ - ]) - } -} diff --git a/Tests/JExtractSwiftTests/JNI/JNIProtocolTests.swift b/Tests/JExtractSwiftTests/JNI/JNIProtocolTests.swift index 3b47fd100..c5302ca64 100644 --- a/Tests/JExtractSwiftTests/JNI/JNIProtocolTests.swift +++ b/Tests/JExtractSwiftTests/JNI/JNIProtocolTests.swift @@ -12,29 +12,21 @@ // //===----------------------------------------------------------------------===// -import SwiftJavaConfigurationShared import JExtractSwiftLib import Testing @Suite struct JNIProtocolTests { - var config: Configuration { - var config = Configuration() - config.enableJavaCallbacks = true - return config - } - let source = """ public protocol SomeProtocol { + var x: Int64 { get set } + public func method() {} - public func withObject(c: SomeClass) -> SomeClass {} } public protocol B {} - public class SomeClass: SomeProtocol { - public func makeClass() -> SomeClass {} - } + public class SomeClass: SomeProtocol {} public func takeProtocol(x: some SomeProtocol, y: any SomeProtocol) public func takeGeneric(s: S) @@ -45,7 +37,6 @@ struct JNIProtocolTests { func generatesJavaInterface() throws { try assertOutput( input: source, - config: config, .jni, .java, detectChunkByInitialLines: 1, expectedChunks: [ @@ -59,35 +50,18 @@ struct JNIProtocolTests { import org.swift.swiftkit.core.util.*; """, """ - public interface SomeProtocol { - ... - public void method(); - ... - public SomeClass withObject(SomeClass c, SwiftArena swiftArena$); - ... + public interface SomeProtocol extends JNISwiftInstance { + ... } + """, """ - ]) - } - - @Test - func emitsDefault() throws { - try assertOutput( - input: source, - config: config, - .jni, .java, - detectChunkByInitialLines: 1, - expectedChunks: [ + public long getX(); + """, """ - public interface SomeProtocol { - ... - public default SomeClass withObject(SomeClass c) { - return withObject(c, SwiftMemoryManagement.GLOBAL_SWIFT_JAVA_ARENA); - } - ... - public SomeClass withObject(SomeClass c, SwiftArena swiftArena$); - ... - } + public void setX(long newValue); + """, + """ + public void method(); """ ]) } @@ -96,17 +70,12 @@ struct JNIProtocolTests { func generatesJavaClassWithExtends() throws { try assertOutput( input: source, - config: config, .jni, .java, detectChunkByInitialLines: 1, expectedChunks: [ """ public final class SomeClass implements JNISwiftInstance, SomeProtocol { - ... - public SomeClass makeClass(SwiftArena swiftArena$) { - ... - } - """, + """ ]) } @@ -114,17 +83,16 @@ struct JNIProtocolTests { func takeProtocol_java() throws { try assertOutput( input: source, - config: config, .jni, .java, detectChunkByInitialLines: 1, expectedChunks: [ """ - public static <_T0 extends SomeProtocol, _T1 extends SomeProtocol> void takeProtocol(_T0 x, _T1 y) { - SwiftModule.$takeProtocol(x, y); + public static <$T0 extends SomeProtocol, $T1 extends SomeProtocol> void takeProtocol($T0 x, $T1 y) { + SwiftModule.$takeProtocol(x.$memoryAddress(), x.$typeMetadataAddress(), y.$memoryAddress(), y.$typeMetadataAddress()); } """, """ - private static native void $takeProtocol(java.lang.Object x, java.lang.Object y); + private static native void $takeProtocol(long x, long x_typeMetadataAddress, long y, long y_typeMetadataAddress); """ ]) } @@ -133,50 +101,43 @@ struct JNIProtocolTests { func takeProtocol_swift() throws { try assertOutput( input: source, - config: config, .jni, .swift, detectChunkByInitialLines: 1, expectedChunks: [ """ - final class _SwiftModule_takeGeneric_s_Wrapper: SwiftJavaSomeProtocolWrapper { - let _javaSomeProtocolInterface: JavaSomeProtocol - init(_javaSomeProtocolInterface: JavaSomeProtocol) { - self._javaSomeProtocolInterface = _javaSomeProtocolInterface + @_cdecl("Java_com_example_swift_SwiftModule__00024takeProtocol__JJJJ") + func Java_com_example_swift_SwiftModule__00024takeProtocol__JJJJ(environment: UnsafeMutablePointer!, thisClass: jclass, x: jlong, x_typeMetadataAddress: jlong, y: jlong, y_typeMetadataAddress: jlong) { + guard let xTypeMetadataPointer$ = UnsafeRawPointer(bitPattern: Int(Int64(fromJNI: x_typeMetadataAddress, in: environment))) else { + fatalError("x_typeMetadataAddress memory address was null") } - } - """, - """ - @_cdecl("Java_com_example_swift_SwiftModule__00024takeProtocol__Ljava_lang_Object_2Ljava_lang_Object_2") - func Java_com_example_swift_SwiftModule__00024takeProtocol__Ljava_lang_Object_2Ljava_lang_Object_2(environment: UnsafeMutablePointer!, thisClass: jclass, x: jobject?, y: jobject?) { - let xswiftObject$: (SomeProtocol) - if environment.interface.IsInstanceOf(environment, x, _JNIMethodIDCache.JNISwiftInstance.class) != 0 { - ... - let xpointer$DynamicType$: Any.Type = unsafeBitCast(xpointer$TypeMetadataPointer$, to: Any.Type.self) - guard let xpointer$RawPointer$ = UnsafeMutableRawPointer(bitPattern: Int(Int64(fromJNI: xpointer$, in: environment))) else { - fatalError("xpointer$ memory address was null") - } - #if hasFeature(ImplicitOpenExistentials) - let xpointer$Existential$ = xpointer$RawPointer$.load(as: xpointer$DynamicType$) as! any (SomeProtocol) - #else - func xpointer$DoLoad(_ ty: Ty.Type) -> any (SomeProtocol) { - xpointer$RawPointer$.load(as: ty) as! any (SomeProtocol) - } - let xpointer$Existential$ = _openExistential(xpointer$DynamicType$, do: xpointer$DoLoad) - #endif - xswiftObject$ = xpointer$Existential$ + let xDynamicType$: Any.Type = unsafeBitCast(xTypeMetadataPointer$, to: Any.Type.self) + guard let xRawPointer$ = UnsafeMutableRawPointer(bitPattern: Int(Int64(fromJNI: x, in: environment))) else { + fatalError("x memory address was null") } - else { - xswiftObject$ = _SwiftModule_takeProtocol_x_Wrapper(_javaSomeProtocolInterface: JavaSomeProtocol(javaThis: x!, environment: environment)) + #if hasFeature(ImplicitOpenExistentials) + let xExistential$ = xRawPointer$.load(as: xDynamicType$) as! any (SomeProtocol) + #else + func xDoLoad(_ ty: Ty.Type) -> any (SomeProtocol) { + xRawPointer$.load(as: ty) as! any (SomeProtocol) } - let yswiftObject$: (SomeProtocol) - if environment.interface.IsInstanceOf(environment, y, _JNIMethodIDCache.JNISwiftInstance.class) != 0 { - ... - yswiftObject$ = ypointer$Existential$ + let xExistential$ = _openExistential(xDynamicType$, do: xDoLoad) + #endif + guard let yTypeMetadataPointer$ = UnsafeRawPointer(bitPattern: Int(Int64(fromJNI: y_typeMetadataAddress, in: environment))) else { + fatalError("y_typeMetadataAddress memory address was null") } - else { - yswiftObject$ = _SwiftModule_takeProtocol_y_Wrapper(_javaSomeProtocolInterface: JavaSomeProtocol(javaThis: y!, environment: environment)) + let yDynamicType$: Any.Type = unsafeBitCast(yTypeMetadataPointer$, to: Any.Type.self) + guard let yRawPointer$ = UnsafeMutableRawPointer(bitPattern: Int(Int64(fromJNI: y, in: environment))) else { + fatalError("y memory address was null") } - SwiftModule.takeProtocol(x: xswiftObject$, y: yswiftObject$) + #if hasFeature(ImplicitOpenExistentials) + let yExistential$ = yRawPointer$.load(as: yDynamicType$) as! any (SomeProtocol) + #else + func yDoLoad(_ ty: Ty.Type) -> any (SomeProtocol) { + yRawPointer$.load(as: ty) as! any (SomeProtocol) + } + let yExistential$ = _openExistential(yDynamicType$, do: yDoLoad) + #endif + SwiftModule.takeProtocol(x: xExistential$, y: yExistential$) } """ ] @@ -187,17 +148,16 @@ struct JNIProtocolTests { func takeGeneric_java() throws { try assertOutput( input: source, - config: config, .jni, .java, detectChunkByInitialLines: 1, expectedChunks: [ """ public static void takeGeneric(S s) { - SwiftModule.$takeGeneric(s); + SwiftModule.$takeGeneric(s.$memoryAddress(), s.$typeMetadataAddress()); } """, """ - private static native void $takeGeneric(java.lang.Object s); + private static native void $takeGeneric(long s, long s_typeMetadataAddress); """ ]) } @@ -206,30 +166,28 @@ struct JNIProtocolTests { func takeGeneric_swift() throws { try assertOutput( input: source, - config: config, .jni, .swift, detectChunkByInitialLines: 1, expectedChunks: [ """ - final class _SwiftModule_takeGeneric_s_Wrapper: SwiftJavaSomeProtocolWrapper { - let _javaSomeProtocolInterface: JavaSomeProtocol - init(_javaSomeProtocolInterface: JavaSomeProtocol) { - self._javaSomeProtocolInterface = _javaSomeProtocolInterface + @_cdecl("Java_com_example_swift_SwiftModule__00024takeGeneric__JJ") + func Java_com_example_swift_SwiftModule__00024takeGeneric__JJ(environment: UnsafeMutablePointer!, thisClass: jclass, s: jlong, s_typeMetadataAddress: jlong) { + guard let sTypeMetadataPointer$ = UnsafeRawPointer(bitPattern: Int(Int64(fromJNI: s_typeMetadataAddress, in: environment))) else { + fatalError("s_typeMetadataAddress memory address was null") } - } - """, - """ - @_cdecl("Java_com_example_swift_SwiftModule__00024takeGeneric__Ljava_lang_Object_2") - func Java_com_example_swift_SwiftModule__00024takeGeneric__Ljava_lang_Object_2(environment: UnsafeMutablePointer!, thisClass: jclass, s: jobject?) { - let sswiftObject$: (SomeProtocol) - if environment.interface.IsInstanceOf(environment, s, _JNIMethodIDCache.JNISwiftInstance.class) != 0 { - ... - sswiftObject$ = spointer$Existential$ + let sDynamicType$: Any.Type = unsafeBitCast(sTypeMetadataPointer$, to: Any.Type.self) + guard let sRawPointer$ = UnsafeMutableRawPointer(bitPattern: Int(Int64(fromJNI: s, in: environment))) else { + fatalError("s memory address was null") } - else { - sswiftObject$ = _SwiftModule_takeGeneric_s_Wrapper(_javaSomeProtocolInterface: JavaSomeProtocol(javaThis: s!, environment: environment)) + #if hasFeature(ImplicitOpenExistentials) + let sExistential$ = sRawPointer$.load(as: sDynamicType$) as! any (SomeProtocol) + #else + func sDoLoad(_ ty: Ty.Type) -> any (SomeProtocol) { + sRawPointer$.load(as: ty) as! any (SomeProtocol) } - SwiftModule.takeGeneric(s: sswiftObject$) + let sExistential$ = _openExistential(sDynamicType$, do: sDoLoad) + #endif + SwiftModule.takeGeneric(s: sExistential$) } """ ] @@ -240,17 +198,16 @@ struct JNIProtocolTests { func takeComposite_java() throws { try assertOutput( input: source, - config: config, .jni, .java, detectChunkByInitialLines: 1, expectedChunks: [ """ - public static <_T0 extends SomeProtocol & B> void takeComposite(_T0 x) { - SwiftModule.$takeComposite(x); + public static <$T0 extends SomeProtocol & B> void takeComposite($T0 x) { + SwiftModule.$takeComposite(x.$memoryAddress(), x.$typeMetadataAddress()); } """, """ - private static native void $takeComposite(java.lang.Object x); + private static native void $takeComposite(long x, long x_typeMetadataAddress); """ ]) } @@ -259,92 +216,28 @@ struct JNIProtocolTests { func takeComposite_swift() throws { try assertOutput( input: source, - config: config, .jni, .swift, detectChunkByInitialLines: 1, expectedChunks: [ """ - final class _SwiftModule_takeComposite_x_Wrapper: SwiftJavaSomeProtocolWrapper, SwiftJavaBWrapper { - let _javaSomeProtocolInterface: JavaSomeProtocol - let _javaBInterface: JavaB - init(_javaSomeProtocolInterface: JavaSomeProtocol, _javaBInterface: JavaB) { - self._javaSomeProtocolInterface = _javaSomeProtocolInterface - self._javaBInterface = _javaBInterface + @_cdecl("Java_com_example_swift_SwiftModule__00024takeComposite__JJ") + func Java_com_example_swift_SwiftModule__00024takeComposite__JJ(environment: UnsafeMutablePointer!, thisClass: jclass, x: jlong, x_typeMetadataAddress: jlong) { + guard let xTypeMetadataPointer$ = UnsafeRawPointer(bitPattern: Int(Int64(fromJNI: x_typeMetadataAddress, in: environment))) else { + fatalError("x_typeMetadataAddress memory address was null") } - } - """, - """ - @_cdecl("Java_com_example_swift_SwiftModule__00024takeComposite__Ljava_lang_Object_2") - func Java_com_example_swift_SwiftModule__00024takeComposite__Ljava_lang_Object_2(environment: UnsafeMutablePointer!, thisClass: jclass, x: jobject?) { - let xswiftObject$: (SomeProtocol & B) - if environment.interface.IsInstanceOf(environment, x, _JNIMethodIDCache.JNISwiftInstance.class) != 0 { - let xpointer$ = environment.interface.CallLongMethodA(environment, x, _JNIMethodIDCache.JNISwiftInstance.memoryAddress, []) - let xtypeMetadata$ = environment.interface.CallLongMethodA(environment, x, _JNIMethodIDCache.JNISwiftInstance.typeMetadataAddress, []) - guard let xpointer$TypeMetadataPointer$ = UnsafeRawPointer(bitPattern: Int(Int64(fromJNI: xtypeMetadata$, in: environment))) else { - fatalError("xtypeMetadata$ memory address was null") - } - let xpointer$DynamicType$: Any.Type = unsafeBitCast(xpointer$TypeMetadataPointer$, to: Any.Type.self) - guard let xpointer$RawPointer$ = UnsafeMutableRawPointer(bitPattern: Int(Int64(fromJNI: xpointer$, in: environment))) else { - fatalError("xpointer$ memory address was null") - } - #if hasFeature(ImplicitOpenExistentials) - let xpointer$Existential$ = xpointer$RawPointer$.load(as: xpointer$DynamicType$) as! any (SomeProtocol & B) - #else - func xpointer$DoLoad(_ ty: Ty.Type) -> any (SomeProtocol & B) { - xpointer$RawPointer$.load(as: ty) as! any (SomeProtocol & B) - } - let xpointer$Existential$ = _openExistential(xpointer$DynamicType$, do: xpointer$DoLoad) - #endif - xswiftObject$ = xpointer$Existential$ + let xDynamicType$: Any.Type = unsafeBitCast(xTypeMetadataPointer$, to: Any.Type.self) + guard let xRawPointer$ = UnsafeMutableRawPointer(bitPattern: Int(Int64(fromJNI: x, in: environment))) else { + fatalError("x memory address was null") } - else { - xswiftObject$ = _SwiftModule_takeComposite_x_Wrapper(_javaSomeProtocolInterface: JavaSomeProtocol(javaThis: x!, environment: environment), _javaBInterface: JavaB(javaThis: x!, environment: environment)) + #if hasFeature(ImplicitOpenExistentials) + let xExistential$ = xRawPointer$.load(as: xDynamicType$) as! any (SomeProtocol & B) + #else + func xDoLoad(_ ty: Ty.Type) -> any (SomeProtocol & B) { + xRawPointer$.load(as: ty) as! any (SomeProtocol & B) } - SwiftModule.takeComposite(x: xswiftObject$) - } - """ - ] - ) - } - - @Test - func generatesProtocolWrappers() throws { - try assertOutput( - input: source, - config: config, - .jni, .swift, - detectChunkByInitialLines: 1, - expectedChunks: [ - """ - protocol SwiftJavaSomeProtocolWrapper: SomeProtocol { - var _javaSomeProtocolInterface: JavaSomeProtocol { get } - } - """, - """ - extension SwiftJavaSomeProtocolWrapper { - public func method() { - _javaSomeProtocolInterface.method() - } - public func withObject(c: SomeClass) -> SomeClass { - let cClass = try! JavaClass(environment: JavaVirtualMachine.shared().environment()) - let cPointer = UnsafeMutablePointer.allocate(capacity: 1) - cPointer.initialize(to: c) - guard let unwrapped$ = _javaSomeProtocolInterface.withObject(cClass.wrapMemoryAddressUnsafe(Int64(Int(bitPattern: cPointer)))) else { - fatalError("Upcall to withObject unexpectedly returned nil") - } - let result$MemoryAddress$ = unwrapped$.as(JavaJNISwiftInstance.self)!.memoryAddress() - let result$Pointer = UnsafeMutablePointer(bitPattern: Int(result$MemoryAddress$))! - return result$Pointer.pointee - } - } - """, - """ - protocol SwiftJavaBWrapper: B { - var _javaBInterface: JavaB { get } - } - """, - """ - extension SwiftJavaBWrapper { + let xExistential$ = _openExistential(xDynamicType$, do: xDoLoad) + #endif + SwiftModule.takeComposite(x: xExistential$) } """ ] diff --git a/Tests/JExtractSwiftTests/JNI/JNISubscriptsTests.swift b/Tests/JExtractSwiftTests/JNI/JNISubscriptsTests.swift deleted file mode 100644 index 0f7b131d0..000000000 --- a/Tests/JExtractSwiftTests/JNI/JNISubscriptsTests.swift +++ /dev/null @@ -1,155 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2025 Apple Inc. and the Swift.org project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of Swift.org project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import JExtractSwiftLib -import Testing - -@Suite -struct JNISubscriptsTests { - private let noParamsSubscriptSource = """ - public struct MyStruct { - private var testVariable: Double = 0 - - public subscript() -> Double { - get { return testVariable } - set { testVariable = newValue } - } - } - """ - - private let subscriptWithParamsSource = """ - public struct MyStruct { - private var testVariable: [Int32] = [] - - public subscript(index: Int32) -> Int32 { - get { return testVariable[Int(index)] } - set { testVariable[Int(index)] = newValue } - } - } - """ - - @Test("Test generation of JavaClass for subscript with no parameters") - func generatesJavaClassForNoParams() throws { - try assertOutput(input: noParamsSubscriptSource, .jni, .java, expectedChunks: [ - """ - public double getSubscript() { - return MyStruct.$getSubscript(this.$memoryAddress()); - """, - """ - private static native double $getSubscript(long self); - """, - """ - public void setSubscript(double newValue) { - MyStruct.$setSubscript(newValue, this.$memoryAddress()); - """, - """ - private static native void $setSubscript(double newValue, long self); - """ - ]) - try assertOutput( - input: noParamsSubscriptSource, .jni, .java, - expectedChunks: [ - """ - private static native void $destroy(long selfPointer); - """ - ]) - } - - @Test("Test generation of JavaClass for subscript with parameters") - func generatesJavaClassForParameters() throws { - try assertOutput(input: subscriptWithParamsSource, .jni, .java, expectedChunks: [ - """ - public int getSubscript(int index) { - return MyStruct.$getSubscript(index, this.$memoryAddress()); - - """, - """ - private static native int $getSubscript(int index, long self); - """, - """ - public void setSubscript(int index, int newValue) { - MyStruct.$setSubscript(index, newValue, this.$memoryAddress()); - """, - """ - private static native void $setSubscript(int index, int newValue, long self); - """ - ]) - } - - @Test("Test generation of Swift thunks for subscript without parameters") - func subscriptWithoutParamsMethodSwiftThunk() throws { - try assertOutput( - input: noParamsSubscriptSource, - .jni, - .swift, - expectedChunks: [ - """ - @_cdecl("Java_com_example_swift_MyStruct__00024getSubscript__J") - func Java_com_example_swift_MyStruct__00024getSubscript__J(environment: UnsafeMutablePointer!, thisClass: jclass, self: jlong) -> jdouble { - assert(self != 0, "self memory address was null") - let selfBits$ = Int(Int64(fromJNI: self, in: environment)) - let self$ = UnsafeMutablePointer(bitPattern: selfBits$) - guard let self$ else { - fatalError("self memory address was null in call to \\(#function)!") - } - return self$.pointee[].getJNIValue(in: environment) - """, - """ - @_cdecl("Java_com_example_swift_MyStruct__00024setSubscript__DJ") - func Java_com_example_swift_MyStruct__00024setSubscript__DJ(environment: UnsafeMutablePointer!, thisClass: jclass, newValue: jdouble, self: jlong) { - assert(self != 0, "self memory address was null") - let selfBits$ = Int(Int64(fromJNI: self, in: environment)) - let self$ = UnsafeMutablePointer(bitPattern: selfBits$) - guard let self$ else { - fatalError("self memory address was null in call to \\(#function)!") - } - self$.pointee[] = Double(fromJNI: newValue, in: environment) - """ - ] - ) - } - - @Test("Test generation of Swift thunks for subscript with parameters") - func subscriptWithParamsMethodSwiftThunk() throws { - try assertOutput( - input: subscriptWithParamsSource, - .jni, - .swift, - expectedChunks: [ - """ - @_cdecl("Java_com_example_swift_MyStruct__00024getSubscript__IJ") - func Java_com_example_swift_MyStruct__00024getSubscript__IJ(environment: UnsafeMutablePointer!, thisClass: jclass, index: jint, self: jlong) -> jint { - assert(self != 0, "self memory address was null") - let selfBits$ = Int(Int64(fromJNI: self, in: environment)) - let self$ = UnsafeMutablePointer(bitPattern: selfBits$) - guard let self$ else { - fatalError("self memory address was null in call to \\(#function)!") - } - return self$.pointee[Int32(fromJNI: index, in: environment)].getJNIValue(in: environment) - """, - """ - @_cdecl("Java_com_example_swift_MyStruct__00024setSubscript__IIJ") - func Java_com_example_swift_MyStruct__00024setSubscript__IIJ(environment: UnsafeMutablePointer!, thisClass: jclass, index: jint, newValue: jint, self: jlong) { - assert(self != 0, "self memory address was null") - let selfBits$ = Int(Int64(fromJNI: self, in: environment)) - let self$ = UnsafeMutablePointer(bitPattern: selfBits$) - guard let self$ else { - fatalError("self memory address was null in call to \\(#function)!") - } - self$.pointee[Int32(fromJNI: index, in: environment)] = Int32(fromJNI: newValue, in: environment) - """ - ] - ) - } -} diff --git a/Tests/JExtractSwiftTests/JNI/JNIToStringTests.swift b/Tests/JExtractSwiftTests/JNI/JNIToStringTests.swift deleted file mode 100644 index 1059002b6..000000000 --- a/Tests/JExtractSwiftTests/JNI/JNIToStringTests.swift +++ /dev/null @@ -1,96 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2025 Apple Inc. and the Swift.org project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of Swift.org project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import JExtractSwiftLib -import Testing -import SwiftJavaConfigurationShared - -@Suite -struct JNIToStringTests { - let source = - """ - public struct MyType {} - """ - - @Test("JNI toString (Java)") - func toString_java() throws { - try assertOutput( - input: source, - .jni, .java, - detectChunkByInitialLines: 1, - expectedChunks: [ - """ - public String toString() { - return $toString(this.$memoryAddress()); - } - """, - """ - private static native java.lang.String $toString(long selfPointer); - """ - ] - ) - } - - @Test("JNI toString (Swift)") - func toString_swift() throws { - try assertOutput( - input: source, - .jni, .swift, - detectChunkByInitialLines: 1, - expectedChunks: [ - """ - @_cdecl("Java_com_example_swift_MyType__00024toString__J") - func Java_com_example_swift_MyType__00024toString__J(environment: UnsafeMutablePointer!, thisClass: jclass, selfPointer: jlong) -> jstring? { - ... - return String(describing: self$.pointee).getJNIValue(in: environment) - } - """, - ] - ) - } - - @Test("JNI toDebugString (Java)") - func toDebugString_java() throws { - try assertOutput( - input: source, - .jni, .java, - detectChunkByInitialLines: 1, - expectedChunks: [ - """ - public String toDebugString() { - return $toDebugString(this.$memoryAddress()); - } - """, - ] - ) - } - - @Test("JNI toDebugString (Swift)") - func toDebugString_swift() throws { - try assertOutput( - input: source, - .jni, .swift, - detectChunkByInitialLines: 1, - expectedChunks: [ - """ - @_cdecl("Java_com_example_swift_MyType__00024toDebugString__J") - func Java_com_example_swift_MyType__00024toDebugString__J(environment: UnsafeMutablePointer!, thisClass: jclass, selfPointer: jlong) -> jstring? { - ... - return String(reflecting: self$.pointee).getJNIValue(in: environment) - } - """, - ] - ) - } -} diff --git a/Tests/JExtractSwiftTests/MemoryManagementModeTests.swift b/Tests/JExtractSwiftTests/MemoryManagementModeTests.swift index 2228aad88..6017c9875 100644 --- a/Tests/JExtractSwiftTests/MemoryManagementModeTests.swift +++ b/Tests/JExtractSwiftTests/MemoryManagementModeTests.swift @@ -74,34 +74,4 @@ struct MemoryManagementModeTests { ] ) } - - @Test - func allowGlobalAutomatic_protocol() throws { - var config = Configuration() - config.memoryManagementMode = .allowGlobalAutomatic - - try assertOutput( - input: - """ - public class MyClass {} - - public protocol MyProtocol { - public func f() -> MyClass - } - """, - config: config, - .jni, .java, - detectChunkByInitialLines: 1, - expectedChunks: [ - """ - public default MyClass f() { - return f(SwiftMemoryManagement.GLOBAL_SWIFT_JAVA_ARENA); - } - """, - """ - public MyClass f(SwiftArena swiftArena$); - """ - ] - ) - } } diff --git a/Tests/JExtractSwiftTests/UnsignedNumberTests.swift b/Tests/JExtractSwiftTests/UnsignedNumberTests.swift index f3f784098..8109714cb 100644 --- a/Tests/JExtractSwiftTests/UnsignedNumberTests.swift +++ b/Tests/JExtractSwiftTests/UnsignedNumberTests.swift @@ -18,392 +18,251 @@ import Testing final class UnsignedNumberTests { - @Test( - "Import: UInt16 (char)", - arguments: [ - ( - JExtractGenerationMode.ffm, - /* expected chunks */ - [ - """ - /** - * {@snippet lang=c : - * void swiftjava_SwiftModule_unsignedChar__(uint16_t arg) - * } - */ - private static class swiftjava_SwiftModule_unsignedChar__ { - private static final FunctionDescriptor DESC = FunctionDescriptor.ofVoid( - /* arg: */SwiftValueLayout.SWIFT_UINT16 - ); - """, - """ - public static void unsignedChar(@Unsigned char arg) { - swiftjava_SwiftModule_unsignedChar__.call(arg); - } - """, - ] - ), - ( - JExtractGenerationMode.jni, - /* expected chunks */ - [ - """ - public static void unsignedChar(@Unsigned char arg) { - SwiftModule.$unsignedChar(arg); - } - private static native void $unsignedChar(char arg); - """, - ] - ) - ]) - func unsignedChar(mode: JExtractGenerationMode, expectedChunks: [String]) throws { + @Test("Import: UInt16 (char)") + func unsignedChar() throws { try assertOutput( input: "public func unsignedChar(_ arg: UInt16)", - mode, .java, + .ffm, .java, detectChunkByInitialLines: 2, - expectedChunks: expectedChunks + expectedChunks: [ + """ + /** + * {@snippet lang=c : + * void swiftjava_SwiftModule_unsignedChar__(uint16_t arg) + * } + */ + private static class swiftjava_SwiftModule_unsignedChar__ { + private static final FunctionDescriptor DESC = FunctionDescriptor.ofVoid( + /* arg: */SwiftValueLayout.SWIFT_UINT16 + ); + """, + """ + public static void unsignedChar(@Unsigned char arg) { + swiftjava_SwiftModule_unsignedChar__.call(arg); + } + """, + ] ) } - @Test( - "Import: UInt32 (wrap)", - arguments: [ - ( - JExtractGenerationMode.ffm, - /* expected chunks */ - [ - """ - /** - * {@snippet lang=c : - * void swiftjava_SwiftModule_unsignedInt__(uint32_t arg) - * } - */ - private static class swiftjava_SwiftModule_unsignedInt__ { - private static final FunctionDescriptor DESC = FunctionDescriptor.ofVoid( - /* arg: */SwiftValueLayout.SWIFT_UINT32 - ); - """, - """ - public static void unsignedInt(com.google.common.primitives.UnsignedInteger arg) { - swiftjava_SwiftModule_unsignedInt__.call(UnsignedNumbers.toPrimitive(arg)); - } - """, - ] - ), - // JNI mode does not support the "wrap" mode - ]) - func unsignedInt_wrap(mode: JExtractGenerationMode, expectedChunks: [String]) throws { + @Test("Import: UInt32 (wrap)") + func unsignedInt() throws { var config = Configuration() config.unsignedNumbersMode = .wrapGuava try assertOutput( input: "public func unsignedInt(_ arg: UInt32)", config: config, - mode, .java, + .ffm, .java, detectChunkByInitialLines: 2, - expectedChunks: expectedChunks + expectedChunks: [ + """ + /** + * {@snippet lang=c : + * void swiftjava_SwiftModule_unsignedInt__(uint32_t arg) + * } + */ + private static class swiftjava_SwiftModule_unsignedInt__ { + private static final FunctionDescriptor DESC = FunctionDescriptor.ofVoid( + /* arg: */SwiftValueLayout.SWIFT_UINT32 + ); + """, + """ + public static void unsignedInt(com.google.common.primitives.UnsignedInteger arg) { + swiftjava_SwiftModule_unsignedInt__.call(UnsignedNumbers.toPrimitive(arg)); + } + """, + ] ) } - @Test( - "Import: UInt32 (annotate)", - arguments: [ - ( - JExtractGenerationMode.ffm, - /* expected chunks */ - [ - """ - /** - * {@snippet lang=c : - * void swiftjava_SwiftModule_unsignedInt__(uint32_t arg) - * } - */ - private static class swiftjava_SwiftModule_unsignedInt__ { - private static final FunctionDescriptor DESC = FunctionDescriptor.ofVoid( - /* arg: */SwiftValueLayout.SWIFT_UINT32 - ); - """, - """ - public static void unsignedInt(@Unsigned int arg) { - swiftjava_SwiftModule_unsignedInt__.call(arg); - } - """, - ] - ), - ( - JExtractGenerationMode.jni, - /* expected chunks */ - [ - """ - public static void unsignedInt(@Unsigned int arg) { - SwiftModule.$unsignedInt(arg); - } - private static native void $unsignedInt(int arg); - """, - ] - ) - ]) - func unsignedIntAnnotate(mode: JExtractGenerationMode, expectedChunks: [String]) throws { + @Test("Import: UInt32 (annotate)") + func unsignedIntAnnotate() throws { var config = Configuration() config.unsignedNumbersMode = .annotate try assertOutput( input: "public func unsignedInt(_ arg: UInt32)", config: config, - mode, .java, + .ffm, .java, detectChunkByInitialLines: 2, - expectedChunks: expectedChunks + expectedChunks: [ + """ + /** + * {@snippet lang=c : + * void swiftjava_SwiftModule_unsignedInt__(uint32_t arg) + * } + */ + private static class swiftjava_SwiftModule_unsignedInt__ { + private static final FunctionDescriptor DESC = FunctionDescriptor.ofVoid( + /* arg: */SwiftValueLayout.SWIFT_UINT32 + ); + """, + """ + public static void unsignedInt(@Unsigned int arg) { + swiftjava_SwiftModule_unsignedInt__.call(arg); + } + """, + ] ) } - @Test( - "Import: return UInt32 (default)", - arguments: [ - ( - JExtractGenerationMode.ffm, - /* expected chunks */ - [ - """ - /** - * {@snippet lang=c : - * uint32_t swiftjava_SwiftModule_returnUnsignedInt(void) - * } - */ - private static class swiftjava_SwiftModule_returnUnsignedInt { - private static final FunctionDescriptor DESC = FunctionDescriptor.of( - /* -> */SwiftValueLayout.SWIFT_UINT32 - ); - """, - """ - @Unsigned - public static int returnUnsignedInt() { - return swiftjava_SwiftModule_returnUnsignedInt.call(); - } - """, - ] - ), - ( - JExtractGenerationMode.jni, - /* expected chunks */ - [ - """ - @Unsigned - public static int returnUnsignedInt() { - return SwiftModule.$returnUnsignedInt(); - } - private static native int $returnUnsignedInt(); - """, - ] - ) - ]) - func returnUnsignedIntDefault(mode: JExtractGenerationMode, expectedChunks: [String]) throws { + @Test("Import: return UInt32 (default)") + func returnUnsignedIntDefault() throws { let config = Configuration() try assertOutput( input: "public func returnUnsignedInt() -> UInt32", config: config, - mode, .java, + .ffm, .java, detectChunkByInitialLines: 2, - expectedChunks: expectedChunks + expectedChunks: [ + """ + /** + * {@snippet lang=c : + * uint32_t swiftjava_SwiftModule_returnUnsignedInt(void) + * } + */ + private static class swiftjava_SwiftModule_returnUnsignedInt { + private static final FunctionDescriptor DESC = FunctionDescriptor.of( + /* -> */SwiftValueLayout.SWIFT_UINT32 + ); + """, + """ + @Unsigned + public static int returnUnsignedInt() { + return swiftjava_SwiftModule_returnUnsignedInt.call(); + } + """, + ] ) } - @Test( - "Import: return UInt64 (wrap)", - arguments: [ - ( - JExtractGenerationMode.ffm, - /* expected chunks */ - [ - """ - /** - * {@snippet lang=c : - * uint64_t swiftjava_SwiftModule_returnUnsignedLong(void) - * } - */ - private static class swiftjava_SwiftModule_returnUnsignedLong { - private static final FunctionDescriptor DESC = FunctionDescriptor.of( - /* -> */SwiftValueLayout.SWIFT_UINT64 - ); - """, - """ - public static com.google.common.primitives.UnsignedLong returnUnsignedLong() { - return com.google.common.primitives.UnsignedLong.fromLongBits(swiftjava_SwiftModule_returnUnsignedLong.call()); - } - """, - ] - ), - // JNI mode does not support "wrap" mode - ]) - func return_unsignedLongWrap(mode: JExtractGenerationMode, expectedChunks: [String]) throws { + @Test("Import: return UInt64 (wrap)") + func return_unsignedLongWrap() throws { var config = Configuration() config.unsignedNumbersMode = .wrapGuava try assertOutput( input: "public func returnUnsignedLong() -> UInt64", config: config, - mode, .java, + .ffm, .java, detectChunkByInitialLines: 2, - expectedChunks: expectedChunks + expectedChunks: [ + """ + /** + * {@snippet lang=c : + * uint64_t swiftjava_SwiftModule_returnUnsignedLong(void) + * } + */ + private static class swiftjava_SwiftModule_returnUnsignedLong { + private static final FunctionDescriptor DESC = FunctionDescriptor.of( + /* -> */SwiftValueLayout.SWIFT_UINT64 + ); + """, + """ + public static com.google.common.primitives.UnsignedLong returnUnsignedLong() { + return com.google.common.primitives.UnsignedLong.fromLongBits(swiftjava_SwiftModule_returnUnsignedLong.call()); + } + """, + ] ) } - @Test( - "Import: return UInt64 (annotate)", - arguments: [ - ( - JExtractGenerationMode.ffm, - /* expected chunks */ - [ - """ - /** - * {@snippet lang=c : - * uint64_t swiftjava_SwiftModule_returnUnsignedLong(void) - * } - */ - private static class swiftjava_SwiftModule_returnUnsignedLong { - private static final FunctionDescriptor DESC = FunctionDescriptor.of( - /* -> */SwiftValueLayout.SWIFT_UINT64 - ); - """, - """ - @Unsigned - public static long returnUnsignedLong() { - return swiftjava_SwiftModule_returnUnsignedLong.call(); - } - """, - ] - ), - ( - JExtractGenerationMode.jni, - /* expected chunks */ - [ - """ - @Unsigned - public static long returnUnsignedLong() { - return SwiftModule.$returnUnsignedLong(); - } - private static native long $returnUnsignedLong(); - """, - ] - ) - ]) - func return_unsignedLong_annotate(mode: JExtractGenerationMode, expectedChunks: [String]) throws { + @Test("Import: return UInt64 (annotate)") + func return_unsignedLong_annotate() throws { var config = Configuration() config.unsignedNumbersMode = .annotate try assertOutput( input: "public func returnUnsignedLong() -> UInt64", config: config, - mode, .java, + .ffm, .java, detectChunkByInitialLines: 2, - expectedChunks: expectedChunks + expectedChunks: [ + """ + /** + * {@snippet lang=c : + * uint64_t swiftjava_SwiftModule_returnUnsignedLong(void) + * } + */ + private static class swiftjava_SwiftModule_returnUnsignedLong { + private static final FunctionDescriptor DESC = FunctionDescriptor.of( + /* -> */SwiftValueLayout.SWIFT_UINT64 + ); + """, + """ + @Unsigned + public static long returnUnsignedLong() { + return swiftjava_SwiftModule_returnUnsignedLong.call(); + } + """, + ] ) } - @Test( - "Import: take UInt64 (annotate)", - arguments: [ - ( - JExtractGenerationMode.ffm, - /* expected chunks */ - [ - """ - /** - * {@snippet lang=c : - * void swiftjava_SwiftModule_takeUnsignedLong_arg(uint64_t arg) - * } - */ - private static class swiftjava_SwiftModule_takeUnsignedLong_arg { - private static final FunctionDescriptor DESC = FunctionDescriptor.ofVoid( - /* arg: */SwiftValueLayout.SWIFT_UINT64 - ); - """, - """ - public static void takeUnsignedLong(@Unsigned long arg) { - swiftjava_SwiftModule_takeUnsignedLong_arg.call(arg); - } - """, - ] - ), - ( - JExtractGenerationMode.jni, - /* expected chunks */ - [ - """ - public static void takeUnsignedLong(@Unsigned long arg) { - SwiftModule.$takeUnsignedLong(arg); - } - private static native void $takeUnsignedLong(long arg); - """, - ] - ) - ]) - func take_unsignedLong_annotate(mode: JExtractGenerationMode, expectedChunks: [String]) throws { + @Test("Import: take UInt64 (annotate)") + func take_unsignedLong_annotate() throws { var config = Configuration() config.unsignedNumbersMode = .annotate try assertOutput( input: "public func takeUnsignedLong(arg: UInt64)", config: config, - mode, .java, + .ffm, .java, detectChunkByInitialLines: 2, - expectedChunks: expectedChunks + expectedChunks: [ + """ + /** + * {@snippet lang=c : + * void swiftjava_SwiftModule_takeUnsignedLong_arg(uint64_t arg) + * } + */ + private static class swiftjava_SwiftModule_takeUnsignedLong_arg { + private static final FunctionDescriptor DESC = FunctionDescriptor.ofVoid( + /* arg: */SwiftValueLayout.SWIFT_UINT64 + ); + """, + """ + public static void takeUnsignedLong(@Unsigned long arg) { + swiftjava_SwiftModule_takeUnsignedLong_arg.call(arg); + } + """, + ] ) } - @Test( - "Import: take UInt64 return UInt32 (annotate)", - arguments: [ - ( - JExtractGenerationMode.ffm, - /* expected chunks */ - [ - """ - /** - * {@snippet lang=c : - * uint32_t swiftjava_SwiftModule_unsignedLong_first_second(uint64_t first, uint32_t second) - * } - */ - private static class swiftjava_SwiftModule_unsignedLong_first_second { - private static final FunctionDescriptor DESC = FunctionDescriptor.of( - /* -> */SwiftValueLayout.SWIFT_UINT32 - /* first: */SwiftValueLayout.SWIFT_UINT64 - /* second: */SwiftValueLayout.SWIFT_UINT32 - ); - """, - """ - @Unsigned - public static int unsignedLong(@Unsigned long first, @Unsigned int second) { - return swiftjava_SwiftModule_unsignedLong_first_second.call(first, second); - } - """, - ] - ), - ( - JExtractGenerationMode.jni, - /* expected chunks */ - [ - """ - @Unsigned - public static int unsignedLong(@Unsigned long first, @Unsigned int second) { - return SwiftModule.$unsignedLong(first, second); - } - private static native int $unsignedLong(long first, int second); - """, - ] - ), - ]) - func echo_unsignedLong_annotate(mode: JExtractGenerationMode, expectedChunks: [String]) throws { + @Test("Import: take UInt64 return UInt32 (annotate)") + func echo_unsignedLong_annotate() throws { var config = Configuration() config.unsignedNumbersMode = .annotate try assertOutput( input: "public func unsignedLong(first: UInt64, second: UInt32) -> UInt32", config: config, - mode, .java, + .ffm, .java, detectChunkByInitialLines: 2, - expectedChunks: expectedChunks + expectedChunks: [ + """ + /** + * {@snippet lang=c : + * uint32_t swiftjava_SwiftModule_unsignedLong_first_second(uint64_t first, uint32_t second) + * } + */ + private static class swiftjava_SwiftModule_unsignedLong_first_second { + private static final FunctionDescriptor DESC = FunctionDescriptor.of( + /* -> */SwiftValueLayout.SWIFT_UINT32 + /* first: */SwiftValueLayout.SWIFT_UINT64 + /* second: */SwiftValueLayout.SWIFT_UINT32 + ); + """, + """ + @Unsigned + public static int unsignedLong(@Unsigned long first, @Unsigned int second) { + return swiftjava_SwiftModule_unsignedLong_first_second.call(first, second); + } + """, + ] ) } } diff --git a/Tests/SwiftJavaTests/BasicRuntimeTests.swift b/Tests/SwiftJavaTests/BasicRuntimeTests.swift index 292c2a688..b6c18bee5 100644 --- a/Tests/SwiftJavaTests/BasicRuntimeTests.swift +++ b/Tests/SwiftJavaTests/BasicRuntimeTests.swift @@ -82,17 +82,6 @@ class BasicRuntimeTests: XCTestCase { let nullString = String(fromJNI: nil, in: environment) XCTAssertEqual(nullString, "") } - - func testCrossThreadAccess() async throws { - let environment = try jvm.environment() - let url = try URL("https://swift.org", environment: environment) - let string = await Task.detached { - // This should be called on a different thread - url.toString() - }.value - - XCTAssertEqual(string, "https://swift.org") - } } @JavaClass("org.swift.javakit.Nonexistent") diff --git a/Tests/SwiftJavaToolLibTests/WrapJavaTests/GenericsWrapJavaTests.swift b/Tests/SwiftJavaToolLibTests/WrapJavaTests/GenericsWrapJavaTests.swift index ceb05df97..cba7b38f5 100644 --- a/Tests/SwiftJavaToolLibTests/WrapJavaTests/GenericsWrapJavaTests.swift +++ b/Tests/SwiftJavaToolLibTests/WrapJavaTests/GenericsWrapJavaTests.swift @@ -338,45 +338,6 @@ final class GenericsWrapJavaTests: XCTestCase { ) } - func test_wrapJava_genericMethodTypeErasure_customInterface_staticMethods() async throws { - let classpathURL = try await compileJava( - """ - package com.example; - - interface MyInterface {} - - final class Public { - public static void useInterface(T myInterface) { } - } - """) - - try assertWrapJavaOutput( - javaClassNames: [ - "com.example.MyInterface", - "com.example.Public" - ], - classpath: [classpathURL], - expectedChunks: [ - """ - @JavaInterface("com.example.MyInterface") - public struct MyInterface { - """, - """ - @JavaClass("com.example.Public") - open class Public: JavaObject { - """, - """ - extension JavaClass { - """, - """ - @JavaStaticMethod - public func useInterface(_ arg0: T?) - } - """ - ] - ) - } - // TODO: this should be improved some more, we need to generated a `: Map` on the Swift side func test_wrapJava_genericMethodTypeErasure_genericExtendsMap() async throws { let classpathURL = try await compileJava( @@ -409,4 +370,4 @@ final class GenericsWrapJavaTests: XCTestCase { ) } -} +} \ No newline at end of file