From c47f64619346f418d9c278743b77ed2d8756ac19 Mon Sep 17 00:00:00 2001 From: Himeshi Date: Fri, 23 Oct 2026 15:24:14 +0900 Subject: [PATCH] JNI: align inherited protocol requirement names across bindings Disambiguate protocol method names using actual requirements, and preserve the declaring interface's names for inherited box methods and native thunks. Avoid caching provisional names while probing translation support. Add regression and Java runtime tests covering default implementations, callbacks, and overloads introduced by a refined protocol. --- .../ProtocolRequirementNames.swift | 48 +++++++++++ .../swift/ProtocolRequirementNamesTest.java | 45 +++++++++++ ...t2JavaGenerator+JavaBindingsPrinting.swift | 3 +- ...ift2JavaGenerator+SwiftThunkPrinting.swift | 3 +- .../JNI/JNISwift2JavaGenerator.swift | 16 +++- .../JNIProtocolRequirementNamesTests.swift | 80 +++++++++++++++++++ 6 files changed, 192 insertions(+), 3 deletions(-) create mode 100644 Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/ProtocolRequirementNames.swift create mode 100644 Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ProtocolRequirementNamesTest.java create mode 100644 Tests/JExtractSwiftTests/JNI/JNIProtocolRequirementNamesTests.swift 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__"] + ) + } +}