diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift index 504514f57..2efb29e0c 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift @@ -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) apply(\(apiParams.joined(separator: .comma))); + + default \(functionType.result.javaType) invoke(\(apiParams.joined(separator: .comma))) { + \(returnKeyword)apply(\(arguments)); + } } """ ) 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/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/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(); + } } } """