diff --git a/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftLibrary.swift b/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftLibrary.swift index b8b19d902..95420d0ed 100644 --- a/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftLibrary.swift +++ b/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/MySwiftLibrary.swift @@ -101,6 +101,17 @@ public func globalReceiveOptional(o1: Int?, o2: (some DataProtocol)?) -> Int { } } +// ==== ----------------------------------------------------------------------- +// MARK: Overloaded functions + +public func globalOverloaded(a: Int) { + p("globalOverloaded(a: \(a))") +} + +public func globalOverloaded(b: Int) { + p("globalOverloaded(b: \(b))") +} + // ==== Internal helpers func p(_ msg: String, file: String = #fileID, line: UInt = #line, function: String = #function) { 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..62c29482a 100644 --- a/Samples/SwiftJavaExtractFFMSampleApp/src/main/java/com/example/swift/HelloJava2Swift.java +++ b/Samples/SwiftJavaExtractFFMSampleApp/src/main/java/com/example/swift/HelloJava2Swift.java @@ -101,6 +101,10 @@ static void examples() { } + // Overloaded functions with label-based disambiguation + MySwiftLibrary.globalOverloadedA(100); + MySwiftLibrary.globalOverloadedB(200); + System.out.println("DONE."); } diff --git a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift index 166709d3a..4c474cca9 100644 --- a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift +++ b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift @@ -27,7 +27,8 @@ extension FFMSwift2JavaGenerator { do { let translation = JavaTranslation( config: self.config, - knownTypes: SwiftKnownTypes(symbolTable: lookupContext.symbolTable) + knownTypes: SwiftKnownTypes(symbolTable: lookupContext.symbolTable), + javaIdentifiers: self.currentJavaIdentifiers ) translated = try translation.translate(decl) } catch { @@ -142,13 +143,18 @@ extension FFMSwift2JavaGenerator { } } + // ==== ------------------------------------------------------------------- + // MARK: Java translation + struct JavaTranslation { let config: Configuration var knownTypes: SwiftKnownTypes + var javaIdentifiers: JavaIdentifierFactory - init(config: Configuration, knownTypes: SwiftKnownTypes) { + init(config: Configuration, knownTypes: SwiftKnownTypes, javaIdentifiers: JavaIdentifierFactory) { self.config = config self.knownTypes = knownTypes + self.javaIdentifiers = javaIdentifiers } func translate(_ decl: ImportedFunc) throws -> TranslatedFunctionDecl { @@ -156,12 +162,7 @@ extension FFMSwift2JavaGenerator { let loweredSignature = try lowering.lowerFunctionSignature(decl.functionSignature) // Name. - let javaName = - switch decl.apiKind { - case .getter, .subscriptGetter: decl.javaGetterName - case .setter, .subscriptSetter: decl.javaSetterName - case .function, .initializer, .enumCase: decl.name - } + let javaName = javaIdentifiers.makeJavaMethodName(decl) // Signature. let translatedSignature = try translate(loweredFunctionSignature: loweredSignature, methodName: javaName) diff --git a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+SwiftThunkPrinting.swift b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+SwiftThunkPrinting.swift index 8880d3090..1d21f658e 100644 --- a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+SwiftThunkPrinting.swift +++ b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+SwiftThunkPrinting.swift @@ -123,6 +123,10 @@ extension FFMSwift2JavaGenerator { self.lookupContext.symbolTable.printImportedModules(&printer) + self.currentJavaIdentifiers = JavaIdentifierFactory( + self.analysis.importedGlobalFuncs + self.analysis.importedGlobalVariables + ) + for thunk in stt.renderGlobalThunks() { printer.print(thunk) printer.println() @@ -152,6 +156,10 @@ extension FFMSwift2JavaGenerator { self.lookupContext.symbolTable.printImportedModules(&printer) + self.currentJavaIdentifiers = JavaIdentifierFactory( + ty.initializers + ty.variables + ty.methods + ) + for thunk in stt.renderThunks(forType: ty) { printer.print("\(thunk)") printer.print("") diff --git a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator.swift b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator.swift index 57ec4e401..dd14ee8ea 100644 --- a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator.swift +++ b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator.swift @@ -39,6 +39,9 @@ package class FFMSwift2JavaGenerator: Swift2JavaGenerator { /// Cached Java translation result. 'nil' indicates failed translation. var translatedDecls: [ImportedFunc: TranslatedFunctionDecl?] = [:] + /// Duplicate identifier tracking for the current batch of methods being generated. + var currentJavaIdentifiers: JavaIdentifierFactory = JavaIdentifierFactory() + /// 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. /// @@ -170,6 +173,10 @@ extension FFMSwift2JavaGenerator { printPackage(&printer) printImports(&printer) + self.currentJavaIdentifiers = JavaIdentifierFactory( + self.analysis.importedGlobalFuncs + self.analysis.importedGlobalVariables + ) + printModuleClass(&printer) { printer in for decl in analysis.importedGlobalVariables { @@ -189,6 +196,10 @@ extension FFMSwift2JavaGenerator { printPackage(&printer) printImports(&printer) // TODO: we could have some imports be driven from types used in the generated decl + self.currentJavaIdentifiers = JavaIdentifierFactory( + decl.initializers + decl.variables + decl.methods + ) + printNominal(&printer, decl) { printer in // We use a static field to abuse the initialization order such that by the time we get type metadata, // we already have loaded the library where it will be obtained from. diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift index c27b6f308..759aa7418 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift @@ -93,6 +93,10 @@ extension JNISwift2JavaGenerator { printPackage(&printer) printImports(&printer) + self.currentJavaIdentifiers = JavaIdentifierFactory( + self.analysis.importedGlobalFuncs + self.analysis.importedGlobalVariables + ) + printModuleClass(&printer) { printer in printer.print( """ @@ -124,6 +128,10 @@ extension JNISwift2JavaGenerator { printPackage(&printer) printImports(&printer) + self.currentJavaIdentifiers = JavaIdentifierFactory( + decl.initializers + decl.variables + decl.methods + ) + switch decl.swiftNominal.kind { case .actor, .class, .enum, .struct: printConcreteType(&printer, decl) diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift index 5212d56f0..966230a55 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift @@ -25,7 +25,8 @@ extension JNISwift2JavaGenerator { javaClassLookupTable: self.javaClassLookupTable, knownTypes: SwiftKnownTypes(symbolTable: lookupContext.symbolTable), protocolWrappers: self.interfaceProtocolWrappers, - logger: self.logger + logger: self.logger, + javaIdentifiers: self.currentJavaIdentifiers ) } @@ -64,7 +65,8 @@ extension JNISwift2JavaGenerator { javaClassLookupTable: self.javaClassLookupTable, knownTypes: SwiftKnownTypes(symbolTable: lookupContext.symbolTable), protocolWrappers: self.interfaceProtocolWrappers, - logger: self.logger + logger: self.logger, + javaIdentifiers: self.currentJavaIdentifiers ) translated = try translation.translate(enumCase: decl) } catch { @@ -84,6 +86,7 @@ extension JNISwift2JavaGenerator { var knownTypes: SwiftKnownTypes let protocolWrappers: [ImportedNominalType: JavaInterfaceSwiftWrapper] let logger: Logger + var javaIdentifiers: JavaIdentifierFactory func translate(enumCase: ImportedEnumCase) throws -> TranslatedEnumCase { let nativeTranslation = NativeJavaTranslation( @@ -226,12 +229,7 @@ extension JNISwift2JavaGenerator { let parentName = decl.parentType?.asNominalType?.nominalTypeDecl.qualifiedName ?? swiftModuleName // Name. - let javaName = - switch decl.apiKind { - case .getter, .subscriptGetter: decl.javaGetterName - case .setter, .subscriptSetter: decl.javaSetterName - case .function, .initializer, .enumCase: decl.name - } + let javaName = javaIdentifiers.makeJavaMethodName(decl) // Swift -> Java var translatedFunctionSignature = try translate( diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift index 3d0c97b75..a6917227a 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift @@ -44,6 +44,9 @@ package class JNISwift2JavaGenerator: Swift2JavaGenerator { var translatedEnumCases: [ImportedEnumCase: TranslatedEnumCase] = [:] var interfaceProtocolWrappers: [ImportedNominalType: JavaInterfaceSwiftWrapper] = [:] + /// Duplicate identifier tracking for the current batch of methods being generated. + var currentJavaIdentifiers: JavaIdentifierFactory = JavaIdentifierFactory() + /// Because we need to write empty files for SwiftPM, keep track which files we didn't write yet, /// and write an empty file for those. /// diff --git a/Sources/JExtractSwiftLib/JavaIdentifierFactory.swift b/Sources/JExtractSwiftLib/JavaIdentifierFactory.swift new file mode 100644 index 000000000..68fdb9c8f --- /dev/null +++ b/Sources/JExtractSwiftLib/JavaIdentifierFactory.swift @@ -0,0 +1,89 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +/// Detects Java method name conflicts caused by Swift overloads that differ +/// only in parameter labels. When a conflict is detected, the affected methods +/// get a camelCase suffix derived from their parameter labels (e.g. `takeValueA`, +/// `takeValueB`) so that Java can distinguish them. +package struct JavaIdentifierFactory { + private var duplicates: Set = [] + + package init() {} + + package init(_ methods: [ImportedFunc]) { + self.init() + record(methods) + } + + /// Analyze the given methods and record any base names that have conflicts. + private mutating func record(_ methods: [ImportedFunc]) { + // Group methods by their Java base name. + var methodsByBaseName: [String: [ImportedFunc]] = [:] + for method in methods { + let baseName: String = + switch method.apiKind { + case .getter, .subscriptGetter: method.javaGetterName + case .setter, .subscriptSetter: method.javaSetterName + case .function, .initializer, .enumCase: method.name + } + methodsByBaseName[baseName, default: []].append(method) + } + + // For each group with 2+ methods, check if any two share the same + // Swift parameter types (which means identical Java parameter types). + for (baseName, group) in methodsByBaseName where group.count > 1 { + var seenSignatures: Set = [] + for method in group { + let key = method.functionSignature.parameters + .map { $0.type.description } + .joined(separator: ",") + if !seenSignatures.insert(key).inserted { + duplicates.insert(baseName) + break + } + } + } + } + + package func needsSuffix(for baseName: String) -> Bool { + duplicates.contains(baseName) + } + + /// Compute the disambiguated Java method name for a declaration. + package func makeJavaMethodName(_ decl: ImportedFunc) -> String { + let baseName: String = + switch decl.apiKind { + case .getter, .subscriptGetter: decl.javaGetterName + case .setter, .subscriptSetter: decl.javaSetterName + case .function, .initializer, .enumCase: decl.name + } + return baseName + paramsSuffix(decl, baseName: baseName) + } + + private func paramsSuffix(_ decl: ImportedFunc, baseName: String) -> String { + switch decl.apiKind { + case .getter, .subscriptGetter, .setter, .subscriptSetter: + return "" + default: + guard needsSuffix(for: baseName) else { return "" } + let labels = decl.functionSignature.parameters + .compactMap { $0.argumentLabel } + // A parameterless function that still conflicts (e.g. with a property + // getter) gets a bare "_" so it compiles as a distinct Java method. + guard !labels.isEmpty else { return "_" } + // Join labels in camelCase: takeValue(a:) → takeValueA + return labels.map { $0.prefix(1).uppercased() + $0.dropFirst() }.joined() + } + } +} diff --git a/Tests/JExtractSwiftTests/MethodImportTests.swift b/Tests/JExtractSwiftTests/MethodImportTests.swift index 93dd4ed33..be9984e08 100644 --- a/Tests/JExtractSwiftTests/MethodImportTests.swift +++ b/Tests/JExtractSwiftTests/MethodImportTests.swift @@ -476,4 +476,257 @@ final class MethodImportTests { "'Any' return type is not supported yet" ) } + + // ==== ------------------------------------------------------------------- + // MARK: FFM overloaded method disambiguation + + let overloaded_interfaceFile = + """ + import Swift + + public func takeValue(a: Swift.String) -> Swift.Int + public func takeValue(b: Swift.String) -> Swift.Int + public func uniqueFunc(x: Swift.Int) -> Swift.Int + public func overloaded(a: Swift.Int) -> Swift.Int + public func overloaded(a: Swift.String) -> Swift.Int + + public class OverloadedClass { + public func bar(a: Swift.String) + public func bar(b: Swift.String) + public func unique(x: Swift.Int) + } + """ + + @Test("FFM: Overloaded global functions get suffixed Java names") + func ffm_overloaded_global_functions_suffixed() throws { + try assertOutput( + input: overloaded_interfaceFile, + .ffm, + .java, + swiftModuleName: "OverloadModule", + expectedChunks: [ + "public static long takeValueA(java.lang.String a)", + "public static long takeValueB(java.lang.String b)", + ] + ) + } + + @Test("FFM: Non-overloaded functions keep clean names") + func ffm_non_overloaded_functions_clean_names() throws { + try assertOutput( + input: overloaded_interfaceFile, + .ffm, + .java, + swiftModuleName: "OverloadModule", + expectedChunks: [ + "public static long uniqueFunc(long x)" + ], + notExpectedChunks: [ + "public static long uniqueFunc_x(" + ] + ) + } + + @Test("FFM: Same name but different types — no suffix needed") + func ffm_overloaded_different_types_no_suffix() throws { + try assertOutput( + input: overloaded_interfaceFile, + .ffm, + .java, + swiftModuleName: "OverloadModule", + expectedChunks: [ + "public static long overloaded(long a)", + "public static long overloaded(java.lang.String a)", + ], + notExpectedChunks: [ + "public static long overloaded_a(" + ] + ) + } + + @Test("FFM: Overloaded methods on a type get suffixed Java names") + func ffm_overloaded_methods_on_type_suffixed() throws { + try assertOutput( + input: overloaded_interfaceFile, + .ffm, + .java, + swiftModuleName: "OverloadModule", + expectedChunks: [ + "public void barA(java.lang.String a)", + "public void barB(java.lang.String b)", + ] + ) + } + + @Test("FFM: Non-overloaded method on a type keeps clean name") + func ffm_non_overloaded_method_on_type_clean_name() throws { + try assertOutput( + input: overloaded_interfaceFile, + .ffm, + .java, + swiftModuleName: "OverloadModule", + expectedChunks: [ + "public void unique(long x)" + ], + notExpectedChunks: [ + "public void unique_x(" + ] + ) + } + + let propertyMethodConflict_interfaceFile = + """ + import Swift + + public class MyClass { + public var name: Swift.Int { get } + public func getName() -> Swift.Int + } + """ + + @Test("FFM: Property getter and method with same Java name are disambiguated") + func ffm_property_getter_vs_method_conflict() throws { + try assertOutput( + input: propertyMethodConflict_interfaceFile, + .ffm, + .java, + swiftModuleName: "ConflictModule", + expectedChunks: [ + // Property getter keeps standard Java bean name + "public long getName()", + // Method gets a trailing underscore to avoid the conflict + "public long getName_()", + ] + ) + } + + let argumentLabel_interfaceFile = + """ + import Swift + + public func takeValue(outer name: Swift.String) -> Swift.Int + public func takeValue(another name: Swift.String) -> Swift.Int + """ + + @Test("FFM: Overloaded functions with argument labels use label for suffix") + func ffm_overloaded_argument_labels() throws { + try assertOutput( + input: argumentLabel_interfaceFile, + .ffm, + .java, + swiftModuleName: "LabelModule", + expectedChunks: [ + "public static long takeValueOuter(java.lang.String name)", + "public static long takeValueAnother(java.lang.String name)", + ] + ) + } + + // ==== ------------------------------------------------------------------- + // MARK: JNI overloaded method disambiguation + + @Test("JNI: Overloaded global functions get suffixed Java names") + func jni_overloaded_global_functions_suffixed() throws { + try assertOutput( + input: overloaded_interfaceFile, + .jni, + .java, + swiftModuleName: "OverloadModule", + expectedChunks: [ + "public static long takeValueA(java.lang.String a)", + "public static long takeValueB(java.lang.String b)", + ] + ) + } + + @Test("JNI: Non-overloaded functions keep clean names") + func jni_non_overloaded_functions_clean_names() throws { + try assertOutput( + input: overloaded_interfaceFile, + .jni, + .java, + swiftModuleName: "OverloadModule", + expectedChunks: [ + "public static long uniqueFunc(long x)" + ], + notExpectedChunks: [ + "public static long uniqueFunc_x(" + ] + ) + } + + @Test("JNI: Same name but different types — no suffix needed") + func jni_overloaded_different_types_no_suffix() throws { + try assertOutput( + input: overloaded_interfaceFile, + .jni, + .java, + swiftModuleName: "OverloadModule", + expectedChunks: [ + "public static long overloaded(long a)", + "public static long overloaded(java.lang.String a)", + ], + notExpectedChunks: [ + "public static long overloaded_a(" + ] + ) + } + + @Test("JNI: Overloaded methods on a type get suffixed Java names") + func jni_overloaded_methods_on_type_suffixed() throws { + try assertOutput( + input: overloaded_interfaceFile, + .jni, + .java, + swiftModuleName: "OverloadModule", + expectedChunks: [ + "public void barA(java.lang.String a)", + "public void barB(java.lang.String b)", + ] + ) + } + + @Test("JNI: Non-overloaded method on a type keeps clean name") + func jni_non_overloaded_method_on_type_clean_name() throws { + try assertOutput( + input: overloaded_interfaceFile, + .jni, + .java, + swiftModuleName: "OverloadModule", + expectedChunks: [ + "public void unique(long x)" + ], + notExpectedChunks: [ + "public void unique_x(" + ] + ) + } + + @Test("JNI: Property getter and method with same Java name are disambiguated") + func jni_property_getter_vs_method_conflict() throws { + try assertOutput( + input: propertyMethodConflict_interfaceFile, + .jni, + .java, + swiftModuleName: "ConflictModule", + expectedChunks: [ + "public long getName()", + "public long getName_()", + ] + ) + } + + @Test("JNI: Overloaded functions with argument labels use label for suffix") + func jni_overloaded_argument_labels() throws { + try assertOutput( + input: argumentLabel_interfaceFile, + .jni, + .java, + swiftModuleName: "LabelModule", + expectedChunks: [ + "public static long takeValueOuter(java.lang.String name)", + "public static long takeValueAnother(java.lang.String name)", + ] + ) + } }