Skip to content
Open
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 @@ -50,7 +50,7 @@ extension JNISwift2JavaGenerator {
p.print(
"""
@JavaMethod
public func apply\(signature)
public func \(config.effectiveClosureMethodName)\(signature)
"""
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,9 @@ extension JNISwift2JavaGenerator {
/// Conversion step for the result
let resultConversion: UpcallConversionStep

/// The name of the interface's single method, `apply` unless `closureMethodName` says otherwise
let methodName: String

/// The original Swift function type
let functionType: SwiftFunctionType
}
Expand Down Expand Up @@ -140,12 +143,13 @@ extension JNISwift2JavaGenerator {
}

/// Generates a synthetic closure function translation.
/// This treats the closure as if it were a protocol with a single `apply` method,
/// This treats the closure as if it were a protocol with a single method named `methodName`,
/// allowing it to use the same conversion infrastructure for optionals, arrays, etc.
func generateSyntheticClosureFunction(
functionType: SwiftFunctionType,
javaInterfaceName: String,
javaBinaryName: String,
methodName: String,
) throws -> SyntheticEscapingClosureFunctionType {
let parameterConversions = try functionType.parameters.enumerated().map { idx, param in
try self.translateParameter(
Expand All @@ -156,14 +160,15 @@ extension JNISwift2JavaGenerator {

let resultConversion = try self.translateResult(
type: functionType.resultType,
methodName: "apply"
methodName: methodName
)

return SyntheticEscapingClosureFunctionType(
javaInterfaceName: javaInterfaceName,
javaBinaryName: javaBinaryName,
parameterConversions: parameterConversions,
resultConversion: resultConversion,
methodName: methodName,
functionType: functionType
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -810,7 +810,7 @@ extension JNISwift2JavaGenerator {
// If contains one function type and it's known java functional interface type, we don't need to print the whole class
if translated.functionTypes.count == 1,
let ty = translated.functionTypes.first,
KnownJavaFunctionalInterface.find(ty) != nil
KnownJavaFunctionalInterface.find(ty, closureMethodName: config.effectiveClosureMethodName) != nil
{
return
}
Expand All @@ -821,7 +821,7 @@ extension JNISwift2JavaGenerator {
"""
) { printer in
for functionType in translated.functionTypes {
if KnownJavaFunctionalInterface.find(functionType) != nil {
if KnownJavaFunctionalInterface.find(functionType, closureMethodName: config.effectiveClosureMethodName) != nil {
continue
}

Expand All @@ -846,7 +846,7 @@ extension JNISwift2JavaGenerator {
"""
@FunctionalInterface
public interface \(functionType.name) {
\(functionType.result.javaType) apply(\(apiParams.joined(separator: .comma)));
\(functionType.result.javaType) \(config.effectiveClosureMethodName)(\(apiParams.joined(separator: .comma)));
}
"""
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -761,7 +761,7 @@ extension JNISwift2JavaGenerator {
parameterAnnotations: [JavaAnnotation]
) -> TranslatedParameter {
let interfacejavaType =
if let known = KnownJavaFunctionalInterface.find(functionType) {
if let known = KnownJavaFunctionalInterface.find(functionType, closureMethodName: config.effectiveClosureMethodName) {
known.javaType
} else {
JavaType.class(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,7 @@ extension JNISwift2JavaGenerator {
functionType: functionType,
javaInterfaceName: javaClosureInterfaceName,
javaBinaryName: javaBinaryName,
methodName: config.effectiveClosureMethodName,
)

return NativeParameter(
Expand Down Expand Up @@ -432,7 +433,7 @@ extension JNISwift2JavaGenerator {

let result = try translateClosureResult(functionType.resultType)
let interfaceJavaType =
if let known = KnownJavaFunctionalInterface.find(functionType) {
if let known = KnownJavaFunctionalInterface.find(functionType, closureMethodName: config.effectiveClosureMethodName) {
known.javaType
} else {
JavaType.class(
Expand All @@ -450,7 +451,8 @@ extension JNISwift2JavaGenerator {
],
conversion: .closureLowering(
parameters: parameters,
result: result
result: result,
closureMethodName: config.effectiveClosureMethodName
),
indirectConversion: nil,
conversionCheck: nil
Expand Down Expand Up @@ -1364,7 +1366,8 @@ extension JNISwift2JavaGenerator {
/// of the `Unsafe(Mutable)Pointer` types in Swift.
indirect case pointee(NativeSwiftConversionStep)

indirect case closureLowering(parameters: [NativeParameter], result: NativeResult)
/// `closureMethodName` is the method of the closure's own interface; a `java.util.function` one keeps its own.
indirect case closureLowering(parameters: [NativeParameter], result: NativeResult, closureMethodName: String)

/// Escaping closure lowering using the protocol infrastructure.
/// This uses UpcallConversionStep for full support of optionals, arrays, custom types, etc.
Expand Down Expand Up @@ -1664,7 +1667,7 @@ extension JNISwift2JavaGenerator {
let inner = inner.render(&printer, placeholder)
return "\(inner).pointee"

case .closureLowering(let parameters, let nativeResult):
case .closureLowering(let parameters, let nativeResult, let closureMethodName):
var printer = SwiftPrinter()

let methodSignature = MethodSignature(
Expand All @@ -1691,10 +1694,10 @@ extension JNISwift2JavaGenerator {


let methodName =
if let known = KnownJavaFunctionalInterface.find(methodSignature) {
if closureMethodName == "apply", let known = KnownJavaFunctionalInterface.find(methodSignature) {
known.method
} else {
"apply"
closureMethodName
}

printer.print(
Expand Down Expand Up @@ -1746,7 +1749,7 @@ extension JNISwift2JavaGenerator {
// Note: The Java interface is synchronous even for async closures.
// The async nature is on the Swift side, inferred from the expected type.
var resultPrinter = SwiftPrinter()
let upcallExpr = "\(javaInterfaceVar).apply(\(upcallArguments.joined(separator: .comma)))"
let upcallExpr = "\(javaInterfaceVar).\(syntheticFunction.methodName)(\(upcallArguments.joined(separator: .comma)))"
let resultConverted = syntheticFunction.resultConversion.render(&resultPrinter, upcallExpr)
let resultPrefix = resultPrinter.finalize()

Expand Down
14 changes: 14 additions & 0 deletions Sources/JExtractSwiftLib/KnownFunctionalInterfaces.swift
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,20 @@ struct KnownJavaFunctionalInterface: Sendable {
find(parameters: methodSignature.parameterTypes, result: methodSignature.resultType)
}

/// The JNI generator's lookup: a custom `closureMethodName` keeps every closure on its own interface, since a
/// `java.util.function` interface names its method `apply…`.
static func find(_ functionType: SwiftFunctionType, closureMethodName: String) -> KnownJavaFunctionalInterface? {
closureMethodName == "apply" ? find(functionType) : nil
}

/// See ``find(_:closureMethodName:)``.
static func find(
_ functionType: JNISwift2JavaGenerator.TranslatedFunctionType,
closureMethodName: String
) -> KnownJavaFunctionalInterface? {
closureMethodName == "apply" ? find(functionType) : nil
}

static func find(_ functionType: SwiftFunctionType) -> KnownJavaFunctionalInterface? {
if functionType.isEscaping {
return nil
Expand Down
9 changes: 9 additions & 0 deletions Sources/SwiftJavaConfigurationShared/Configuration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,15 @@ public struct Configuration: Codable {
enableJavaCallbacks ?? false
}

/// The name of the single method of the functional interfaces jextract generates for Swift closures, `apply` by
/// default. With `invoke`, Kotlin calls such a closure as a function, `onTap()` instead of `onTap.apply()`. With
/// any name other than `apply`, closures such as `(Int64) -> Int64` also get their own interface instead of a
/// `java.util.function` one, so they can be called the same way. Only supported in `jni` mode.
public var closureMethodName: String?
public var effectiveClosureMethodName: String {
closureMethodName ?? "apply"
}

/// If specified, JExtract will output to this file a list of paths to all generated Java source files.
public var generatedJavaSourcesListFileOutput: String?

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,18 @@ only supported in `jni` mode.

---

#### closureMethodName

- **Type:** `String?`
- **Default:** `"apply"`

The name of the single method of the functional interfaces jextract generates for Swift closures, `apply` by
default. With `invoke`, Kotlin calls such a closure as a function, `onTap()` instead of `onTap.apply()`. With
any name other than `apply`, closures such as `(Int64) -> Int64` also get their own interface instead of a
`java.util.function` one, so they can be called the same way. Only supported in `jni` mode.

---

#### generatedJavaSourcesListFileOutput

- **Type:** `String?`
Expand Down
90 changes: 90 additions & 0 deletions Tests/JExtractSwiftTests/JNI/JNIClosureMethodNameTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
//===----------------------------------------------------------------------===//
//
// 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

/// `closureMethodName` names the method of closure interfaces, so Kotlin can call a closure as `handler()`.
@Suite
struct JNIClosureMethodNameTests {
let source =
"""
public final class Notifier {
public init() {}
public func onMessage(_ handler: @escaping (String) -> Void) {}
public func double(_ f: (Int64) -> Int64) -> Int64 { f(2) }
}
"""

var config: Configuration {
var config = Configuration()
config.closureMethodName = "invoke"
return config
}

@Test
func javaInterfacesUseTheName() throws {
try assertOutput(
input: source,
config: config,
.jni,
.java,
detectChunkByInitialLines: 1,
expectedChunks: [
"void invoke(java.lang.String _0);",
// A closure `java.util.function` covers gets its own interface, so it is called the same way.
"long invoke(long _0);",
],
notExpectedChunks: [
" apply(",
"LongUnaryOperator",
]
)
}

@Test
func swiftCallsTheName() throws {
try assertOutput(
input: source,
config: config,
.jni,
.swift,
detectChunkByInitialLines: 1,
expectedChunks: [
"public func invoke(_ _0: String)",
"javaInterface_handler$.invoke(_0)",
#"environment.interface.GetMethodID(environment, class$, "invoke", "(J)J")!"#,
],
notExpectedChunks: [
"func apply(",
".apply(",
]
)
}

@Test
func applyIsTheDefault() throws {
try assertOutput(
input: source,
.jni,
.java,
detectChunkByInitialLines: 1,
expectedChunks: [
"void apply(java.lang.String _0);",
"java.util.function.LongUnaryOperator",
]
)
}
}
Loading