CLI tool
swiftlang/swift-java avatar
swiftlang/swift-java

swift-java: two ways to call across the Swift and Java boundary

Java interopability support for Swift

1,252 stars125 forksSwiftApache-2.0

At a glance

What is it?
The Swift project's tooling for interoperability in both directions, with a Swift library that wraps Java classes as Swift types, a generator that extracts bindings from either language, and a choice between the Foreign Function and Memory API and JNI that decides the performance floor and the deployment floor. The versions required to build it are newer than the versions it targets.
Who is it for?
swift-java suits a team with an existing Java service and a Swift component, or the other way round, that needs to share a library boundary rather than a network protocol. It does not suit a team that needs the generated bindings to run on Android or on an old JVM, because that rules out the FFM mode, and it does not suit a project that wants a stable published dependency, because the supporting Java libraries are still under development and are not on Maven Central.
Can I use it commercially?
Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 3 days ago.
What is it written in?
Mainly Swift, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 21, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Two directions, two deliverables

This project contains tools and libraries that facilitate Swift and Java interoperability, and it splits into two things that are used in opposite directions.

The Swift library, SwiftJava, comes with a bindings generator that allows a Swift program to make use of Java libraries by wrapping Java classes in corresponding Swift types, which lets Swift call any wrapped Java API directly. The `swift-java` tool offers automated ways to import or extract bindings to sources or libraries in either language, and the results are bindings for Swift or for Java.

So there are three moving parts. SwiftJava is the runtime library, the macros and generated bindings that let Swift code call Java. The `wrap-java` command generates Swift bindings to Java libraries. And `jextract` generates Java bindings to existing Swift libraries.

The quick introduction the README points at is the WWDC25 session, Explore Swift and Java interoperability, and for worked examples it points at the `Samples/` directory, which showcases the various ways to use swift-java in Swift or Java projects.

That last point is the one to act on. The README says plainly that while the project works on more quickstarts and documentation, you should refer to the sample projects, and the sample list in the repository root makes the coverage visible: a Java dependency sample, a Java kit sample, a Java sieve sample, a Gradle plugin sample, a Swift and Java jar FFM sample library, and both a Swift Java extract FFM sample and a Swift Java extract JNI sample. The presence of paired FFM and JNI samples for the same feature is a useful signal about how the two modes differ in practice.

The two jextract modes, and why the choice matters

`jextract` is a source generator that generates Java bindings to existing Swift libraries. Its inputs are Swift sources or packages, and its outputs are the generated Swift and Java code needed to call those functions efficiently from Java. It has two modes, `ffm` and `jni`.

The default is `ffm`, and it offers the most flexibility and performance, and it can reduce the amount of data copied between Swift and Java. It requires the Foreign Function and Memory API from JEP-454, which is final since JDK 22. The README calls this the primary way the project envisions calling Swift code from server-side Java libraries and applications.

The alternative is `jni`, where the generated sources use JNI to call native code. It offers less performance and flexibility than FFM, but it is the most compatible, because it works on older JVMs as well as on Android. The guidance is to use it when FFM is not available or when wide deployment compatibility is the priority, and to prefer FFM when performance is paramount.

The version requirements differ between them, and that is the part to plan around. The FFM mode needs Swift 6.2 because of a dependence on rich Swift interface files, and JDK 25 or newer, because the implementation is validated against the currently supported non-LTS release, which at present means JDK 25. The JNI mode needs the same Swift 6.2, and JDK 17 or newer, with the generated Java sources targeting the `javaSourceLevel` setting, which supports 17 through 25 and defaults to 22.

So the two modes are not two implementations of the same thing. One is the fast path for a modern server, and the other is the compatibility path for a JVM you do not control.

Building it needs a newer JDK than the code it produces

The dependency section of the README opens with a warning that the project consists of multiple modules which currently have different Swift and Java runtime requirements, and you need to install the necessary JDK version locally.

The development requirements spell it out. You need Swift 6.2.x or newer, Java 25 or newer to build the project even though the published libraries target lower JDK versions, and Gradle, which the Gradle wrapper installs automatically when you run it through `./gradlew`.

That gap between the build JDK and the target JDK is the detail that trips people up. Java 25 is required to build the project, and the libraries being published may target lower Java versions. The README also says that the use of JDK 25 is required to build the project, even though the libraries being published may target lower Java versions, and warns in bold to make sure `JAVA_HOME` is exported so swift-java can find the necessary Java libraries. With sdkman the easiest export is the home of the current JDK.

bash
export JAVA_HOME="$(sdk home java current)"

The installation advice in the dependency section is otherwise practical. On macOS you can install a JDK with homebrew, and there is a specific cask, corretto, and a symlink into `/Library/Java/JavaVirtualMachines` for the non-cask install, because that is where the JDK expects to find itself. Alternatively you can use a JDK manager such as sdkman and point `JAVA_HOME` at the installation you want, with `sdk install java 25.0.1-amzn` given as the example command.

Self-publishing the supporting libraries, for now

There is a temporary state in this project that a user has to work around, and the README explains it plainly.

swift-java relies on supporting libraries that are under active development and not yet published to Maven Central. To use the project you need to self-publish these libraries locally so that your Java project can depend on them. The command, run in the `swift-java/` directory, is:

bash
./gradlew publishToMavenLocal

And to consume those libraries in a Java project built with Gradle, you include the local repository in your repositories:

kotlin
repositories {
    mavenLocal()
    mavenCentral()
}

The README adds a single sentence about the future: they anticipate simplifying this in the future.

So the current state is that a consumer of this project has a build step that a normal dependency would not have, and a local Maven repository in the developer's home directory that has to exist before the build works. That is an inconvenience with a clear end date, but it is also a signal about where the project is in its life.

It is worth reading the two sentences together with the release numbers, which are 0.7.0 from 2026-10-14, 0.6.0 from 2026-09-05 and 0.5.1 from 2026-08-10. A pre-1.0 version line with releases every few weeks, and a note that the supporting libraries are not yet published, together say the same thing.

SwiftJava macros, and what they take on the Java side

SwiftJava is a Swift library offering macros which simplify writing JNI code by hand, and calling Java code from Swift.

The point of the macros is to remove the boilerplate. Calling Java from Swift by hand means writing the JNI glue, and the macros generate it. The README's requirement list for this half is much lighter than for jextract: JDK 17 or newer, with any recent JDK installation sufficient because only general reflection and JNI APIs are used by the integration, and Swift 6.2.x because the library uses modern Swift macros.

The version asymmetry is notable. The Swift side needs 6.2 because it uses macros, a relatively recent Swift feature. The Java side needs only 17 because it sticks to reflection and JNI, which have been stable for years. That combination is what makes the SwiftJava path the more deployable of the two, and it is worth knowing that a project using SwiftJava does not need the Foreign Function and Memory API at all.

You can also generate Swift bindings to Java libraries with the `swift-java wrap-java` command, which is the automated path for the case where a Java library is large enough that hand-writing the wrapper is not sensible.

The repository layout backs this up. There is a `Sources/` directory for the library, a `SwiftKitCore/` directory and a `SwiftKitFFM/` directory, the latter named for the Foreign Function and Memory approach, and a `Plugins/` directory for the SwiftPM plugins, plus a `Snippets/` directory. The presence of both a core and an FFM-specific module is the structural counterpart of the two jextract modes.

Testing, and the sample apps that carry the runtime tests

The testing section explains an arrangement that is unusual enough to be worth reading carefully.

Many tests, including source generation tests, are written in Swift, and they all run with `swift test`. When adding tests in the `Tests/` targets you can run those tests, or filter a specific test by type or method name with `swift test --filter`.

Some tests are implemented in Java and need to be executed using Gradle, and the instruction is to always use the Gradle wrapper so the correct Gradle version is used, with `./gradlew test`.

Then the tip, which is the important one. Many of the runtime tests for code relying on `jextract` are located in the sample apps, so if you need to runtime test code that depends on jextract's source generation you should consider adding the tests to an appropriate Sample. Those tests also run in CI, through the `ci-validate.sh` script in each sample that has one.

So the samples are not only documentation. They are the test harness for the generated code, and the per-sample CI script is how a change to the generator gets validated end to end. A contributor who only ran `swift test` would not have exercised the code path that matters most.

The samples are built by default by Gradle, and building them can be skipped by appending the flag `-PskipSamples=true` to a Gradle command, which is how a fast inner loop is kept separate from the full validation.

Where swift-java is the wrong choice

The first wrong case is a deployment target you do not control, such as Android or an old JVM on a client device. The FFM mode needs JEP-454, which is final since JDK 22, and the JNI mode is the compatibility answer, with less performance and flexibility. If your runtime is fixed below the FFM line, you are choosing the slower path from the start.

The second is a project that wants to depend on a published artefact. The supporting Java libraries are not on Maven Central yet, so every consumer has to publish them to their own local repository first, and the README frames that as something they anticipate simplifying. That is fine for a team with a build pipeline and awkward for anyone expecting to add one line to a dependency block.

The third is a team that wants a service boundary. If a Swift service and a Java service are separate deployables, an HTTP or gRPC interface is a better investment than a shared library boundary, because the generated bindings tie the two sides to a specific toolchain and version matrix.

The fourth is a project that cannot take the build requirements. Java 25 to build, Swift 6.2 to compile, Gradle for the Java half and SwiftPM for the Swift half, all in one repository, is a heavy environment for a small interop need.

The fifth is a team expecting a stable API. The version line is pre-1.0, the release cadence is every few weeks, and the project is explicitly still working on quickstarts and documentation.

What the project offers is the thing that is genuinely hard to build by hand: a generator that produces bindings in either direction, with a FFM path for a modern server and a JNI path for everywhere else, and a sample suite that shows the round trip working in both languages.

Editorial conclusion

swift-java suits a team with an existing Java service and a Swift component, or the other way round, that needs to share a library boundary rather than a network protocol. It does not suit a team that needs the generated bindings to run on Android or on an old JVM, because that rules out the FFM mode, and it does not suit a project that wants a stable published dependency, because the supporting Java libraries are still under development and are not on Maven Central. Before adopting it, read the two mode sections carefully, because the JDK 25 requirement is for building the project while the generated code targets a `javaSourceLevel` from 17 to 25, and be prepared to publish the supporting libraries to your local Maven repository for now.

Frequently asked questions

What does swift-java provide?

A Swift library, SwiftJava, with a bindings generator that wraps Java classes in Swift types so Swift can call any wrapped Java API, and the `swift-java` tool, whose `wrap-java` command generates Swift bindings to Java libraries and whose `jextract` command generates Java bindings to Swift libraries.

What is the difference between the ffm and jni modes of jextract?

The ffm mode, which is the default, uses the Foreign Function and Memory API from JEP-454, which is final since JDK 22, and offers the most flexibility and performance. The jni mode generates sources that call native code through JNI, with less performance and flexibility but the widest compatibility, working on older JVMs and on Android.

Which JDK and Swift versions does swift-java need?

Building the project needs Swift 6.2.x or newer and Java 25 or newer, even though the published libraries target lower JDK versions. The jextract ffm mode needs Swift 6.2 and JDK 25, the jni mode needs Swift 6.2 and JDK 17 with a `javaSourceLevel` from 17 to 25, and the SwiftJava macros need Swift 6.2.x and JDK 17 or newer.

Why do I have to publish the supporting libraries myself?

Because swift-java relies on supporting libraries that are under active development and are not yet published to Maven Central. You publish them to your local Maven repository with `./gradlew publishToMavenLocal` and add `mavenLocal()` to the repositories in your Gradle build, and the README says they anticipate simplifying this in the future.

How do I test changes to swift-java?

Many tests, including source generation tests, are written in Swift and run with `swift test`, while tests implemented in Java run with `./gradlew test` using the Gradle wrapper. The runtime tests for jextract live in the sample apps and run in CI through the `ci-validate.sh` script in each sample that has one.

Official sources

  1. License: Apache-2.0
  2. Project website
  3. README
  4. Releases
  5. swiftlang/swift-java on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/swiftlang-swift-java.svg)](https://hysenlabs.com/projects/swiftlang-swift-java)

Was this useful?