Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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))"
Expand All @@ -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)]"
Expand All @@ -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)"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 6 additions & 1 deletion Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
25 changes: 15 additions & 10 deletions Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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") {
Expand All @@ -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") {
Expand All @@ -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
Comment thread
ktoso marked this conversation as resolved.

// 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 {
Expand Down
12 changes: 12 additions & 0 deletions Sources/SwiftExtract/AnalysisResult.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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) }
}
}
}
55 changes: 55 additions & 0 deletions Sources/SwiftExtract/ExtractedDecls.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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] {
Comment thread
amanmaurya92 marked this conversation as resolved.
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..<count {
let name = "arg\(i)"
expandedParams.append(
SwiftParameter(
convention: variadicParam.convention,
argumentLabel: (i == 0) ? variadicParam.argumentLabel : nil,
parameterName: name, // We use the same name so the call site matches `callee.sum(arg0, arg1)`
type: variadicParam.type,
isVariadic: false
)
)
}

newParameters.insert(contentsOf: expandedParams, at: variadicIndex)

var newSignature = functionSignature
newSignature.parameters = newParameters

overloads.append(
ExtractedFunc(
module: module,
swiftDecl: swiftDecl,
name: name,
apiKind: apiKind,
functionSignature: newSignature
)
)
}

return overloads
}
}

extension ExtractedFunc: Hashable {
Expand Down
24 changes: 24 additions & 0 deletions Sources/SwiftJavaConfigurationShared/Configuration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,30 @@ public struct Configuration: Codable {
/// The directory where generated Java files should be written. Generally used with jextract mode.
public var outputJavaDirectory: String?

/// 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.
public var maxVariadicOverloads: Int?
public var effectiveMaxVariadicOverloads: Int {
maxVariadicOverloads ?? 3
}

/// Determine `jextract` source generation mode, using JNI or FFM.
public var mode: JExtractGenerationMode?

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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?`
Expand Down
38 changes: 38 additions & 0 deletions Tests/JExtractSwiftTests/JNI/JNIModuleTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)"
]
)
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please make a runtime test; we make those by adding a func in the Samples/SwiftJavaExtractJNISampleApp and exercise the functions in Java test code in the same project. This verifies it all works at runtime as well

}
Loading