Skip to content

Commit 7fd6685

Browse files
amanmaurya92ktoso
andauthored
Fix: Expand Swift variadic functions into explicit Java overloads (#859)
Co-authored-by: Konrad Malawski <ktoso@apple.com>
1 parent 0c53571 commit 7fd6685

12 files changed

Lines changed: 200 additions & 16 deletions

File tree

‎Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftLibrary.swift‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,10 @@ public func globalOverloaded(_ c: Int) -> Int {
7878
c + 3
7979
}
8080

81+
public func globalConcatStrings(_ strings: String...) -> String {
82+
strings.joined()
83+
}
84+
8185
// ==== Internal helpers
8286

8387
func p(_ msg: String, file: String = #fileID, line: UInt = #line, function: String = #function) {

‎Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/MySwiftLibraryTest.java‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,4 +97,12 @@ void call_consumeValueFromOtherModule_crossModule() {
9797
assertEquals(42, result);
9898
}
9999
}
100+
101+
@Test
102+
void variadicOverloads() {
103+
assertEquals("", MySwiftLibrary.globalConcatStrings());
104+
assertEquals("a", MySwiftLibrary.globalConcatStrings("a"));
105+
assertEquals("ab", MySwiftLibrary.globalConcatStrings("a", "b"));
106+
assertEquals("abc", MySwiftLibrary.globalConcatStrings("a", "b", "c"));
107+
}
100108
}

‎Sources/JExtractSwiftLib/FFM/CDeclLowering/FFMSwift2JavaGenerator+FunctionLowering.swift‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1034,7 +1034,9 @@ extension LoweredFunctionSignature {
10341034
let arguments = paramExprs.enumerated()
10351035
.map { (i, argument) -> String in
10361036
let argExpr = original.parameters[i].convention == .inout ? "&\(argument)" : argument
1037-
return LabeledExprSyntax(label: original.parameters[i].argumentLabel, expression: argExpr).description
1037+
let labelStr = original.parameters[i].argumentLabel
1038+
let label = labelStr == "_" ? nil : labelStr
1039+
return LabeledExprSyntax(label: label, expression: argExpr).description
10381040
}
10391041
.joined(separator: .comma)
10401042
resultExpr = "\(callee)(\(raw: arguments))"
@@ -1054,7 +1056,9 @@ extension LoweredFunctionSignature {
10541056
case .subscriptGetter:
10551057
let parameters = paramExprs.enumerated()
10561058
.map { (i, argument) -> String in
1057-
LabeledExprSyntax(label: original.parameters[i].argumentLabel, expression: argument).description
1059+
let labelStr = original.parameters[i].argumentLabel
1060+
let label = labelStr == "_" ? nil : labelStr
1061+
return LabeledExprSyntax(label: label, expression: argument).description
10581062
}
10591063
.joined(separator: .comma)
10601064
resultExpr = "\(callee)[\(raw: parameters)]"
@@ -1066,7 +1070,9 @@ extension LoweredFunctionSignature {
10661070

10671071
let parameters = argumentsWithoutNewValue.enumerated()
10681072
.map { (i, argument) -> String in
1069-
LabeledExprSyntax(label: original.parameters[i].argumentLabel, expression: argument).description
1073+
let labelStr = original.parameters[i].argumentLabel
1074+
let label = labelStr == "_" ? nil : labelStr
1075+
return LabeledExprSyntax(label: label, expression: argument).description
10701076
}
10711077
.joined(separator: .comma)
10721078
resultExpr = "\(callee)[\(raw: parameters)] = \(newValueArgument)"

‎Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+SwiftThunkPrinting.swift‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ extension FFMSwift2JavaGenerator {
9191
.map(\.value)
9292
.sorted(by: { $0.qualifiedName < $1.qualifiedName })
9393

94-
let inputFileName = "\(group.key)".split(separator: "/").last ?? "__Unknown.swift"
94+
let inputFileName = "\(group.key)".split { $0 == "/" || $0 == "\\" }.last ?? "__Unknown.swift"
9595
let filename = "\(inputFileName)".replacing(/\.swift(interface)?/, with: "+SwiftJava.swift")
9696

9797
// Print file header before all type thunks

‎Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator.swift‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ package class FFMSwift2JavaGenerator: Swift2JavaGenerator {
7777
) {
7878
self.log = Logger(label: "ffm-generator", logLevel: translator.log.logLevel)
7979
self.config = config
80-
self.analysis = translator.result
80+
let analysis = translator.result
8181
self.swiftModuleName = translator.swiftModuleName
8282
self.javaPackage = javaPackage
8383
self.swiftOutputDirectory = swiftOutputDirectory
@@ -116,6 +116,11 @@ package class FFMSwift2JavaGenerator: Swift2JavaGenerator {
116116
} else {
117117
self.expectedOutputSwiftFileNames = []
118118
}
119+
120+
// Expand variadic functions into N overloads
121+
var expandedAnalysis = analysis
122+
expandedAnalysis.expandVariadicOverloads(maxOverloads: config.effectiveMaxVariadicOverloads)
123+
self.analysis = expandedAnalysis
119124
}
120125

121126
func generate() throws {

‎Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ extension JNISwift2JavaGenerator {
9696
.map(\.value)
9797
.sorted(by: { $0.qualifiedName < $1.qualifiedName })
9898

99-
let inputFileName = "\(group.key)".split(separator: "/").last ?? "__Unknown.swift"
99+
let inputFileName = "\(group.key)".split { $0 == "/" || $0 == "\\" }.last ?? "__Unknown.swift"
100100
let filename = "\(inputFileName)".replacing(/\.swift(interface)?/, with: "+SwiftJava.swift")
101101

102102
for ty in extractedTypesForThisFile {

‎Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift‎

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ package class JNISwift2JavaGenerator: Swift2JavaGenerator {
7878
) {
7979
self.config = config
8080
self.logger = Logger(label: "jni-generator", logLevel: translator.log.logLevel)
81-
self.analysis = translator.result
81+
let analysis = translator.result
8282
self.swiftModuleName = translator.swiftModuleName
8383
self.javaPackage = javaPackage
8484
self.swiftOutputDirectory = swiftOutputDirectory
@@ -92,7 +92,7 @@ package class JNISwift2JavaGenerator: Swift2JavaGenerator {
9292
if config.effectiveWriteEmptyFiles {
9393
self.expectedOutputSwiftFileNames = Set(
9494
translator.inputs.compactMap { (input) -> String? in
95-
guard let fileName = input.path.split(separator: PATH_SEPARATOR).last else {
95+
guard let fileName = input.path.split(whereSeparator: { $0 == "/" || $0 == "\\" }).last else {
9696
return nil
9797
}
9898
if fileName.hasSuffix(".swift") {
@@ -105,7 +105,7 @@ package class JNISwift2JavaGenerator: Swift2JavaGenerator {
105105
)
106106
// Also include filtered-out files so SwiftPM gets the empty outputs it expects
107107
for path in translator.filteredOutPaths {
108-
guard let fileName = path.split(separator: PATH_SEPARATOR).last else {
108+
guard let fileName = path.split(whereSeparator: { $0 == "/" || $0 == "\\" }).last else {
109109
continue
110110
}
111111
if fileName.hasSuffix(".swift") {
@@ -120,18 +120,23 @@ package class JNISwift2JavaGenerator: Swift2JavaGenerator {
120120
self.expectedOutputSwiftFileNames = []
121121
}
122122

123-
if config.enableJavaCallbacks ?? false {
124-
// We translate all the protocol wrappers
125-
// as we need them to know what protocols we can allow the user to implement themselves
126-
// in Java.
127-
self.interfaceProtocolWrappers = self.generateInterfaceWrappers(Array(self.analysis.extractedTypes.values))
128-
}
123+
// Expand variadic functions into N overloads
124+
var expandedAnalysis = analysis
125+
expandedAnalysis.expandVariadicOverloads(maxOverloads: config.effectiveMaxVariadicOverloads)
126+
self.analysis = expandedAnalysis
129127

130128
// Every extracted protocol that also gets a plain Java `interface`
131129
// generated for it is eligible to be boxed as an existential.
132-
self.existentialProtocolBoxes = self.analysis.extractedTypes.values
130+
self.existentialProtocolBoxes = expandedAnalysis.extractedTypes.values
133131
.filter { $0.swiftNominal.kind == .protocol }
134132
.sorted { $0.swiftNominal.qualifiedName < $1.swiftNominal.qualifiedName }
133+
134+
if config.effectiveEnableJavaCallbacks {
135+
// We translate all the protocol wrappers
136+
// as we need them to know what protocols we can allow the user to implement themselves
137+
// in Java.
138+
self.interfaceProtocolWrappers = self.generateInterfaceWrappers(Array(expandedAnalysis.extractedTypes.values))
139+
}
135140
}
136141

137142
func generate() throws {

‎Sources/SwiftExtract/AnalysisResult.swift‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,16 @@ public struct AnalysisResult {
2828
self.extractedGlobalVariables = extractedGlobalVariables
2929
self.extractedGlobalFuncs = extractedGlobalFuncs
3030
}
31+
32+
/// Expands variadic functions into distinct overloads.
33+
public mutating func expandVariadicOverloads(maxOverloads: Int) {
34+
self.extractedGlobalFuncs = self.extractedGlobalFuncs.flatMap {
35+
$0.expandingVariadicOverloads(maxOverloads: maxOverloads)
36+
}
37+
38+
for type in self.extractedTypes.values {
39+
type.methods = type.methods.flatMap { $0.expandingVariadicOverloads(maxOverloads: maxOverloads) }
40+
type.initializers = type.initializers.flatMap { $0.expandingVariadicOverloads(maxOverloads: maxOverloads) }
41+
}
42+
}
3143
}

‎Sources/SwiftExtract/ExtractedDecls.swift‎

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,61 @@ public final class ExtractedFunc: ExtractedSwiftDecl, CustomStringConvertible {
400400
functionSignature: functionSignature
401401
)
402402
}
403+
404+
/// Expands this function into `maxOverloads + 1` functions if it contains a variadic parameter.
405+
/// Replaces the variadic parameter `T...` with `N` discrete parameters (`arg0: T`, `arg1: T`, etc.)
406+
/// for `N` in `0...maxOverloads`.
407+
/// Returns `[self]` if the function has no variadic parameters.
408+
public func expandingVariadicOverloads(maxOverloads: Int) -> [ExtractedFunc] {
409+
guard functionSignature.hasVariadicParams else {
410+
return [self]
411+
}
412+
413+
var overloads: [ExtractedFunc] = []
414+
415+
// Find the index of the variadic parameter. Swift only allows one.
416+
guard let variadicIndex = functionSignature.parameters.firstIndex(where: \.isVariadic) else {
417+
return [self]
418+
}
419+
420+
let variadicParam = functionSignature.parameters[variadicIndex]
421+
422+
for count in 0...maxOverloads {
423+
var newParameters = functionSignature.parameters
424+
newParameters.remove(at: variadicIndex)
425+
426+
var expandedParams: [SwiftParameter] = []
427+
for i in 0..<count {
428+
let name = "arg\(i)"
429+
expandedParams.append(
430+
SwiftParameter(
431+
convention: variadicParam.convention,
432+
argumentLabel: (i == 0) ? variadicParam.argumentLabel : nil,
433+
parameterName: name, // We use the same name so the call site matches `callee.sum(arg0, arg1)`
434+
type: variadicParam.type,
435+
isVariadic: false
436+
)
437+
)
438+
}
439+
440+
newParameters.insert(contentsOf: expandedParams, at: variadicIndex)
441+
442+
var newSignature = functionSignature
443+
newSignature.parameters = newParameters
444+
445+
overloads.append(
446+
ExtractedFunc(
447+
module: module,
448+
swiftDecl: swiftDecl,
449+
name: name,
450+
apiKind: apiKind,
451+
functionSignature: newSignature
452+
)
453+
)
454+
}
455+
456+
return overloads
457+
}
403458
}
404459

405460
extension ExtractedFunc: Hashable {

‎Sources/SwiftJavaConfigurationShared/Configuration.swift‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,30 @@ public struct Configuration: Codable {
7373
/// The directory where generated Java files should be written. Generally used with jextract mode.
7474
public var outputJavaDirectory: String?
7575

76+
/// Maximum number of overloads to generate for a function with a variadic parameter.
77+
/// When a variadic parameter `T...` is encountered, the generator will produce
78+
/// up to `maxVariadicOverloads` distinct overloads instead of failing.
79+
///
80+
/// Example:
81+
/// ```swift
82+
/// func concat(s: String...) -> String
83+
/// ```
84+
/// results in:
85+
/// ```java
86+
/// // Java
87+
/// String concat() -> String
88+
/// String concat(s0: String, s1: String) -> String
89+
/// String concat(s0: String, s1: String, s2: String) -> String
90+
/// ```
91+
///
92+
/// The reason for this is that Swift cannot "splat" an array into a `...`
93+
/// parameter, therefore we cannot transfer an arbitrary amount of varargs
94+
/// parameters over the native boundary.
95+
public var maxVariadicOverloads: Int?
96+
public var effectiveMaxVariadicOverloads: Int {
97+
maxVariadicOverloads ?? 3
98+
}
99+
76100
/// Determine `jextract` source generation mode, using JNI or FFM.
77101
public var mode: JExtractGenerationMode?
78102

0 commit comments

Comments
 (0)