Skip to content

jextract: choose what to export with @JavaExport(false) and exportMode - #951

Open
piercifani wants to merge 4 commits into
swiftlang:mainfrom
piercifani:jextract-export-control
Open

piercifani wants to merge 4 commits into
swiftlang:mainfrom
piercifani:jextract-export-control

Conversation

@piercifani

@piercifani piercifani commented Nov 4, 2026 •

Copy link
Copy Markdown

Builds on #950 (prefix and postfix operators); only the second commit is new here.

Problem

jextract exports every declaration that passes the access level. To leave something out, the only option is a file or type pattern in swift-java.config (swiftFilterExclude). You can't leave out a single member, and a module can't export only the few declarations it means to.

In a large codebase this gets in the way:

  • Warnings on Swift-only API. Public API with no Java mapping, such as tuples, [String: Any], Combine publishers or Codable plumbing, is reported as skipped on every run.
  • Build failures. A declaration that is translated incorrectly breaks the build of the whole module, even when nobody needs it in Java.
  • Size. Every exported declaration costs a JNI entrypoint and generated Java.

Running jextract over every public declaration of a production codebase with about 2,900 skipped declarations showed the scale. About 2,000 of those skips, and three quarters of the declarations whose generated Java didn't compile, were in code the codebase doesn't need in Java.

Changes

  • @JavaExport(false) keeps a declaration out of the bindings. On a type or an extension, it applies to all their members and nested types:

    import SwiftJavaAnnotations // or SwiftJava, which re-exports it
    
    public struct Session {
      public var token: String
      @JavaExport(false) public var debugDump: [String: Any]
    }
    
    @JavaExport(false)
    extension Session { ... }

    @JavaExport keeps working as before; the macro now takes an optional Bool.

  • "exportMode": "annotated" in swift-java.config exports only declarations marked @JavaExport, and the members of a type or extension marked @JavaExport. An unmarked type is still exported, with only its marked members, when one of its members is marked. The default, all, keeps today's behavior.

  • Both are decided in JavaExtractDecider. The nearest marked enclosing declaration is found by walking up the syntax tree, so extensions and nested types need nothing extra. Declarations that aren't exported never reach translation.

  • ExtractDecider.mayExtractMembers(of:), defaulting to true. An extension of a type that doesn't resolve, such as a Foundation type, is no longer reported as skipped when none of its members can be exported. Without this, annotated mode still warned about every such extension.

  • Unexported types. A declaration whose signature uses a type that isn't exported is skipped with "uses 'T', which is not exported", instead of generating Java that refers to a missing class. ExtractDecider.isExported(_:) answers this for types from the module and from its dependencies, using each dependency's exportMode from its swift-java.config.

  • Foundation types in annotated mode. The Data, Date and URL types jextract extracts when a module uses them are never filtered out by export mode, since SwiftExtract describes them itself and they carry no markers.

  • A new SwiftJavaAnnotations module and product holds the JavaExport macro, with no dependency on the JNI runtime. Code that is also built for platforms without Java can then carry the marker. SwiftJava re-exports it, so existing @JavaExport uses compile unchanged.

Docs: the new option is in SwiftJavaConfigFile.md (regenerated with generate-config-docs), and SwiftJavaJextract.md gains a "Choosing what to export" section.

Open to other spellings, for example @JavaExport(.never) or a separate @JavaIgnore, or a different name for exportMode.

Testing

  • New JNIExportControlTests (7 tests):
    • opted-out members, types and extensions don't appear in the Java output;
    • annotated mode exports exactly the marked declarations;
    • in both modes, the diagnostics sink receives no skipped declaration for code that isn't exported, including an extension of an unresolved type. Without the markers, the same source reports 4;
    • annotated mode keeps the Date and URL types;
    • a declaration using an unexported type, from the module or from a dependency, is skipped with that reason, and kept when the dependency exports everything.
  • swift test passes.

🤖 Generated with Claude Code

piercifani and others added 2 commits November 4, 2026 11:57
JavaExtractDecider meant to skip prefix and postfix operators by checking
the name token for .prefixOperator or .postfixOperator. In a declaration
such as `public static prefix func - (value: Number) -> Number` the name is
lexed as a binary operator, and only the `prefix` modifier tells it apart.
So the operator was extracted as a binary operator, and in JNI mode the
Swift thunk printer stopped with `Binary operator must have exactly 2
arguments`. Check the modifiers instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
jextract exports every declaration that passes the access level, and the
only way to leave one out is a file or type pattern in swift-java.config.

- `@JavaExport(false)` keeps a declaration out of the bindings. On a type or
  an extension it applies to all their members and nested types.
- `"exportMode": "annotated"` exports only declarations marked `@JavaExport`
  and the members of marked types and extensions; an unmarked type is still
  exported, with only its marked members, when one of them is marked. The
  default, `all`, keeps today's behavior.

Both are decided in JavaExtractDecider, so declarations that aren't exported
are never translated. ExtractDecider gains mayExtractMembers(of:), so an
extension of an unresolved type is not reported as skipped when none of its
members can be exported.

The JavaExport macro moves to a new SwiftJavaAnnotations module without the
JNI dependency, so code that is also built without Java can carry the marker.
SwiftJava re-exports it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@piercifani
piercifani requested a review from ktoso as a code owner November 4, 2026 11:08
piercifani and others added 2 commits November 4, 2026 12:13
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nexported types

Two gaps in `exportMode: annotated`:

- The `Data`, `Date` and `URL` wrapper types jextract extracts when a module
  uses them come from SwiftExtract's own description of Foundation, which has
  no `@JavaExport` markers, so annotated mode dropped them while exported
  declarations still referred to them. Declarations from SwiftExtract's known
  module sources are no longer filtered by export mode.
- A declaration whose signature used a type that isn't exported, from this
  module or a dependency, produced Java referring to a class that doesn't
  exist. ExtractDecider.isExported(_:) now answers that, using each
  dependency's `exportMode` from its swift-java.config, and such declarations
  are skipped with "uses 'T', which is not exported".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

1 participant