diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftLibrary.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftLibrary.swift index 0335ff7e4..ecf6ddf6d 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftLibrary.swift +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftLibrary.swift @@ -78,6 +78,10 @@ public func globalOverloaded(_ c: Int) -> Int { c + 3 } +public func globalConcatStrings(_ strings: String...) -> String { + strings.joined() +} + // ==== Internal helpers func p(_ msg: String, file: String = #fileID, line: UInt = #line, function: String = #function) { diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/MySwiftLibraryTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/MySwiftLibraryTest.java index 64aedc61d..09f1057de 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/MySwiftLibraryTest.java +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/MySwiftLibraryTest.java @@ -97,4 +97,12 @@ void call_consumeValueFromOtherModule_crossModule() { assertEquals(42, result); } } + + @Test + void variadicOverloads() { + assertEquals("", MySwiftLibrary.globalConcatStrings()); + assertEquals("a", MySwiftLibrary.globalConcatStrings("a")); + assertEquals("ab", MySwiftLibrary.globalConcatStrings("a", "b")); + assertEquals("abc", MySwiftLibrary.globalConcatStrings("a", "b", "c")); + } } diff --git a/Sources/JExtractSwiftLib/FFM/CDeclLowering/FFMSwift2JavaGenerator+FunctionLowering.swift b/Sources/JExtractSwiftLib/FFM/CDeclLowering/FFMSwift2JavaGenerator+FunctionLowering.swift index ad8f0a6f4..f609588bb 100644 --- a/Sources/JExtractSwiftLib/FFM/CDeclLowering/FFMSwift2JavaGenerator+FunctionLowering.swift +++ b/Sources/JExtractSwiftLib/FFM/CDeclLowering/FFMSwift2JavaGenerator+FunctionLowering.swift @@ -1034,7 +1034,9 @@ extension LoweredFunctionSignature { let arguments = paramExprs.enumerated() .map { (i, argument) -> String in let argExpr = original.parameters[i].convention == .inout ? "&\(argument)" : argument - return LabeledExprSyntax(label: original.parameters[i].argumentLabel, expression: argExpr).description + let labelStr = original.parameters[i].argumentLabel + let label = labelStr == "_" ? nil : labelStr + return LabeledExprSyntax(label: label, expression: argExpr).description } .joined(separator: .comma) resultExpr = "\(callee)(\(raw: arguments))" @@ -1054,7 +1056,9 @@ extension LoweredFunctionSignature { case .subscriptGetter: let parameters = paramExprs.enumerated() .map { (i, argument) -> String in - LabeledExprSyntax(label: original.parameters[i].argumentLabel, expression: argument).description + let labelStr = original.parameters[i].argumentLabel + let label = labelStr == "_" ? nil : labelStr + return LabeledExprSyntax(label: label, expression: argument).description } .joined(separator: .comma) resultExpr = "\(callee)[\(raw: parameters)]" @@ -1066,7 +1070,9 @@ extension LoweredFunctionSignature { let parameters = argumentsWithoutNewValue.enumerated() .map { (i, argument) -> String in - LabeledExprSyntax(label: original.parameters[i].argumentLabel, expression: argument).description + let labelStr = original.parameters[i].argumentLabel + let label = labelStr == "_" ? nil : labelStr + return LabeledExprSyntax(label: label, expression: argument).description } .joined(separator: .comma) resultExpr = "\(callee)[\(raw: parameters)] = \(newValueArgument)" diff --git a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+SwiftThunkPrinting.swift b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+SwiftThunkPrinting.swift index c78cb8688..c69950631 100644 --- a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+SwiftThunkPrinting.swift +++ b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+SwiftThunkPrinting.swift @@ -91,7 +91,7 @@ extension FFMSwift2JavaGenerator { .map(\.value) .sorted(by: { $0.qualifiedName < $1.qualifiedName }) - let inputFileName = "\(group.key)".split(separator: "/").last ?? "__Unknown.swift" + let inputFileName = "\(group.key)".split { $0 == "/" || $0 == "\\" }.last ?? "__Unknown.swift" let filename = "\(inputFileName)".replacing(/\.swift(interface)?/, with: "+SwiftJava.swift") // Print file header before all type thunks diff --git a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator.swift b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator.swift index feda997c4..ffde1222a 100644 --- a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator.swift +++ b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator.swift @@ -77,7 +77,7 @@ package class FFMSwift2JavaGenerator: Swift2JavaGenerator { ) { self.log = Logger(label: "ffm-generator", logLevel: translator.log.logLevel) self.config = config - self.analysis = translator.result + let analysis = translator.result self.swiftModuleName = translator.swiftModuleName self.javaPackage = javaPackage self.swiftOutputDirectory = swiftOutputDirectory @@ -116,6 +116,11 @@ package class FFMSwift2JavaGenerator: Swift2JavaGenerator { } else { self.expectedOutputSwiftFileNames = [] } + + // Expand variadic functions into N overloads + var expandedAnalysis = analysis + expandedAnalysis.expandVariadicOverloads(maxOverloads: config.effectiveMaxVariadicOverloads) + self.analysis = expandedAnalysis } func generate() throws { diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift index db8c66320..78e4f7c76 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift @@ -96,7 +96,7 @@ extension JNISwift2JavaGenerator { .map(\.value) .sorted(by: { $0.qualifiedName < $1.qualifiedName }) - let inputFileName = "\(group.key)".split(separator: "/").last ?? "__Unknown.swift" + let inputFileName = "\(group.key)".split { $0 == "/" || $0 == "\\" }.last ?? "__Unknown.swift" let filename = "\(inputFileName)".replacing(/\.swift(interface)?/, with: "+SwiftJava.swift") for ty in extractedTypesForThisFile { diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift index c3006b00c..4accdc0a3 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift @@ -78,7 +78,7 @@ package class JNISwift2JavaGenerator: Swift2JavaGenerator { ) { self.config = config self.logger = Logger(label: "jni-generator", logLevel: translator.log.logLevel) - self.analysis = translator.result + let analysis = translator.result self.swiftModuleName = translator.swiftModuleName self.javaPackage = javaPackage self.swiftOutputDirectory = swiftOutputDirectory @@ -92,7 +92,7 @@ package class JNISwift2JavaGenerator: Swift2JavaGenerator { if config.effectiveWriteEmptyFiles { self.expectedOutputSwiftFileNames = Set( translator.inputs.compactMap { (input) -> String? in - guard let fileName = input.path.split(separator: PATH_SEPARATOR).last else { + guard let fileName = input.path.split(whereSeparator: { $0 == "/" || $0 == "\\" }).last else { return nil } if fileName.hasSuffix(".swift") { @@ -105,7 +105,7 @@ package class JNISwift2JavaGenerator: Swift2JavaGenerator { ) // Also include filtered-out files so SwiftPM gets the empty outputs it expects for path in translator.filteredOutPaths { - guard let fileName = path.split(separator: PATH_SEPARATOR).last else { + guard let fileName = path.split(whereSeparator: { $0 == "/" || $0 == "\\" }).last else { continue } if fileName.hasSuffix(".swift") { @@ -120,18 +120,23 @@ package class JNISwift2JavaGenerator: Swift2JavaGenerator { self.expectedOutputSwiftFileNames = [] } - if 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.extractedTypes.values)) - } + // Expand variadic functions into N overloads + var expandedAnalysis = analysis + expandedAnalysis.expandVariadicOverloads(maxOverloads: config.effectiveMaxVariadicOverloads) + self.analysis = expandedAnalysis // Every extracted protocol that also gets a plain Java `interface` // generated for it is eligible to be boxed as an existential. - self.existentialProtocolBoxes = self.analysis.extractedTypes.values + self.existentialProtocolBoxes = expandedAnalysis.extractedTypes.values .filter { $0.swiftNominal.kind == .protocol } .sorted { $0.swiftNominal.qualifiedName < $1.swiftNominal.qualifiedName } + + if config.effectiveEnableJavaCallbacks { + // 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(expandedAnalysis.extractedTypes.values)) + } } func generate() throws { diff --git a/Sources/SwiftExtract/AnalysisResult.swift b/Sources/SwiftExtract/AnalysisResult.swift index 4b39506de..b995ec1bd 100644 --- a/Sources/SwiftExtract/AnalysisResult.swift +++ b/Sources/SwiftExtract/AnalysisResult.swift @@ -28,4 +28,16 @@ public struct AnalysisResult { self.extractedGlobalVariables = extractedGlobalVariables self.extractedGlobalFuncs = extractedGlobalFuncs } + + /// Expands variadic functions into distinct overloads. + public mutating func expandVariadicOverloads(maxOverloads: Int) { + self.extractedGlobalFuncs = self.extractedGlobalFuncs.flatMap { + $0.expandingVariadicOverloads(maxOverloads: maxOverloads) + } + + for type in self.extractedTypes.values { + type.methods = type.methods.flatMap { $0.expandingVariadicOverloads(maxOverloads: maxOverloads) } + type.initializers = type.initializers.flatMap { $0.expandingVariadicOverloads(maxOverloads: maxOverloads) } + } + } } diff --git a/Sources/SwiftExtract/ExtractedDecls.swift b/Sources/SwiftExtract/ExtractedDecls.swift index 580bddb47..f276da292 100644 --- a/Sources/SwiftExtract/ExtractedDecls.swift +++ b/Sources/SwiftExtract/ExtractedDecls.swift @@ -400,6 +400,61 @@ public final class ExtractedFunc: ExtractedSwiftDecl, CustomStringConvertible { functionSignature: functionSignature ) } + + /// Expands this function into `maxOverloads + 1` functions if it contains a variadic parameter. + /// Replaces the variadic parameter `T...` with `N` discrete parameters (`arg0: T`, `arg1: T`, etc.) + /// for `N` in `0...maxOverloads`. + /// Returns `[self]` if the function has no variadic parameters. + public func expandingVariadicOverloads(maxOverloads: Int) -> [ExtractedFunc] { + guard functionSignature.hasVariadicParams else { + return [self] + } + + var overloads: [ExtractedFunc] = [] + + // Find the index of the variadic parameter. Swift only allows one. + guard let variadicIndex = functionSignature.parameters.firstIndex(where: \.isVariadic) else { + return [self] + } + + let variadicParam = functionSignature.parameters[variadicIndex] + + for count in 0...maxOverloads { + var newParameters = functionSignature.parameters + newParameters.remove(at: variadicIndex) + + var expandedParams: [SwiftParameter] = [] + for i in 0.. String + /// ``` + /// results in: + /// ```java + /// // Java + /// String concat() -> String + /// String concat(s0: String, s1: String) -> String + /// String concat(s0: String, s1: String, s2: String) -> String + /// ``` + /// + /// The reason for this is that Swift cannot "splat" an array into a `...` + /// parameter, therefore we cannot transfer an arbitrary amount of varargs + /// parameters over the native boundary. + public var maxVariadicOverloads: Int? + public var effectiveMaxVariadicOverloads: Int { + maxVariadicOverloads ?? 3 + } + /// Determine `jextract` source generation mode, using JNI or FFM. public var mode: JExtractGenerationMode? diff --git a/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaConfigFile.md b/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaConfigFile.md index fb166fff1..0687233d1 100644 --- a/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaConfigFile.md +++ b/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaConfigFile.md @@ -118,6 +118,33 @@ The directory where generated Java files should be written. Generally used with --- +#### maxVariadicOverloads + +- **Type:** `Int?` +- **Default:** `3` + +Maximum number of overloads to generate for a function with a variadic parameter. +When a variadic parameter `T...` is encountered, the generator will produce +up to `maxVariadicOverloads` distinct overloads instead of failing. + +Example: +```swift +func concat(s: String...) -> String +``` +results in: +```java +// Java +String concat() -> String +String concat(s0: String, s1: String) -> String +String concat(s0: String, s1: String, s2: String) -> String +``` + +The reason for this is that Swift cannot "splat" an array into a `...` +parameter, therefore we cannot transfer an arbitrary amount of varargs +parameters over the native boundary. + +--- + #### mode - **Type:** `JExtractGenerationMode?` diff --git a/Tests/JExtractSwiftTests/JNI/JNIModuleTests.swift b/Tests/JExtractSwiftTests/JNI/JNIModuleTests.swift index 8b63d982a..07fbda3a0 100644 --- a/Tests/JExtractSwiftTests/JNI/JNIModuleTests.swift +++ b/Tests/JExtractSwiftTests/JNI/JNIModuleTests.swift @@ -326,4 +326,42 @@ struct JNIModuleTests { ] ) } + + @Test + func expandsVariadicParameter() throws { + let input = """ + public func helloWorld() + public func sum(_ xs: Int64...) -> Int64 { xs.reduce(0, +) } + """ + + var config = Configuration() + config.maxVariadicOverloads = 3 + + try assertOutput( + input: input, + config: config, + .jni, + .java, + expectedChunks: [ + """ + public static void helloWorld() + """, + """ + public static long sum() + """, + """ + public static long sum(long arg0) + """, + """ + public static long sum(long arg0, long arg1) + """, + """ + public static long sum(long arg0, long arg1, long arg2) + """, + ], + notExpectedChunks: [ + "sum(long arg0, long arg1, long arg2, long arg3)" + ] + ) + } }