From 31b7befe9ace43295c03ffc6c913535d5fccb343 Mon Sep 17 00:00:00 2001 From: Pierluigi Cifani Date: Wed, 4 Nov 2026 15:32:40 +0100 Subject: [PATCH 1/2] jextract: closureMethodName names the method of closure interfaces Closure interfaces declare one method, `apply`, so Kotlin code calls a closure value as `handler.apply(x)`. Kotlin treats a Java method named `invoke` as the invoke operator, so with `"closureMethodName": "invoke"` the same closure is called as `handler(x)`, and Kotlin lambdas still convert to the interface. The default stays `apply`. The name is used for the Java interface, the `@JavaInterface` printed for escaping closures and the call into Java, and the method looked up for non-escaping closures. With a name other than `apply`, closures that a `java.util.function` interface would cover, such as `(Int64) -> Int64` with `LongUnaryOperator.applyAsLong`, get their own interface too, so every closure is called the same way. Co-Authored-By: Claude Opus 5.5 --- ...avaGenerator+EscapingClosureWrapJava.swift | 2 +- ...Generator+InterfaceWrapperGeneration.swift | 9 +- ...t2JavaGenerator+JavaBindingsPrinting.swift | 6 +- ...ISwift2JavaGenerator+JavaTranslation.swift | 2 +- ...wift2JavaGenerator+NativeTranslation.swift | 17 ++-- .../KnownFunctionalInterfaces.swift | 14 +++ .../Configuration.swift | 9 ++ .../Documentation.docc/SwiftJavaConfigFile.md | 12 +++ .../JNI/JNIClosureMethodNameTests.swift | 90 +++++++++++++++++++ 9 files changed, 147 insertions(+), 14 deletions(-) create mode 100644 Tests/JExtractSwiftTests/JNI/JNIClosureMethodNameTests.swift diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+EscapingClosureWrapJava.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+EscapingClosureWrapJava.swift index 6b6a4e729..408d8d062 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+EscapingClosureWrapJava.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+EscapingClosureWrapJava.swift @@ -50,7 +50,7 @@ extension JNISwift2JavaGenerator { p.print( """ @JavaMethod - public func apply\(signature) + public func \(config.effectiveClosureMethodName)\(signature) """ ) } diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+InterfaceWrapperGeneration.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+InterfaceWrapperGeneration.swift index 236977e5e..cab09920b 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+InterfaceWrapperGeneration.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+InterfaceWrapperGeneration.swift @@ -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 } @@ -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( @@ -156,7 +160,7 @@ extension JNISwift2JavaGenerator { let resultConversion = try self.translateResult( type: functionType.resultType, - methodName: "apply" + methodName: methodName ) return SyntheticEscapingClosureFunctionType( @@ -164,6 +168,7 @@ extension JNISwift2JavaGenerator { javaBinaryName: javaBinaryName, parameterConversions: parameterConversions, resultConversion: resultConversion, + methodName: methodName, functionType: functionType ) } diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift index 504514f57..e3e78d0ce 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift @@ -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 } @@ -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 } @@ -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))); } """ ) diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift index 9dc93d507..37a86d845 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift @@ -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( diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift index 6490586d7..d443b6c54 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift @@ -398,6 +398,7 @@ extension JNISwift2JavaGenerator { functionType: functionType, javaInterfaceName: javaClosureInterfaceName, javaBinaryName: javaBinaryName, + methodName: config.effectiveClosureMethodName, ) return NativeParameter( @@ -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( @@ -450,7 +451,8 @@ extension JNISwift2JavaGenerator { ], conversion: .closureLowering( parameters: parameters, - result: result + result: result, + closureMethodName: config.effectiveClosureMethodName ), indirectConversion: nil, conversionCheck: nil @@ -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. @@ -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( @@ -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( @@ -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() diff --git a/Sources/JExtractSwiftLib/KnownFunctionalInterfaces.swift b/Sources/JExtractSwiftLib/KnownFunctionalInterfaces.swift index 9b1f75ab2..1f0465b2e 100644 --- a/Sources/JExtractSwiftLib/KnownFunctionalInterfaces.swift +++ b/Sources/JExtractSwiftLib/KnownFunctionalInterfaces.swift @@ -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 diff --git a/Sources/SwiftJavaConfigurationShared/Configuration.swift b/Sources/SwiftJavaConfigurationShared/Configuration.swift index c45b00b2e..a4a7a36df 100644 --- a/Sources/SwiftJavaConfigurationShared/Configuration.swift +++ b/Sources/SwiftJavaConfigurationShared/Configuration.swift @@ -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? diff --git a/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaConfigFile.md b/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaConfigFile.md index bacce7ccf..da21337b7 100644 --- a/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaConfigFile.md +++ b/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaConfigFile.md @@ -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?` diff --git a/Tests/JExtractSwiftTests/JNI/JNIClosureMethodNameTests.swift b/Tests/JExtractSwiftTests/JNI/JNIClosureMethodNameTests.swift new file mode 100644 index 000000000..3d5ddd03d --- /dev/null +++ b/Tests/JExtractSwiftTests/JNI/JNIClosureMethodNameTests.swift @@ -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", + ] + ) + } +} From a6a39bc2e7c19be98d3f9babc63a5e47d32afe96 Mon Sep 17 00:00:00 2001 From: Pierluigi Cifani Date: Sat, 7 Nov 2026 09:11:07 +0100 Subject: [PATCH 2/2] jextract: closure interfaces get a default invoke method Replaces the `closureMethodName` option. Each closure interface jextract generates keeps its abstract `apply` method and adds a default `invoke` that calls it. Kotlin treats a method named `invoke` as the invoke operator, so a closure value is called as `handler(x)`, and Kotlin lambdas still convert to the interface since `apply` stays its only abstract method. The Swift side is unchanged: it still calls `apply`. Closures that a `java.util.function` interface covers keep that interface and its method. Co-Authored-By: Claude Opus 5.5 --- ...avaGenerator+EscapingClosureWrapJava.swift | 2 +- ...Generator+InterfaceWrapperGeneration.swift | 9 +- ...t2JavaGenerator+JavaBindingsPrinting.swift | 15 +++- ...ISwift2JavaGenerator+JavaTranslation.swift | 2 +- ...wift2JavaGenerator+NativeTranslation.swift | 17 ++-- .../KnownFunctionalInterfaces.swift | 14 --- .../Configuration.swift | 9 -- .../Documentation.docc/FeaturesJextract.md | 6 ++ .../Documentation.docc/SwiftJavaConfigFile.md | 12 --- .../JNI/JNIClosureInvokeTests.swift | 78 ++++++++++++++++ .../JNI/JNIClosureMethodNameTests.swift | 90 ------------------- .../JNI/JNIClosureTests.swift | 4 + .../JNI/JNIEscapingClosureTests.swift | 12 +++ 13 files changed, 123 insertions(+), 147 deletions(-) create mode 100644 Tests/JExtractSwiftTests/JNI/JNIClosureInvokeTests.swift delete mode 100644 Tests/JExtractSwiftTests/JNI/JNIClosureMethodNameTests.swift diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+EscapingClosureWrapJava.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+EscapingClosureWrapJava.swift index 408d8d062..6b6a4e729 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+EscapingClosureWrapJava.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+EscapingClosureWrapJava.swift @@ -50,7 +50,7 @@ extension JNISwift2JavaGenerator { p.print( """ @JavaMethod - public func \(config.effectiveClosureMethodName)\(signature) + public func apply\(signature) """ ) } diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+InterfaceWrapperGeneration.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+InterfaceWrapperGeneration.swift index cab09920b..236977e5e 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+InterfaceWrapperGeneration.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+InterfaceWrapperGeneration.swift @@ -99,9 +99,6 @@ 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 } @@ -143,13 +140,12 @@ extension JNISwift2JavaGenerator { } /// Generates a synthetic closure function translation. - /// This treats the closure as if it were a protocol with a single method named `methodName`, + /// This treats the closure as if it were a protocol with a single `apply` method, /// 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( @@ -160,7 +156,7 @@ extension JNISwift2JavaGenerator { let resultConversion = try self.translateResult( type: functionType.resultType, - methodName: methodName + methodName: "apply" ) return SyntheticEscapingClosureFunctionType( @@ -168,7 +164,6 @@ extension JNISwift2JavaGenerator { javaBinaryName: javaBinaryName, parameterConversions: parameterConversions, resultConversion: resultConversion, - methodName: methodName, functionType: functionType ) } diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift index e3e78d0ce..2efb29e0c 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift @@ -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, closureMethodName: config.effectiveClosureMethodName) != nil + KnownJavaFunctionalInterface.find(ty) != nil { return } @@ -821,7 +821,7 @@ extension JNISwift2JavaGenerator { """ ) { printer in for functionType in translated.functionTypes { - if KnownJavaFunctionalInterface.find(functionType, closureMethodName: config.effectiveClosureMethodName) != nil { + if KnownJavaFunctionalInterface.find(functionType) != nil { continue } @@ -836,17 +836,26 @@ extension JNISwift2JavaGenerator { _ functionType: TranslatedFunctionType, ) { let apiParams = functionType.parameters.map({ $0.parameter.renderParameter() }) + let arguments = functionType.parameters.map(\.parameter.name).joined(separator: .comma) + let returnKeyword = functionType.result.javaType.isVoid ? "" : "return " printer.printJavadocComment( """ Corresponds to the Swift closure parameter of type {@code \(functionType.swiftType)}. """ ) + // `invoke` lets Kotlin call the closure as `handler(x)`: Kotlin treats a Java method named `invoke` + // as the invoke operator. Kotlin lambdas still convert to the interface, since `apply` is its only + // abstract method. printer.print( """ @FunctionalInterface public interface \(functionType.name) { - \(functionType.result.javaType) \(config.effectiveClosureMethodName)(\(apiParams.joined(separator: .comma))); + \(functionType.result.javaType) apply(\(apiParams.joined(separator: .comma))); + + default \(functionType.result.javaType) invoke(\(apiParams.joined(separator: .comma))) { + \(returnKeyword)apply(\(arguments)); + } } """ ) diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift index 37a86d845..9dc93d507 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift @@ -761,7 +761,7 @@ extension JNISwift2JavaGenerator { parameterAnnotations: [JavaAnnotation] ) -> TranslatedParameter { let interfacejavaType = - if let known = KnownJavaFunctionalInterface.find(functionType, closureMethodName: config.effectiveClosureMethodName) { + if let known = KnownJavaFunctionalInterface.find(functionType) { known.javaType } else { JavaType.class( diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift index d443b6c54..6490586d7 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift @@ -398,7 +398,6 @@ extension JNISwift2JavaGenerator { functionType: functionType, javaInterfaceName: javaClosureInterfaceName, javaBinaryName: javaBinaryName, - methodName: config.effectiveClosureMethodName, ) return NativeParameter( @@ -433,7 +432,7 @@ extension JNISwift2JavaGenerator { let result = try translateClosureResult(functionType.resultType) let interfaceJavaType = - if let known = KnownJavaFunctionalInterface.find(functionType, closureMethodName: config.effectiveClosureMethodName) { + if let known = KnownJavaFunctionalInterface.find(functionType) { known.javaType } else { JavaType.class( @@ -451,8 +450,7 @@ extension JNISwift2JavaGenerator { ], conversion: .closureLowering( parameters: parameters, - result: result, - closureMethodName: config.effectiveClosureMethodName + result: result ), indirectConversion: nil, conversionCheck: nil @@ -1366,8 +1364,7 @@ extension JNISwift2JavaGenerator { /// of the `Unsafe(Mutable)Pointer` types in Swift. indirect case pointee(NativeSwiftConversionStep) - /// `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) + indirect case closureLowering(parameters: [NativeParameter], result: NativeResult) /// Escaping closure lowering using the protocol infrastructure. /// This uses UpcallConversionStep for full support of optionals, arrays, custom types, etc. @@ -1667,7 +1664,7 @@ extension JNISwift2JavaGenerator { let inner = inner.render(&printer, placeholder) return "\(inner).pointee" - case .closureLowering(let parameters, let nativeResult, let closureMethodName): + case .closureLowering(let parameters, let nativeResult): var printer = SwiftPrinter() let methodSignature = MethodSignature( @@ -1694,10 +1691,10 @@ extension JNISwift2JavaGenerator { let methodName = - if closureMethodName == "apply", let known = KnownJavaFunctionalInterface.find(methodSignature) { + if let known = KnownJavaFunctionalInterface.find(methodSignature) { known.method } else { - closureMethodName + "apply" } printer.print( @@ -1749,7 +1746,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).\(syntheticFunction.methodName)(\(upcallArguments.joined(separator: .comma)))" + let upcallExpr = "\(javaInterfaceVar).apply(\(upcallArguments.joined(separator: .comma)))" let resultConverted = syntheticFunction.resultConversion.render(&resultPrinter, upcallExpr) let resultPrefix = resultPrinter.finalize() diff --git a/Sources/JExtractSwiftLib/KnownFunctionalInterfaces.swift b/Sources/JExtractSwiftLib/KnownFunctionalInterfaces.swift index 1f0465b2e..9b1f75ab2 100644 --- a/Sources/JExtractSwiftLib/KnownFunctionalInterfaces.swift +++ b/Sources/JExtractSwiftLib/KnownFunctionalInterfaces.swift @@ -225,20 +225,6 @@ 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 diff --git a/Sources/SwiftJavaConfigurationShared/Configuration.swift b/Sources/SwiftJavaConfigurationShared/Configuration.swift index a4a7a36df..c45b00b2e 100644 --- a/Sources/SwiftJavaConfigurationShared/Configuration.swift +++ b/Sources/SwiftJavaConfigurationShared/Configuration.swift @@ -181,15 +181,6 @@ 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? diff --git a/Sources/SwiftJavaDocumentation/Documentation.docc/FeaturesJextract.md b/Sources/SwiftJavaDocumentation/Documentation.docc/FeaturesJextract.md index 24d50e508..118794eb4 100644 --- a/Sources/SwiftJavaDocumentation/Documentation.docc/FeaturesJextract.md +++ b/Sources/SwiftJavaDocumentation/Documentation.docc/FeaturesJextract.md @@ -855,6 +855,12 @@ Subscripts taking parameters keep them on the generated Java methods. Swift closure parameters become Java functional interfaces that can be implemented with a lambda expression. +In JNI mode, an interface jextract generates declares `apply` and a default +`invoke` method that calls it. Kotlin treats a method named `invoke` as the +invoke operator, so Kotlin code can call a closure value as `handler(x)`. +Closures that a `java.util.function` interface covers, such as +`(Int64) -> Int64` with `LongUnaryOperator`, use that interface instead. + #### Non-escaping closures Non-escaping closures with `Void` return or primitive arguments/results are supported diff --git a/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaConfigFile.md b/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaConfigFile.md index da21337b7..bacce7ccf 100644 --- a/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaConfigFile.md +++ b/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaConfigFile.md @@ -246,18 +246,6 @@ 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?` diff --git a/Tests/JExtractSwiftTests/JNI/JNIClosureInvokeTests.swift b/Tests/JExtractSwiftTests/JNI/JNIClosureInvokeTests.swift new file mode 100644 index 000000000..016a7a386 --- /dev/null +++ b/Tests/JExtractSwiftTests/JNI/JNIClosureInvokeTests.swift @@ -0,0 +1,78 @@ +//===----------------------------------------------------------------------===// +// +// 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 Testing + +/// Closure interfaces get a default `invoke` method, so Kotlin can call a closure as `handler()`. +@Suite +struct JNIClosureInvokeTests { + let source = + """ + public final class Notifier { + public init() {} + public func onMessage(_ handler: @escaping (String) -> Void) {} + public func format(_ f: @escaping (String, Int64) -> String) {} + public func double(_ f: (Int64) -> Int64) -> Int64 { f(2) } + } + """ + + @Test + func interfacesForwardInvokeToApply() throws { + try assertOutput( + input: source, + .jni, + .java, + expectedChunks: [ + """ + public interface handler { + void apply(java.lang.String _0); + + default void invoke(java.lang.String _0) { + apply(_0); + } + } + """, + """ + public interface f { + java.lang.String apply(java.lang.String _0, long _1); + + default java.lang.String invoke(java.lang.String _0, long _1) { + return apply(_0, _1); + } + } + """, + // A closure `java.util.function` covers keeps that interface. + "java.util.function.LongUnaryOperator", + ] + ) + } + + @Test + func swiftCallsApply() throws { + try assertOutput( + input: source, + .jni, + .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + "public func apply(_ _0: String)", + "javaInterface_handler$.apply(_0)", + ], + notExpectedChunks: [ + "invoke" + ] + ) + } +} diff --git a/Tests/JExtractSwiftTests/JNI/JNIClosureMethodNameTests.swift b/Tests/JExtractSwiftTests/JNI/JNIClosureMethodNameTests.swift deleted file mode 100644 index 3d5ddd03d..000000000 --- a/Tests/JExtractSwiftTests/JNI/JNIClosureMethodNameTests.swift +++ /dev/null @@ -1,90 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// 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", - ] - ) - } -} diff --git a/Tests/JExtractSwiftTests/JNI/JNIClosureTests.swift b/Tests/JExtractSwiftTests/JNI/JNIClosureTests.swift index aa5f1fa6e..a75317afd 100644 --- a/Tests/JExtractSwiftTests/JNI/JNIClosureTests.swift +++ b/Tests/JExtractSwiftTests/JNI/JNIClosureTests.swift @@ -1217,6 +1217,10 @@ struct JNIClosureTests { @FunctionalInterface public interface closure { long apply(long _0, boolean _1); + + default long invoke(long _0, boolean _1) { + return apply(_0, _1); + } } } """, diff --git a/Tests/JExtractSwiftTests/JNI/JNIEscapingClosureTests.swift b/Tests/JExtractSwiftTests/JNI/JNIEscapingClosureTests.swift index b6ca93af7..d691da948 100644 --- a/Tests/JExtractSwiftTests/JNI/JNIEscapingClosureTests.swift +++ b/Tests/JExtractSwiftTests/JNI/JNIEscapingClosureTests.swift @@ -61,6 +61,10 @@ struct JNIEscapingClosureTests { @FunctionalInterface public interface callback { void apply(); + + default void invoke() { + apply(); + } } } """, @@ -97,6 +101,10 @@ struct JNIEscapingClosureTests { @FunctionalInterface public interface closure { long apply(long _0); + + default long invoke(long _0) { + return apply(_0); + } } } """ @@ -150,6 +158,10 @@ struct JNIEscapingClosureTests { @FunctionalInterface public interface callback { void apply(); + + default void invoke() { + apply(); + } } } """