Summary of features supported by the swift-java interoperability libraries and tools.
SwiftJava supports both directions of interoperability, using Swift macros and source generation
(via the swift-java wrap-java command).
It is possible to use SwiftJava macros and the wrap-java command to simplify implementing
Java native functions. SwiftJava simplifies the type conversions
tip: This direction of interoperability is covered in the WWDC2025 session 'Explore Swift and Java interoperability' around the 7-minute mark.
| Feature | Macro support |
|---|---|
Java static native method implemented by Swift |
✅ @JavaImplementation |
| This list is very work in progress |
tip: This direction of interoperability is covered in the WWDC2025 session 'Explore Swift and Java interoperability' around the 10-minute mark.
| Java Feature | Macro support |
|---|---|
Java class |
✅ |
| Java class inheritance | ✅ |
Java abstract class |
TODO |
Java enum |
❌ |
Java methods: static, member |
✅ @JavaMethod |
| This list is very work in progress |
SwiftJava's swift-java jextract tool automates generating Java bindings from Swift sources.
tip: This direction of interoperability is covered in the WWDC2025 session 'Explore Swift and Java interoperability' around the 14-minute mark.
| Swift Feature | FFM | JNI |
|---|---|---|
Initializers: class, struct |
✅ | ✅ |
| Optional Initializers / Throwing Initializers | ❌ | ✅ |
Deinitializers: class, struct |
✅ | ✅ |
enum |
❌ | ✅ |
actor |
❌ | ❌ |
Global Swift func |
✅ | ✅ |
Class/struct member func |
✅ | ✅ |
Throwing functions: func x() throws |
❌ | ✅ |
Typed throws: func x() throws(E) |
❌ | ❌ |
Stored properties: var, let (with willSet, didSet) |
✅ | ✅ |
Computed properties: var (incl. throws) |
✅ / TODO | ✅ |
Async functions func async and properties: var { get async {} } |
❌ | ✅ |
Arrays: [UInt8], [MyType], Array<Int64> etc |
❌ | ✅ |
Dictionaries: [String: Int], [K:V] |
❌ | ❌ |
Generic parameters in functions: func f<T: A & B>(x: T) |
❌ | ✅ |
Generic return values in functions: func f<T: A & B>() -> T |
❌ | ❌ |
Tuples: (Int, String), (A, B, C) |
❌ | ❌ |
Protocols: protocol |
❌ | ✅ |
Protocols: protocol with associated types |
❌ | ❌ |
Existential parameters f(x: any SomeProtocol) (excepts Any) |
❌ | ✅ |
Existential parameters f(x: any (A & B)) |
❌ | ✅ |
Existential return types f() -> any Collection |
❌ | ❌ |
Foundation Data and DataProtocol: f(x: any DataProtocol) -> Data |
✅ | ❌ |
Opaque parameters: func take(worker: some Builder) -> some Builder |
❌ | ✅ |
Opaque return types: func get() -> some Builder |
❌ | ❌ |
Optional parameters: func f(i: Int?, class: MyClass?) |
✅ | ✅ |
Optional return types: func f() -> Int?, func g() -> MyClass? |
❌ | ✅ |
Primitive types: Bool, Int, Int8, Int16, Int32, Int64, Float, Double |
✅ | ✅ |
Parameters: SwiftJava wrapped types JavaLong, JavaInteger |
❌ | ✅ |
Return values: SwiftJava wrapped types JavaLong, JavaInteger |
❌ | ❌ |
Unsigned primitive types: UInt, UInt8, UInt16, UInt32, UInt64 |
✅ * | ✅ * |
| String (with copying data) | ✅ | ✅ |
Variadic parameters: T... |
❌ | ❌ |
| Parametrer packs / Variadic generics | ❌ | ❌ |
Ownership modifiers: inout, borrowing, consuming |
❌ | ❌ |
Default parameter values: func p(name: String = "") |
❌ | ❌ |
Operators: +, -, user defined |
❌ | ❌ |
Subscripts: subscript() |
✅ | ✅ |
| Equatable | ❌ | ❌ |
Pointers: UnsafeRawPointer, UnsafeBufferPointer (?) |
🟡 | ❌ |
Nested types: struct Hello { struct World {} } |
❌ | ✅ |
Inheritance: class Caplin: Capybara |
❌ | ❌ |
Non-escaping Void closures: func callMe(maybe: () -> ()) |
✅ | ✅ |
Non-escaping closures with primitive arguments/results: func callMe(maybe: (Int) -> (Double)) |
✅ | ✅ |
Non-escaping closures with object arguments/results: func callMe(maybe: (JavaObj) -> (JavaObj)) |
❌ | ❌ |
@escaping closures: func callMe(_: @escaping () -> ()) |
❌ | ❌ |
Swift type extensions: extension String { func uppercased() } |
✅ | ✅ |
| Swift macros (maybe) | ❌ | ❌ |
| Result builders | ❌ | ❌ |
| Automatic Reference Counting of class types / lifetime safety | ✅ | ✅ |
| Value semantic types (e.g. struct copying) | ❌ | ❌ |
tip: The list of features may be incomplete, please file an issue if something is unclear or should be clarified in this table.
Java does not support unsigned numbers (other than the 16-bit wide char), and therefore mapping Swift's (and C)
unsigned integer types is somewhat problematic.
SwiftJava's jextract mode, similar to OpenJDK jextract, does extract unsigned types from native code to Java
as their bit-width equivalents. This is potentially dangerous because values larger than the MAX_VALUE of a given
signed type in Java, e.g. 200 stored in an UInt8 in Swift, would be interpreted as a byte of value -56,
because Java's byte type is signed.
Because in many situations the data represented by such numbers is merely passed along, and not interpreted by Java, this may be safe to pass along. However, interpreting unsigned values incorrectly like this can lead to subtle mistakes on the Java side.
| Swift type | Java type |
|---|---|
Int8 |
byte |
UInt8 |
byte |
Int16 |
short |
UInt16 |
char |
Int32 |
int |
UInt32 |
int |
Int64 |
long |
UInt64 |
long |
Float |
float |
Double |
double |
Note: Enums are currently only supported in JNI mode.
Swift enums are extracted into a corresponding Java class. To support associated values
all cases are also extracted as Java records.
Consider the following Swift enum:
public enum Vehicle {
case car(String)
case bicycle(maker: String)
}You can then instantiate a case of Vehicle by using one of the static methods:
try (var arena = SwiftArena.ofConfined()) {
Vehicle vehicle = Vehicle.car("BMW", arena);
Optional<Vehicle.Car> car = vehicle.getAsCar();
assertEquals("BMW", car.orElseThrow().arg0());
}As you can see above, to access the associated values of a case you can call one of the
getAsX methods that will return an Optional record with the associated values.
try (var arena = SwiftArena.ofConfined()) {
Vehicle vehicle = Vehicle.bycicle("My Brand", arena);
Optional<Vehicle.Car> car = vehicle.getAsCar();
assertFalse(car.isPresent());
Optional<Vehicle.Bicycle> bicycle = vehicle.getAsBicycle();
assertEquals("My Brand", bicycle.orElseThrow().maker());
}If you only need to switch on the case and not access any associated values,
you can use the getDiscriminator() method:
Vehicle vehicle = ...;
switch (vehicle.getDiscriminator()) {
case BICYCLE:
System.out.println("I am a bicycle!");
break;
case CAR:
System.out.println("I am a car!");
break;
}If you also want access to the associated values, you have various options depending on the Java version you are using. If you are running Java 21+ you can use pattern matching for switch:
Vehicle vehicle = ...;
switch (vehicle.getCase()) {
case Vehicle.Bicycle b:
System.out.println("Bicycle maker: " + b.maker());
break;
case Vehicle.Car c:
System.out.println("Car: " + c.arg0());
break;
}or even, destructuring the records in the switch statement's pattern match directly:
Vehicle vehicle = ...;
switch (vehicle.getCase()) {
case Vehicle.Car(var name, var unused):
System.out.println("Car: " + name);
break;
default:
break;
}For Java 16+ you can use pattern matching for instanceof
Vehicle vehicle = ...;
Vehicle.Case case = vehicle.getCase();
if (case instanceof Vehicle.Bicycle b) {
System.out.println("Bicycle maker: " + b.maker());
} else if(case instanceof Vehicle.Car c) {
System.out.println("Car: " + c.arg0());
}For any previous Java versions you can resort to casting the Case to the expected type:
Vehicle vehicle = ...;
Vehicle.Case case = vehicle.getCase();
if (case instanceof Vehicle.Bicycle) {
Vehicle.Bicycle b = (Vehicle.Bicycle) case;
System.out.println("Bicycle maker: " + b.maker());
} else if(case instanceof Vehicle.Car) {
Vehicle.Car c = (Vehicle.Car) case;
System.out.println("Car: " + c.arg0());
}JExtract also supports extracting enums that conform to RawRepresentable
by giving access to an optional initializer and the rawValue variable.
Consider the following example:
public enum Alignment: String {
case horizontal
case vertical
}you can then initialize Alignment from a String and also retrieve back its rawValue:
try (var arena = SwiftArena.ofConfined()) {
Optional<Alignment> alignment = Alignment.init("horizontal", arena);
assertEqual(HORIZONTAL, alignment.orElseThrow().getDiscriminator());
assertEqual("horizontal", alignment.orElseThrow().getRawValue());
}Note: Protocols are currently only supported in JNI mode.
With the exception of
any DataProtocolwhich is handled asFoundation.Datain the FFM mode.
Swift protocol types are imported as Java interfaces. For now, we require that all
concrete types of an interface wrap a Swift instance. In the future, we will add support
for providing Java-based implementations of interfaces, that you can pass to Java functions.
Consider the following Swift protocol:
protocol Named {
var name: String { get }
func describe() -> String
}will be exported as
interface Named extends JNISwiftInstance {
public String getName();
public String describe();
}Any opaque, existential or generic parameters are imported as Java generics. This means that the following function:
func f<S: A & B>(x: S, y: any C, z: some D)will be exported as
<S extends A & B, T1 extends C, T2 extends D> void f(S x, T1 y, T2 z) On the Java side, only SwiftInstance implementing types may be passed; so this isn't a way for compatibility with just any arbitrary Java interfaces, but specifically, for allowing passing concrete binding types generated by jextract from Swift types which conform a to a given Swift protocol.
Protocols are not yet supported as return types.
Note: Importing
asyncfunctions is currently only available in the JNI mode of jextract.
Asynchronous functions in Swift can be extraced using different modes, which are explained below.
In this mode async functions in Swift are extracted as Java methods returning a java.util.concurrent.CompletableFuture.
This mode gives the most flexibility and should be prefered if your platform supports CompletableFuture.
This is a mode for legacy platforms, where CompletableFuture is not available, such as Android 23 and below.
In this mode async functions in Swift are extracted as Java methods returning a java.util.concurrent.Future.
To enable this mode pass the --async-func-mode future command line option,
or set the asyncFuncMode configuration value in swift-java.config