Swift 6.2 Java interoperability in practice
If you’ve ever tried straddling two worlds—one foot in the elegant, type-safe realm of Swift and the other in the sprawling ecosystem of Java—you know the pain of keeping them in sync. Swift 6.2 introduces a first-class Swift–Java interoperability layer via the open-source Swift–Java package, a bridge that finally feels less like a rickety rope and more like a solid handshake between two old friends. In this article, we’ll explore how the swift-java package makes it possible to call Java APIs from Swift with minimal ceremony, dive into real-world examples, and uncover some of the hidden quirks that can turn this smooth ride into a bumpy road. Grab your favorite coffee, and let’s demystify the art of weaving Swift and Java together—hacky workarounds and all.
Prerequisites
Before diving into the code, make sure you’ve got the right toolkit:
- JDK 24: Java Development Kit 24 is required to compile and run any Java code you’ll be bridging into. If you haven’t installed it yet, grab it from the Oracle website or use your favorite package manager (SDKMAN! or Homebrew both have you covered).
- Xcode 26 Beta: Swift–Java interop relies on some of the latest compiler hooks only available in Xcode 26 Beta. Download it from Apple’s developer portal and switch your toolchain in Xcode’s Preferences > Components.
Once you’ve got JDK 24 and Xcode 26 Beta installed, you’re ready to move on to setting up the swift-java package itself.
Project structure
Before we jump into the code, here’s a bird’s-eye view of our repo layout—just enough to keep your bearings:
.
├── java-lib
│ └── src
│ └── com
│ └── example
│ └── Greeting.java
├── sim
│ ├── Sources
│ │ ├── sim
│ │ │ ├── Simulation.swift
│ │ │ └── swift-java.config
│ │ └── sim-app
│ │ └── main.swift
│ ├── Package.swift
└── Makefile
- java-lib/src/com/example/Greeting.java: A simple Java class exposing sayHello().
- sim/Sources/sim/Simulation.swift: Swift entry point demonstrating interop calls.
- sim/Sources/sim/swift-java.config: Configuration file for the swift-java bridge.
- sim/Sources/sim-app/main.swift: Sample app wiring everything together.
- Package.swift & Makefile: Build definitions to compile Java and Swift targets in one go.
Java side
Let’s kick off with our Java side. In java-lib/src/com/example/Greeting.java
, we have a minimal class exposing a static greet
method:
package com.example;
public class Greeting {
public static String greet(String name) {
return "Hello, " + name + "!";
}
}
Nothing magical here—just a single entry point for Swift to invoke. We’ll compile this into a JAR and point our Swift bridge at it in the next section.
Java Build Step
Before we wire up Swift, let’s compile and package our Java library. We’ll use the java
target from the Makefile:
JAVAC := javac
JAR := jar
BUILD := build
.PHONY: java
java:
@echo "Compiling Java library..."
mkdir -p $(BUILD)/java
$(JAVAC) -d $(BUILD)/java java-lib/src/com/example/Greeting.java
$(JAR) cf $(BUILD)/java/Greeting.jar -C $(BUILD)/java com/example/Greeting.class
Run make java
to produce build/java/Greeting.jar
(which we’ll reference from Swift).
Preparing Interop
To wire Swift and Java together, we need a swift-java.config
file in sim/Sources/sim/
:
{
"classpath": "../build/java/Greeting.jar",
"classes": {
"com.example.Greeting": "Greeting"
}
}
- classpath: Points to the JAR we built earlier.
- classes: Maps the fully-qualified Java class name to the Swift-friendly alias.
With this in place, running swift build
will auto-generate the necessary Swift wrappers for Greeting
.
Swift Simulator
Next, let’s inspect our Swift-side adapter in sim/Sources/sim/Simulation.swift
, but pay extra attention to the classpath—this tripped me up for two days:
import JavaKit
import Foundation
public struct Simulation {
public static func run(with name: String) {
do {
// 🚨 CLASSPATH IS EVERYTHING 🚨
// Any custom JAR not on the standard Java classpath *must* be listed here.
let jvm = try JavaVirtualMachine.shared(
classpath: ["../build/java/Greeting.jar"] // <- this line is non-negotiable
)
let jniEnvironment = try jvm.environment()
let greetingClass = try JavaClass<Greeting>(environment: jniEnvironment)
print(greetingClass.greet(name))
} catch {
print("Failure: \(error)")
}
}
}
Why the fuss about classpath?
- JavaKit spawns a JVM in-process and only loads classes it knows about.
- If your JAR isn’t explicitly on the
classpath
array, it won’t be found—and you’ll get inexplicableClassNotFound
errors. - Even if it lives in your project, Swift’s build system won’t magically add it for you.
Emphasizing once more: any JAR outside of the standard Java classpath must go into that classpath
array.
Static vs. Instance Methods
- Static methods (like our
greet
) require theJavaClass<T>
wrapper to invoke directly on the class.
Instance methods (non-static) can be called by constructing an object:
let greetingObj = try Greeting(environment: jniEnvironment)
print(try greetingObj.someInstanceMethod())
With that settled, you’re ready to wire up and run your simulation.
Command-Line Entry Point
Finally, let’s glue everything together in our simple CLI app. In sim/Sources/sim-app/main.swift
, we have:
import sim
let name = CommandLine.arguments.dropFirst().first ?? "World"
Simulation.run(with: name)
Here’s what’s happening:
- Import the module: We bring in our
sim
package, which contains theSimulation
struct. - Parse the argument: We drop the executable name and take the first argument as the user’s name, defaulting to "World" if none is provided.
- Run the simulation: Pass the name into
Simulation.run
, which boots the JVM, invokes the Javagreet
method, and prints the result.
We’ll wire up the Swift side via our Package.swift
in the next paragraph, so hold off on running for now.
Swift Package Configuration
Below is our Package.swift
(swift-tools-version: 6.2) lifted from the java-swift examples, with extra logic to locate JAVA_HOME
and configure JNI include paths:
// swift-tools-version: 6.2
import PackageDescription
import class Foundation.FileManager
import class Foundation.ProcessInfo
// Note: the JAVA_HOME environment variable must be set to point to where
// Java is installed, e.g.,
// /Library/Java/JavaVirtualMachines/jdk-24.jdk/Contents/Home
func findJavaHome() -> String {
if let home = ProcessInfo.processInfo.environment["JAVA_HOME"] {
return home
}
// Workaround for IDEs that don’t pick up shell envs during builds
let path = "\(FileManager.default.homeDirectoryForCurrentUser.path()).java_home"
if let home = try? String(contentsOfFile: path, encoding: .utf8) {
return home.trimmingCharacters(in: .newlines)
}
fatalError("Please set JAVA_HOME to your JDK installation.")
}
let javaHome = findJavaHome()
let javaIncludePath = "\(javaHome)/include"
#if os(Linux)
let javaPlatformIncludePath = "\(javaIncludePath)/linux"
#elseif os(macOS)
let javaPlatformIncludePath = "\(javaIncludePath)/darwin"
#else
#error("Only macOS and Linux are supported currently.")
#endif
let package = Package(
name: "java-sim",
platforms: [.macOS(.v15)],
dependencies: [
.package(url: "https://github.com/swiftlang/swift-java.git", branch: "main"),
],
targets: [
.target(
name: "sim",
dependencies: [
.product(name: "JavaKit", package: "swift-java"),
.product(name: "JavaKitJar", package: "swift-java"),
.product(name: "JavaKitCollection", package: "swift-java"),
],
exclude: ["swift-java.config"],
swiftSettings: [
.unsafeFlags(["-I\(javaIncludePath)", "-I\(javaPlatformIncludePath)"])
],
plugins: [
// Auto-generates Swift wrappers for our Java classes into `.build`
.plugin(name: "SwiftJavaPlugin", package: "swift-java"),
]
),
.executableTarget(
name: "sim-app",
dependencies: ["sim"]
)
]
)
Environment Variables
JAVA_HOME
must point to your JDK 24 install and be exported in your shell (e.g., in ~/.zshrc
):
export JAVA_HOME="/Library/Java/JavaVirtualMachines/jdk-24.jdk/Contents/Home"
Plugins Explained
- The
SwiftJavaPlugin
integrates into SwiftPM’s build process to generate JNI bindings. - Generated Swift wrapper code for your Java classes appears under SwiftPM’s hidden
.build
directory—no manual glue code needed.
Once JAVA_HOME
is set and you’ve updated Package.swift
, we’re still not running swift build
directly. Instead, we’ll modify our Makefile in the next section so that invoking make
will handle both Java compilation and Swift package builds seamlessly—no manual commands required.
Unified Build via Makefile
Below is our updated Makefile that ties everything together:
JAVAC := javac
JAR := jar
BUILD := build
.PHONY: all java swift run clean
all: java swift
java:
@echo "Compiling Java library..."
mkdir -p $(BUILD)/java
$(JAVAC) -d $(BUILD)/java java-lib/src/com/example/Greeting.java
$(JAR) cf $(BUILD)/java/Greeting.jar -C $(BUILD)/java com/example/Greeting.class
swift:
@echo "Building Swift package (auto-generates wrappers)..."
cd sim && swift build
run:
@echo "Running sample app..."
cd sim && swift run sim-app $(name)
clean:
rm -rf $(BUILD) sim/.build
Build Order:
make
– compiles Java and builds the Swift package (including wrapper generation).make run name=John
– runs the CLI app withJohn
as the argument, printingHello, John!
.
With this in place, you only need to type make run name=YourName
to go from code to interop output in one command.
Summary
In this guide, we explored how Swift 6.2’s swift-java package bridges the gap between Swift and Java, enabling seamless method calls across runtimes. We started by setting up our environment—JDK 24 and Xcode 26 Beta—before compiling our simple Greeting
Java class into a JAR. We then configured the interop via swift-java.config
, emphasizing the critical role of the classpath in avoiding ClassNotFound
errors. On the Swift side, we built a Simulation
struct using JavaVirtualMachine.shared
and JavaClass<Greeting>
, highlighted the difference between static and instance method invocation, and provided a minimal CLI entry point in main.swift
. We demonstrated how the SwiftJavaPlugin
in Package.swift
auto-generates JNI bindings (with JAVA_HOME
correctly exported) and finalized the setup by unifying the workflow in a Makefile. With a single make run name=YourName
, you can now invoke Java code from Swift effortlessly—no more rope bridges, just a solid handshake between two ecosystems.