Skip to content

jextract: closure interfaces get a default invoke method for Kotlin - #953

Open
piercifani wants to merge 2 commits into
swiftlang:mainfrom
piercifani:jextract-closure-method-name
Open

piercifani wants to merge 2 commits into
swiftlang:mainfrom
piercifani:jextract-closure-method-name

Conversation

@piercifani

@piercifani piercifani commented Nov 4, 2026 •

Copy link
Copy Markdown

Problem

A Swift closure crosses to Java as a functional interface with one method, apply. From Kotlin, passing a lambda into Swift reads naturally, since Kotlin converts { … } to the interface. Calling a closure value doesn't:

handler.apply("hi")

Kotlin function types are called as handler("hi"), and that's what Kotlin code expects.

Changes

Each closure interface jextract generates in JNI mode keeps apply and adds a default invoke method that calls it:

@FunctionalInterface
public interface handler {
  void apply(java.lang.String _0);

  default void invoke(java.lang.String _0) {
    apply(_0);
  }
}

Kotlin treats a Java method named invoke as the invoke operator, so:

  • a closure value is called as handler("hi");
  • Kotlin lambdas still convert to the interface, since apply stays its only abstract method;
  • Java callers are unaffected and keep calling apply.

There's no new configuration option, and the Swift side is unchanged: it still calls apply. Closures that a java.util.function interface covers, such as (Int64) -> Int64 with LongUnaryOperator, keep that interface and its method.

Testing

  • New JNIClosureInvokeTests (2 tests): interfaces with and without a result forward invoke to apply, a java.util.function closure keeps its interface, and the Swift side still calls apply.
  • Updated expected interfaces in JNIClosureTests and JNIEscapingClosureTests.
  • Kotlin 2.2 calls a Java interface's default invoke as h("x") and still converts a lambda to the interface.
  • swift test and ./gradlew :Samples:SwiftJavaExtractJNISampleApp:test pass.

🤖 Generated with Claude Code

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 <noreply@anthropic.com>
@piercifani
piercifani requested a review from ktoso as a code owner November 4, 2026 14:32
@sidepelican

Copy link
Copy Markdown
Contributor

I think that's a good point.
Since adding more options is a hassle, how about we basically generate it as invoke, and then additionally generate things like applyAsLong for KnownJavaFunctionalInterface cases?

@ktoso

ktoso commented Nov 5, 2026

Copy link
Copy Markdown
Collaborator

I wouldn't mind switching to calling it invoke by default if it happens to help kotlin interop. For specific interfaces we need we'd generate the specific names still.

Could you look into that?

@ktoso

ktoso commented Nov 5, 2026

Copy link
Copy Markdown
Collaborator

alternatively we could generate a default method invoke perhaps? 🤔

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 <noreply@anthropic.com>
@piercifani piercifani changed the title jextract: closureMethodName names the method of closure interfaces jextract: closure interfaces get a default invoke method for Kotlin Nov 7, 2026
@piercifani

Copy link
Copy Markdown
Author

Thanks both! I went with the default method. Each generated closure interface keeps apply and adds default R invoke(...) { return apply(...); }:

  • Kotlin treats it as the invoke operator, so handler("hi") works.
  • Kotlin lambdas still convert to the interface, since apply is still the only abstract method.
  • Java callers and the Swift side don't change.

The closureMethodName option is gone. Closures that a java.util.function interface covers keep that interface. I've updated the description to match.

Happy to do any further tweaks to this! Cheers!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants