diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/ProtocolRequirementNames.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/ProtocolRequirementNames.swift new file mode 100644 index 000000000..d3f990be9 --- /dev/null +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/ProtocolRequirementNames.swift @@ -0,0 +1,48 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2026 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 NamingCallback { + func message() -> String +} + +extension NamingCallback { + public func message() -> String { "default" } +} + +struct DefaultNamingCallback: NamingCallback {} + +public func callNamingCallback(_ callback: some NamingCallback) -> String { + callback.message() +} + +public func makeNamingCallback() -> any NamingCallback { DefaultNamingCallback() } + +public protocol NamingParent { + var id: Int64 { get } + func value(number: Int64) -> Int64 +} + +public protocol NamingChild: NamingParent { + var tag: Int64 { get } + func value(text: Int64) -> Int64 +} + +struct NamingValue: NamingChild { + var id: Int64 { 1 } + var tag: Int64 { 2 } + func value(number: Int64) -> Int64 { number + 10 } + func value(text: Int64) -> Int64 { text + 20 } +} + +public func makeNamingChild() -> any NamingChild { NamingValue() } diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ProtocolRequirementNamesTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ProtocolRequirementNamesTest.java new file mode 100644 index 000000000..297f49c6f --- /dev/null +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ProtocolRequirementNamesTest.java @@ -0,0 +1,45 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2026 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 static org.junit.jupiter.api.Assertions.assertEquals; + +public class ProtocolRequirementNamesTest { + @Test + void javaCallbackOverridesTheDefaultImplementation() { + NamingCallback callback = () -> "Java"; + assertEquals("Java", MySwiftLibrary.callNamingCallback(callback)); + } + + @Test + void returnedBoxUsesTheDefaultImplementation() { + try (var arena = SwiftArena.ofConfined()) { + assertEquals("default", MySwiftLibrary.makeNamingCallback(arena).message()); + } + } + + @Test + void inheritedOverloadsDispatchToTheCorrectWitness() { + try (var arena = SwiftArena.ofConfined()) { + NamingChild child = MySwiftLibrary.makeNamingChild(arena); + assertEquals(1, child.getId()); + assertEquals(2, child.getTag()); + assertEquals(13, child.value(3)); + assertEquals(23, child.valueText(3)); + } + } +} diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift index 504514f57..e83d3847b 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift @@ -173,7 +173,7 @@ extension JNISwift2JavaGenerator { let filename = "\(decl.effectiveJavaSimpleName).java" logger.debug("Printing contents: \(filename)") - resetForNewOutputFile(decl.initializers + decl.variables + decl.methods) + resetForNewOutputFile(decl.swiftNominal.kind == .protocol ? self.allProtocolRequirementMethods(of: decl) : decl.initializers + decl.variables + decl.methods) printHeader(&printer) printPackage(&printer) @@ -283,6 +283,7 @@ extension JNISwift2JavaGenerator { printer.println() for method in self.allProtocolRequirementMethods(of: decl) { + self.currentJavaIdentifiers = identifiers(forProtocolRequirement: method) printExistentialBoxMethod(&printer, decl, method) printer.println() } diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift index c5ac7bba0..eea65d3bd 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift @@ -311,7 +311,7 @@ extension JNISwift2JavaGenerator { printer.println() self.currentJavaIdentifiers = JavaIdentifierFactory( - type.initializers + type.variables + type.methods + type.swiftNominal.kind == .protocol ? self.allProtocolRequirementMethods(of: type) : type.initializers + type.variables + type.methods ) switch type.swiftNominal.kind { @@ -396,6 +396,7 @@ extension JNISwift2JavaGenerator { let boxParentName = SwiftQualifiedTypeName(type.swiftNominal.javaExistentialBoxName) for method in self.allProtocolRequirementMethods(of: type) { + self.currentJavaIdentifiers = identifiers(forProtocolRequirement: method) guard var translated = try? self.javaTranslator.translate(method) else { self.logger.debug("Failed to translate protocol requirement for existential box dispatch thunk: \(method)") continue diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift index f775a293c..1320bf29f 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift @@ -203,11 +203,25 @@ extension JNISwift2JavaGenerator { /// (getters/setters), excluding statics and anything whose signature /// doesn't translate (e.g. referencing `Self`/associated types). func supportedProtocolRequirements(of type: ExtractedNominalType) -> [ExtractedFunc] { + // Probe support without caching names before the declaring interface's + // complete requirement set has been selected for overload disambiguation. (type.methods + type.variables).filter { requirement in - requirement.isProtocolRequirement && !requirement.isStatic && !requirement.isClass && self.translatedDecl(for: requirement) != nil + requirement.isProtocolRequirement && !requirement.isStatic && !requirement.isClass + && (try? self.javaTranslator.translate(requirement)) != nil } } + /// Use the declaring interface's names when implementing an inherited requirement. + /// A refined protocol may introduce overloads, but cannot rename a parent's API. + func identifiers(forProtocolRequirement requirement: ExtractedFunc) -> JavaIdentifierFactory { + guard let name = requirement.parentType?.asNominalType?.nominalTypeDecl.qualifiedName, + let owner = analysis.extractedTypes[name] + else { + fatalError("Missing declaring protocol for requirement: \(requirement)") + } + return JavaIdentifierFactory(allProtocolRequirementMethods(of: owner)) + } + /// All wrappable requirements for `type` (a protocol), including those /// inherited from refined protocols — the transitive closure of /// `supportedProtocolRequirements(of:)`. Used to build an existential diff --git a/Tests/JExtractSwiftTests/JNI/JNIProtocolRequirementNamesTests.swift b/Tests/JExtractSwiftTests/JNI/JNIProtocolRequirementNamesTests.swift new file mode 100644 index 000000000..f6d4c8ae9 --- /dev/null +++ b/Tests/JExtractSwiftTests/JNI/JNIProtocolRequirementNamesTests.swift @@ -0,0 +1,80 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2026 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 JNIProtocolRequirementNamesTests { + @Test("Default implementations do not rename Java requirements") + func defaultImplementation_java() throws { + var config = Configuration() + config.enableJavaCallbacks = true + try assertOutput( + input: """ + public protocol Callback { func message() -> String } + extension Callback { public func message() -> String { "default" } } + public func makeCallback() -> any Callback + """, + config: config, + .jni, + .java, + expectedChunks: ["public java.lang.String message();", "public java.lang.String message() {"], + notExpectedChunks: ["message_()"] + ) + } + + private let inheritedSource = """ + public protocol Parent { + var id: Int64 { get } + func value(number: Int64) -> Int64 + } + public protocol Child: Parent { + var tag: Int64 { get } + func value(text: Int64) -> Int64 + } + public func makeChild() -> any Child + """ + + @Test("Inherited box methods retain the declaring interface's names") + func inheritedRequirements_java() throws { + try assertOutput( + input: inheritedSource, + .jni, + .java, + expectedChunks: [ + "public long value(long number);", + "public long valueText(long text);", + "public long value(long number) {", + "public long valueText(long text) {", + ], + notExpectedChunks: ["valueNumber("] + ) + } + + @Test("Inherited box dispatch thunks use the same names as Java") + func inheritedRequirements_swift() throws { + try assertOutput( + input: inheritedSource, + .jni, + .swift, + expectedChunks: [ + "@_cdecl(\"Java_com_example_swift_ChildBox__00024value__JJJ\")", + "@_cdecl(\"Java_com_example_swift_ChildBox__00024valueText__JJJ\")", + ], + notExpectedChunks: ["ChildBox__00024valueNumber__"] + ) + } +}