Skip to content

Latest commit

 

History

History
316 lines (266 loc) · 15.1 KB

File metadata and controls

316 lines (266 loc) · 15.1 KB

Supported Features

Summary of features supported by the swift-java interoperability libraries and tools.

Overview

SwiftJava supports both directions of interoperability, using Swift macros and source generation (via the swift-java wrap-java command).

Java -> Swift

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

Swift -> Java

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

JExtract – calling Swift from Java

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.

Detailed feature support discussion

Unsigned integers

Java <-> Swift Type mapping

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

Enums

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());
}

Switching and pattern matching

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());
}

RawRepresentable enums

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());
}

Protocols

Note: Protocols are currently only supported in JNI mode.

With the exception of any DataProtocol which is handled as Foundation.Data in 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();
}

Parameters

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.

Returning protocol types

Protocols are not yet supported as return types.

async functions

Note: Importing async functions is currently only available in the JNI mode of jextract.

Asynchronous functions in Swift can be extraced using different modes, which are explained below.

Async function mode: completable-future (default)

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.

Async mode: future

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