Skip to content

jextract: closureMethodName names the method of closure interfaces - #953

Open
piercifani wants to merge 1 commit into
swiftlang:mainfrom
piercifani:jextract-closure-method-name
Open

piercifani wants to merge 1 commit into
swiftlang:mainfrom
piercifani:jextract-closure-method-name

Conversation

@piercifani

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

"closureMethodName", a new option in swift-java.config (JNI mode), names the interfaces' method. The default stays apply.

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

  • a closure value is called as handler("hi");
  • Kotlin lambdas still convert to the interface;
  • Java callers write handler.invoke("hi").

The name is used in four places:

  • the Java interface;
  • the @JavaInterface printed for escaping closures;
  • the call into Java from Swift;
  • 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.

Testing

  • New JNIClosureMethodNameTests (3 tests):
    • the Java interfaces, the Swift wrapper, the call and the method lookup all use invoke;
    • no java.util.function interface is used with a custom name;
    • apply and LongUnaryOperator remain the default.
  • End to end, outside the repository: a package built with JExtractSwiftPlugin and "closureMethodName": "invoke", called from Java. An escaping (String) -> Void stored and called back from Swift, and a non-escaping (Int64) -> Int64, both reach the Java lambdas through invoke.
  • swift test passes; config docs regenerated.

🤖 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? 🤔

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