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 @@ -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));
}
}
"""
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
78 changes: 78 additions & 0 deletions Tests/JExtractSwiftTests/JNI/JNIClosureInvokeTests.swift
Original file line number Diff line number Diff line change
@@ -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"
]
)
}
}
4 changes: 4 additions & 0 deletions Tests/JExtractSwiftTests/JNI/JNIClosureTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
}
""",
Expand Down
12 changes: 12 additions & 0 deletions Tests/JExtractSwiftTests/JNI/JNIEscapingClosureTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ struct JNIEscapingClosureTests {
@FunctionalInterface
public interface callback {
void apply();

default void invoke() {
apply();
}
}
}
""",
Expand Down Expand Up @@ -97,6 +101,10 @@ struct JNIEscapingClosureTests {
@FunctionalInterface
public interface closure {
long apply(long _0);

default long invoke(long _0) {
return apply(_0);
}
}
}
"""
Expand Down Expand Up @@ -150,6 +158,10 @@ struct JNIEscapingClosureTests {
@FunctionalInterface
public interface callback {
void apply();

default void invoke() {
apply();
}
}
}
"""
Expand Down
Loading