Skip to content

jextract: Foundation values as standard Java types - #952

Open
piercifani wants to merge 2 commits into
swiftlang:mainfrom
piercifani:jextract-foundation-java-types
Open

piercifani wants to merge 2 commits into
swiftlang:mainfrom
piercifani:jextract-foundation-java-types

Conversation

@piercifani

Copy link
Copy Markdown

Problem

In JNI mode, Data, Date and URL reach Java as Swift-backed wrapper classes. jextract generates those classes into the Java package of every module that uses them, so a codebase with several Swift modules ends up with several URL classes: com.example.models.URL, com.example.features.URL and so on. A URL returned by one module's API is a different Java type from the URL another module's API accepts, so the value can't be passed along. Running jextract over a production codebase with 9 modules generated a URL class in 7 of them.

The wrappers have other costs too:

  • Each value is a Swift heap allocation that Java code has to release.
  • Optional URL? parameters aren't supported.
  • Java and Kotlin code mostly wants java.net.URI, java.util.Date and byte[] anyway.

UUID already avoids all of this: it crosses as java.util.UUID.

Changes

"foundationTypesAsJavaTypes": true in swift-java.config (JNI mode) makes these Foundation values cross as standard Java types, and stops generating the wrapper classes:

Swift Java
URL java.net.URI
Date java.util.Date
Data byte[]
Locale java.util.Locale
TimeZone java.util.TimeZone
  • Runtime. The five types conform to JavaValue in SwiftJavaRuntimeSupport, which the generated thunks already import, so they work wherever a String does: parameters, results, optionals, arrays and closure parameters. Values are copied: a Date keeps millisecond precision, and a Locale crosses as its language tag (pt_BR in Swift is pt-BR in Java).
  • Generator. JNIJavaTypeTranslator maps the kinds to the Java types when the option is on, and the places that special-case the wrapper classes defer to it then. ExtractDecider.shouldExtractFoundationType(_:), defaulting to true, lets the Java decider skip the wrapper types.
  • New known types. Locale, TimeZone and CGFloat become known types in SwiftExtract. CGFloat is a Java double, with or without the option: it was unsupported before.

The option is off by default, so existing generated APIs don't change. Not covered yet: Java-implemented protocols (enableJavaCallbacks) with these types. They go through wrap-java's own type mapping, so jextract skips such a protocol with a warning, as it does for other unsupported types.

Testing

  • New JNIFoundationJavaTypesTests, with Java signatures and Swift thunks for every type in each position, and CGFloat without the option.
  • New FoundationJavaValueTests in SwiftJavaTests (5 tests on a real JVM). They round-trip each type through JNI and check the Java side: a java.net.URI with its percent-encoding intact, and a byte[] of the right length.
  • End to end, outside the repository: a package built with JExtractSwiftPlugin and this option, called from Java. 21 checks passed, covering every type as a parameter, result, Optional (parameter and result), array and closure argument, including unsigned bytes in Data and a custom Java TimeZone ID.
  • On that 9-module codebase, the option takes the wrapper classes from 15 to 0 and exports 66 declarations that weren't extracted before (optional URL, Date and Data parameters, among others). The Java still compiles with the same number of javac errors: 2 go away (wrapper names that collided with the codebase's own types), and 1 is a separate existing bug that now becomes reachable. An enum case with a closure payload references a functional interface that isn't generated, with or without this option: case custom((String) -> Bool) fails the same way on main.
  • swift test passes; config docs regenerated, and FeaturesJextract.md gains a "Foundation values as Java types" section.

🤖 Generated with Claude Code

piercifani and others added 2 commits November 4, 2026 12:48
The `Data`, `Date` and `URL` wrapper classes are generated into the Java
package of every module that uses them, so a `URL` returned by one module's
API is a different Java type from the `URL` another module's API accepts.

With `"foundationTypesAsJavaTypes": true` (JNI mode), Foundation values cross
as standard Java types and no wrapper classes are generated: `URL` as
`java.net.URI`, `Date` as `java.util.Date`, `Data` as `byte[]`, `Locale` as
`java.util.Locale` and `TimeZone` as `java.util.TimeZone`. They conform to
`JavaValue` in SwiftJavaRuntimeSupport, so they work wherever a `String` does:
parameters, results, optionals, arrays and closure parameters.

`Locale`, `TimeZone` and `CGFloat` become known types. `CGFloat` is a Java
`double`, with or without the option.

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