diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/ProtocolDefaults.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/ProtocolDefaults.swift new file mode 100644 index 000000000..d5226bf59 --- /dev/null +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/ProtocolDefaults.swift @@ -0,0 +1,57 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +public protocol DefaultOperations { + func seed() -> Int + func add(_ value: Int) -> Int + func total() -> Int + func checked(_ shouldThrow: Bool) throws -> Int64 +} + +extension DefaultOperations { + public func add(_ renamed: Int) -> Int { seed() + renamed } + public func total() -> Int { add(2) } + public func checked(_ shouldThrow: Bool) throws -> Int64 { + if shouldThrow { throw GreeterError() } + return Int64(total()) + } +} + +public func callDefaultOperations(_ operations: some DefaultOperations) -> Int { + operations.total() +} + +public struct DefaultNamespace { + public protocol Parent { func seed() -> Int } + public protocol Child: Parent { func extra() -> Int } +} + +extension DefaultNamespace.Child { + public func extra() -> Int { seed() + 3 } +} + +public func callNestedDefaults(_ child: some DefaultNamespace.Child) -> Int { + child.extra() +} + +public protocol DefaultParent { func value() -> Int64 } +public protocol RefinedDefault: DefaultParent {} + +extension RefinedDefault { + public func value() -> Int64 { 7 } +} + +public func callRefinedDefault(_ child: some RefinedDefault) -> Int64 { + child.value() +} diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ProtocolDefaultsTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ProtocolDefaultsTest.java new file mode 100644 index 000000000..c59acda01 --- /dev/null +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ProtocolDefaultsTest.java @@ -0,0 +1,59 @@ +//===----------------------------------------------------------------------===// +// +// 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 static org.junit.jupiter.api.Assertions.*; + +public class ProtocolDefaultsTest { + @Test + void javaCallbacksInheritSwiftDefaults() throws Exception { + DefaultOperations operations = () -> 10; + assertEquals(12, operations.add(2)); + assertEquals(12, operations.total()); + assertEquals(12, MySwiftLibrary.callDefaultOperations(operations)); + assertEquals(12, operations.checked(false)); + assertThrows(GreeterError.class, () -> operations.checked(true)); + } + + @Test + void defaultsPreserveJavaOverridesOfOtherDefaultedRequirements() throws Exception { + DefaultOperations operations = new DefaultOperations() { + public long seed() { return 10; } + public long add(long value) { return 100 + value; } + }; + assertEquals(102, operations.total()); + assertEquals(102, operations.checked(false)); + assertEquals(102, MySwiftLibrary.callDefaultOperations(operations)); + } + + @Test + void nestedProtocolsUseTheirInheritedJavaCallback() { + DefaultNamespace.Child child = () -> 4; + assertEquals(7, child.extra()); + assertEquals(7, MySwiftLibrary.callNestedDefaults(child)); + DefaultNamespace.Parent parent = child; + assertEquals(4, parent.seed()); + } + + @Test + void refinedProtocolsCanDefaultAnInheritedRequirement() { + RefinedDefault child = new RefinedDefault() {}; + assertEquals(7, child.value()); + assertEquals(7, MySwiftLibrary.callRefinedDefault(child)); + DefaultParent parent = child; + assertEquals(7, parent.value()); + } +} diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ProtocolRequirementNamesTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ProtocolRequirementNamesTest.java index 297f49c6f..42eacbd04 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ProtocolRequirementNamesTest.java +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ProtocolRequirementNamesTest.java @@ -21,7 +21,9 @@ public class ProtocolRequirementNamesTest { @Test void javaCallbackOverridesTheDefaultImplementation() { - NamingCallback callback = () -> "Java"; + NamingCallback callback = new NamingCallback() { + public String message() { return "Java"; } + }; assertEquals("Java", MySwiftLibrary.callNamingCallback(callback)); } diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift index e83d3847b..e7378278f 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift @@ -189,7 +189,7 @@ extension JNISwift2JavaGenerator { } private func printProtocol(_ printer: inout JavaPrinter, _ decl: ExtractedNominalType) { - var extends = self.inheritedProtocols(of: decl).map(\.effectiveJavaSimpleName) + var extends = self.inheritedProtocols(of: decl).map(\.effectiveJavaName) // If we cannot generate Swift wrappers // that allows the user to implement the wrapped interface in Java @@ -210,11 +210,52 @@ extension JNISwift2JavaGenerator { // (`supportedProtocolRequirements(of:)`) so the interface never // declares a requirement (e.g. a property setter) that the box // doesn't implement. - for requirement in self.supportedProtocolRequirements(of: decl) { - printFunctionDowncallMethods(&printer, requirement, skipMethodBody: true) + let defaults = protocolDefaultMethods(of: decl) + var requirements = self.supportedProtocolRequirements(of: decl) + // A refined protocol may supply a default for an inherited requirement. + for method in defaults where !requirements.contains(where: { sameProtocolSignature($0, method.requirement) }) { + requirements.append(method.requirement) + } + for requirement in requirements { + if let method = defaults.first(where: { sameProtocolSignature($0.requirement, requirement) }), + let target = translatedDecl(for: method.helper), let original = translatedDecl(for: requirement) + { + printJavaBindingWrapperMethod( + &printer, + original, + importedFunc: requirement, + skipMethodBody: false, + protocolDefaultTarget: target + ) + } else { + printFunctionDowncallMethods(&printer, requirement, skipMethodBody: true) + } printer.println() } } + + let defaults = protocolDefaultMethods(of: decl) + if !defaults.isEmpty { + // Java interfaces cannot declare native methods. Keep the native entry + // points in a package-private companion, initialized on first default call. + printer.println() + let staticModifier = decl.parent == nil ? "" : "static " + printer.printBraceBlock("\(staticModifier)final class \(decl.effectiveJavaSimpleName)$SwiftDefaults") { printer in + printer.printBraceBlock("static") { printer in + if let loading = config.overrideStaticBlockLibraryLoading { + for line in loading { printer.print(line) } + } else { + printer.print("SwiftLibraries.loadLibraryWithFallbacks(SwiftLibraries.LIB_NAME_SWIFT_JAVA);") + printer.print("SwiftLibraries.loadLibraryWithFallbacks(\"\(config.nativeLibraryName ?? swiftModuleName)\");") + } + } + for method in defaults { + if let translated = translatedDecl(for: method.helper) { + printJavaBindingWrapperMethod(&printer, translated, skipMethodBody: false) + } + } + } + } } /// Prints the full `.java` file for a protocol's existential box — the @@ -241,7 +282,7 @@ extension JNISwift2JavaGenerator { /// (dynamic) value pointer and the concrete (dynamic) type-metadata pointer. private func printExistentialBox(_ printer: inout JavaPrinter, _ decl: ExtractedNominalType) { let boxName = decl.swiftNominal.javaExistentialBoxName - let implementsClause = "JNISwiftInstance, \(decl.effectiveJavaSimpleName)" + let implementsClause = "JNISwiftInstance, \(decl.effectiveJavaName)" printer.printBraceBlock("final class \(boxName) implements \(implementsClause)") { printer in printDesignatedConstructor(&printer, javaName: boxName, pointerParams: ["selfPointer", "selfTypePointer"]) @@ -353,8 +394,19 @@ extension JNISwift2JavaGenerator { type.parent == decl.swiftNominal } - for nestedType in nestedTypes { - printConcreteType(&printer, nestedType.value) + for (_, nestedType) in nestedTypes.sorted(by: { $0.key < $1.key }) { + let identifiers = currentJavaIdentifiers + resetForNewOutputFile( + nestedType.swiftNominal.kind == .protocol + ? allProtocolRequirementMethods(of: nestedType) + : nestedType.initializers + nestedType.variables + nestedType.methods + ) + if nestedType.swiftNominal.kind == .protocol { + printProtocol(&printer, nestedType) + } else { + printConcreteType(&printer, nestedType) + } + currentJavaIdentifiers = identifiers printer.println() } @@ -880,8 +932,10 @@ extension JNISwift2JavaGenerator { _ translatedDecl: TranslatedFunctionDecl, importedFunc: ExtractedFunc? = nil, skipMethodBody: Bool, + protocolDefaultTarget: TranslatedFunctionDecl? = nil, ) { var modifiers = ["public"] + if protocolDefaultTarget != nil { modifiers.append("default") } if translatedDecl.isStatic { modifiers.append("static") } @@ -931,7 +985,7 @@ extension JNISwift2JavaGenerator { var modifiers = modifiers // If we are a protocol, we emit this as default method - if isParentProtocol { + if isParentProtocol && !modifiers.contains("default") { modifiers.insert("default", at: 1) } @@ -967,10 +1021,17 @@ extension JNISwift2JavaGenerator { printer.print("\(signature);") } else { printer.printBraceBlock(signature) { printer in - printDowncall(&printer, translatedDecl) + if let target = protocolDefaultTarget { + var arguments = ["this"] + translatedSignature.parameters.map(\.parameter.name) + if translatedSignature.requiresSwiftArena { arguments.append("swiftArena") } + let call = "\(target.parentName).\(target.name)(\(arguments.joined(separator: .comma)))" + printer.print((resultType.isVoid ? "" : "return ") + call + ";") + } else { + printDowncall(&printer, translatedDecl) + } } - printNativeFunction(&printer, translatedDecl) + if protocolDefaultTarget == nil { printNativeFunction(&printer, translatedDecl) } } } diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+ProtocolDefaults.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+ProtocolDefaults.swift new file mode 100644 index 000000000..74163993c --- /dev/null +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+ProtocolDefaults.swift @@ -0,0 +1,139 @@ +//===----------------------------------------------------------------------===// +// +// 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 SwiftExtract +import SwiftSyntax + +extension JNISwift2JavaGenerator { + struct ProtocolDefaultMethod { + let requirement: ExtractedFunc + let helper: ExtractedFunc + let adapterName: String + } + + /// Compare API signatures rather than local parameter names, which need not + /// agree between a requirement and its extension implementation. + func sameProtocolSignature(_ lhs: ExtractedFunc, _ rhs: ExtractedFunc) -> Bool { + lhs.name == rhs.name && lhs.apiKind == rhs.apiKind + && lhs.functionSignature.result == rhs.functionSignature.result + && lhs.functionSignature.effectSpecifiers == rhs.functionSignature.effectSpecifiers + && lhs.functionSignature.parameters.count == rhs.functionSignature.parameters.count + && zip(lhs.functionSignature.parameters, rhs.functionSignature.parameters).allSatisfy { + $0.argumentLabel == $1.argumentLabel && $0.type == $1.type + && $0.convention == $1.convention && $0.isVariadic == $1.isVariadic + } + } + + /// Only unconditional defaults are valid for every Java implementor. + private func isUnconditionalProtocolDefault(_ method: ExtractedFunc) -> Bool { + guard !method.isProtocolRequirement, !method.isStatic, + method.attributeList?.isEmpty != false, + method.functionSignature.genericParameters.isEmpty, + method.functionSignature.genericRequirements.isEmpty + else { return false } + var parent = method.swiftDecl.parent + while let node = parent { + if let ext = node.as(ExtensionDeclSyntax.self) { + return ext.genericWhereClause == nil && ext.attributes.isEmpty + } + parent = node.parent + } + return false + } + + func protocolDefaultMethods(of type: ExtractedNominalType) -> [ProtocolDefaultMethod] { + if let cached = protocolDefaultMethodsCache[type] { return cached } + // Reuse the callback translator's capability checks. Unsupported protocols + // keep abstract Java requirements rather than acquiring invalid defaults. + guard interfaceProtocolWrappers[type] != nil, type.attributeList.isEmpty else { return [] } + let requirements = allProtocolRequirementMethods(of: type) + guard + requirements.allSatisfy({ method in + guard case .instance(.byValue, _) = method.functionSignature.selfParameter else { return false } + return method.apiKind == .function && !method.isAsync && !method.isIsolated + && method.attributeList?.isEmpty != false + && method.parentType?.asNominalType?.nominalTypeDecl.syntax.attributes.isEmpty != false + && method.functionSignature.genericParameters.isEmpty + && !method.functionSignature.hasVariadicParams + }) + else { return [] } + let candidates = type.methods.filter(isUnconditionalProtocolDefault) + var defaults: [ProtocolDefaultMethod] = [] + for requirement in requirements { + guard candidates.contains(where: { sameProtocolSignature($0, requirement) }), + case .instance(.byValue, _) = requirement.functionSignature.selfParameter + else { continue } + let index = defaults.count + let name = "__swiftDefault_\(type.effectiveOutputTypeName.fullFlatName)_\(index)" + var signature = requirement.functionSignature + signature.selfParameter = nil + signature.parameters = + [ + SwiftParameter( + convention: .byValue, + parameterName: "receiver", + type: .existential(type.swiftType) + ) + ] + + signature.parameters.enumerated().map { index, parameter in + var parameter = parameter + parameter.argumentLabel = nil + parameter.parameterName = "p\(index)" + return parameter + } + let helper = ExtractedFunc( + module: swiftModuleName, + swiftDecl: requirement.swiftDecl, + name: name, + apiKind: .function, + functionSignature: signature, + sourceFilePath: requirement.sourceFilePath + ) + guard var translated = translatedDecl(for: helper) else { continue } + var companionName = type.effectiveJavaTypeName.components + companionName[companionName.count - 1] += "$SwiftDefaults" + translated.parentName = SwiftQualifiedTypeName(companionName) + translatedDecls[helper] = translated + defaults.append( + ProtocolDefaultMethod( + requirement: requirement, + helper: helper, + adapterName: name + "_Receiver" + ) + ) + } + protocolDefaultMethodsCache[type] = defaults + return defaults + } + + func protocolDefaultParameters(_ method: ExtractedFunc) -> [SwiftParameter] { + method.functionSignature.parameters.enumerated().map { index, parameter in + var parameter = parameter + parameter.parameterName = "p\(index)" + return parameter + } + } + + func protocolDefaultArguments(_ method: ExtractedFunc) -> String { + protocolDefaultParameters(method).map { parameter in + let label = parameter.argumentLabel.map { "\($0): " } ?? "" + return label + (parameter.isInout ? "&" : "") + parameter.name + }.joined(separator: ", ") + } + + func protocolDefaultEffects(_ method: ExtractedFunc) -> String { + if let error = method.functionSignature.thrownTypedError { return " throws(\(error))" } + return method.isThrowing ? " throws" : "" + } +} diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift index cb9a31f40..819a5d1af 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift @@ -194,7 +194,7 @@ extension JNISwift2JavaGenerator { try printer.printBraceBlock("extension \(translatedWrapper.wrapperName)") { printer in for inherited in inheritedWrappers { printer.printBraceBlock("var \(inherited.javaInterfaceVariableName): \(inherited.javaInterfaceName)") { printer in - printer.print("\(translatedWrapper.javaInterfaceVariableName)") + printer.print("\(inherited.javaInterfaceName)(javaHolder: \(translatedWrapper.javaInterfaceVariableName).javaHolder)") } printer.println() } @@ -204,6 +204,24 @@ extension JNISwift2JavaGenerator { printer.println() } + // A refinement default competes with the parent callback wrapper's witness. + // Emit an explicit forwarding witness on this more specific wrapper so + // calls still reach the original Java receiver. + for method in protocolDefaultMethods(of: translatedWrapper.importedType) { + let signature = method.requirement.functionSignature + if translatedWrapper.functions.contains(where: { $0.originalFunctionSignature == signature }) { + continue + } + guard let parent = method.requirement.parentType, + let declaringType = asExtractedNominalTypeDecl(parent), + let function = interfaceProtocolWrappers[declaringType]?.functions.first(where: { + $0.originalFunctionSignature == signature + }) + else { continue } + try 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) @@ -218,7 +236,7 @@ extension JNISwift2JavaGenerator { inside wrapper: JavaInterfaceSwiftWrapper, ) throws { guard - let protocolMethod = wrapper.importedType.methods.first(where: { + let protocolMethod = allProtocolRequirementMethods(of: wrapper.importedType).first(where: { $0.functionSignature == function.originalFunctionSignature }) else { @@ -249,7 +267,9 @@ extension JNISwift2JavaGenerator { function.parameterConversions, ).map { param, conversion in // Wrap-java does not extract parameter names, so no labels - conversion.render(&printer, param.parameterName!) + let converted = conversion.render(&printer, param.parameterName!) + // Java long is projected by wrap-java as Int64, including Swift Int. + return param.type.asNominalType?.asKnownType == .int ? "Int64(\(converted))" : converted } // If the underlying translated method requires @@ -258,11 +278,12 @@ extension JNISwift2JavaGenerator { upcallArguments.append("JavaSwiftArena.defaultAutoArena") } - let tryClause = function.originalFunctionSignature.isThrowing ? "try " : "" + let tryClause = translatedDecl.throwsClause().isEmpty ? "" : "try " let javaUpcall = "\(tryClause)\(wrapper.javaInterfaceVariableName).\(function.swiftFunctionName)(\(upcallArguments.joined(separator: .comma)))" - let result = function.resultConversion.render(&printer, javaUpcall) + let converted = function.resultConversion.render(&printer, javaUpcall) + let result = resultType.asNominalType?.asKnownType == .int ? "Int(\(converted))" : converted printer.print("\(returnStmt)\(result)") } } @@ -381,6 +402,36 @@ extension JNISwift2JavaGenerator { try printSwiftInterfaceWrapper(&printer, protocolWrapper) } + for method in protocolDefaultMethods(of: type) { + // Omit exactly this requirement. Swift then selects its extension default; + // all other requirements still dispatch to the original receiver, including + // Java overrides of other default methods. Calling the original witness + // directly here would recurse through the Java interface default method. + printer.printBraceBlock("private final class \(method.adapterName): \(type.effectiveSwiftTypeName)") { printer in + printer.print("let base: any \(type.effectiveSwiftTypeName)") + printer.printBraceBlock("init(base: any \(type.effectiveSwiftTypeName))") { printer in + printer.print("self.base = base") + } + for forwarded in allProtocolRequirementMethods(of: type) + where !sameProtocolSignature(forwarded, method.requirement) { + let parameters = protocolDefaultParameters(forwarded).map(\.description).joined(separator: ", ") + let result = forwarded.functionSignature.result.type + printer.printBraceBlock("func `\(forwarded.name)`(\(parameters))\(protocolDefaultEffects(forwarded)) -> \(result)") { printer in + let prefix = (result.isVoid ? "" : "return ") + (forwarded.isThrowing ? "try " : "") + printer.print("\(prefix)base.`\(forwarded.name)`(\(protocolDefaultArguments(forwarded)))") + } + } + } + let parameters = method.helper.functionSignature.parameters.map(\.description).joined(separator: ", ") + let result = method.helper.functionSignature.result.type + printer.printBraceBlock("private func \(method.helper.name)(\(parameters))\(protocolDefaultEffects(method.helper)) -> \(result)") { printer in + printer.print("let value = \(method.adapterName)(base: receiver)") + let prefix = (result.isVoid ? "" : "return ") + (method.requirement.isThrowing ? "try " : "") + printer.print("\(prefix)value.`\(method.requirement.name)`(\(protocolDefaultArguments(method.requirement)))") + } + printSwiftFunctionThunk(&printer, method.helper) + } + // Independent of `interfaceProtocolWrappers`/`enableJavaCallbacks` — this // is the opposite direction (Swift-implements / Java-receives a // downcall), so it must not be gated on the upcall machinery. @@ -1315,7 +1366,7 @@ extension SwiftNominalTypeDeclaration { /// The name of the corresponding `@JavaInterface` of this type. var javaInterfaceName: String { - "Java\(safeProtocolName)" + "Java\(qualifiedName)" } var javaInterfaceSwiftProtocolWrapperName: String { @@ -1323,7 +1374,7 @@ extension SwiftNominalTypeDeclaration { } var javaInterfaceVariableName: String { - "_\(javaInterfaceName.firstCharacterLowercased)Interface" + "_java\(safeProtocolName)Interface" } /// The name of the generated Java class that boxes a value returned as diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift index 1320bf29f..fa0ae4a2c 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift @@ -73,6 +73,7 @@ package class JNISwift2JavaGenerator: Swift2JavaGenerator { /// Since Swift files in SwiftPM builds needs to be unique, we use this fact to flatten paths into plain names here. /// For uniqueness checking "did we write this file already", just checking the name should be sufficient. var expectedOutputSwiftFileNames: Set + var protocolDefaultMethodsCache: [ExtractedNominalType: [ProtocolDefaultMethod]] = [:] package init( config: Configuration, diff --git a/Sources/SwiftJavaDocumentation/Documentation.docc/FeaturesJextract.md b/Sources/SwiftJavaDocumentation/Documentation.docc/FeaturesJextract.md index a359f7db2..f5ce9c28c 100644 --- a/Sources/SwiftJavaDocumentation/Documentation.docc/FeaturesJextract.md +++ b/Sources/SwiftJavaDocumentation/Documentation.docc/FeaturesJextract.md @@ -568,6 +568,21 @@ initializers declared in protocol extensions retain their Swift implementations; they do not add requirements to the Java interface or prevent an otherwise supported protocol from being implemented in Java. +When Java callbacks are enabled, an unconditional extension implementation of a +supported synchronous instance-method requirement is exposed as a Java interface +`default` method. Java implementations can inherit that Swift implementation or +override it. Calls from a Swift default to other requirements continue to dispatch +to the original Java receiver, including its overrides of other default methods. +Refined protocols can provide defaults for inherited requirements, and nested +protocols retain their qualified Java names. + +These bridges require a callback-compatible protocol whose requirements are +synchronous, nonisolated, nongeneric instance methods. Attributed declarations (including global-actor isolation), conditional extension +implementations, property requirements, and extension-only convenience members do +not acquire default bridges. A protocol whose every abstract method becomes a +default is no longer a Java functional interface; implement it with an anonymous +class rather than a lambda. + > Note: `any DataProtocol` is handled as `Foundation.Data` in FFM mode; see below. @TabNavigator { diff --git a/Tests/JExtractSwiftTests/JNI/JNIProtocolDefaultMethodTests.swift b/Tests/JExtractSwiftTests/JNI/JNIProtocolDefaultMethodTests.swift new file mode 100644 index 000000000..c94ecd0ca --- /dev/null +++ b/Tests/JExtractSwiftTests/JNI/JNIProtocolDefaultMethodTests.swift @@ -0,0 +1,168 @@ +//===----------------------------------------------------------------------===// +// +// 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 SwiftJavaConfigurationShared +import Testing + +@Suite +struct JNIProtocolDefaultMethodTests { + private var config: Configuration { + var config = Configuration() + config.enableJavaCallbacks = true + return config + } + + private let source = """ + public protocol Methods { + func seed() -> Int + func add(_ value: Int) -> Int + func total() -> Int + } + extension Methods { + public func add(_ renamed: Int) -> Int { seed() + renamed } + public func total() -> Int { add(2) } + } + """ + + @Test("Requirement defaults become Java default methods") + func javaDefaultMethods() throws { + try assertOutput( + input: source, + config: config, + .jni, + .java, + expectedChunks: [ + "public long seed();", + "public default long add(long value) throws SwiftIntegerOverflowException {", + "public default long total() {", + "final class Methods$SwiftDefaults {", + "return Methods$SwiftDefaults.__swiftDefault_Methods_0(this, value);", + ], + notExpectedChunks: ["public long add(long value);"] + ) + } + + @Test("Default receivers omit their own witness and forward other requirements") + func swiftDefaultReceivers() throws { + try assertOutput( + input: source, + config: config, + .jni, + .swift, + expectedChunks: [ + "private final class __swiftDefault_Methods_0_Receiver: Methods {", + "let value = __swiftDefault_Methods_0_Receiver(base: receiver)", + "return value.`add`(p0)", + "return base.`total`()", + ] + ) + } + + @Test("Nested protocols retain qualified Java names") + func nestedProtocols() throws { + try assertOutput( + input: """ + public enum Namespace { + public protocol Parent { func seed() -> Int64 } + public protocol Child: Parent { func message() -> String } + } + extension Namespace.Child { public func message() -> String { "default" } } + """, + config: config, + .jni, + .java, + expectedChunks: [ + "public interface Child extends Namespace.Parent, SwiftDowncastable {", + "public default java.lang.String message() {", + "static final class Child$SwiftDefaults {", + ] + ) + } + + @Test("Refinements can supply defaults for inherited requirements") + func refinedDefault() throws { + let source = """ + public protocol Parent { func value() -> Int64 } + public protocol Child: Parent {} + extension Child { public func value() -> Int64 { 7 } } + """ + try assertOutput( + input: source, + config: config, + .jni, + .java, + expectedChunks: ["public default long value() {"] + ) + try assertOutput( + input: source, + config: config, + .jni, + .swift, + expectedChunks: ["return _javaChildInterface.value()"] + ) + } + + @Test("Conditional defaults remain abstract") + func conditionalDefaults() throws { + try assertOutput( + input: """ + public protocol Callback { func message() -> String } + extension Callback where Self: AnyObject { + public func message() -> String { "conditional" } + } + """, + config: config, + .jni, + .java, + expectedChunks: ["public java.lang.String message();"], + notExpectedChunks: ["public default java.lang.String message", "$SwiftDefaults"] + ) + } + + @Test("Disabled callbacks do not acquire default bridges") + func disabledCallbacks() throws { + var config = config + config.enableJavaCallbacks = false + try assertOutput( + input: source, + config: config, + .jni, + .java, + expectedChunks: ["public long add(long value) throws SwiftIntegerOverflowException;"], + notExpectedChunks: ["$SwiftDefaults", "public default long add"] + ) + } + @Test( + "Unsupported and actor-isolated protocols retain abstract requirements", + arguments: [ + "@MainActor public protocol Callback { func message() -> String }; extension Callback { public func message() -> String { \"default\" } }", + "public protocol Callback { @MainActor func message() -> String }; extension Callback { @MainActor public func message() -> String { \"default\" } }", + "public protocol Callback { func message() -> String }; @MainActor extension Callback { public func message() -> String { \"default\" } }", + "public protocol Callback { func message() -> String; var value: Int64 { get } }; extension Callback { public func message() -> String { \"default\" } }", + "public protocol Callback { func message() async -> String }; extension Callback { public func message() async -> String { \"default\" } }", + ] + ) + func unsupportedDefaults(source: String) throws { + try assertOutput( + input: source, + config: config, + .jni, + .java, + expectedChunks: [], + notExpectedChunks: ["$SwiftDefaults", "public default java.lang.String message"] + ) + } + +} diff --git a/Tests/JExtractSwiftTests/JNI/JNIProtocolRequirementNamesTests.swift b/Tests/JExtractSwiftTests/JNI/JNIProtocolRequirementNamesTests.swift index f6d4c8ae9..ddd9c8d79 100644 --- a/Tests/JExtractSwiftTests/JNI/JNIProtocolRequirementNamesTests.swift +++ b/Tests/JExtractSwiftTests/JNI/JNIProtocolRequirementNamesTests.swift @@ -31,7 +31,7 @@ struct JNIProtocolRequirementNamesTests { config: config, .jni, .java, - expectedChunks: ["public java.lang.String message();", "public java.lang.String message() {"], + expectedChunks: ["public default java.lang.String message() {", "public java.lang.String message() {"], notExpectedChunks: ["message_()"] ) } diff --git a/Tests/JExtractSwiftTests/JNI/JNIProtocolTests.swift b/Tests/JExtractSwiftTests/JNI/JNIProtocolTests.swift index eef6119d9..f7ae2dda6 100644 --- a/Tests/JExtractSwiftTests/JNI/JNIProtocolTests.swift +++ b/Tests/JExtractSwiftTests/JNI/JNIProtocolTests.swift @@ -439,7 +439,7 @@ struct JNIProtocolTests { """ extension SwiftJavaChildProtocolWrapper { var _javaParentProtocolInterface: JavaParentProtocol { - _javaChildProtocolInterface + JavaParentProtocol(javaHolder: _javaChildProtocolInterface.javaHolder) } public func childMethod() { ...