Jextract-swift high-level design discussion

Hi everyone,
To complement the number of new issues on the github repo, I'd like to share with you some design goals and ideas that shape the java interop work we've embarked on here that should help wrap your head around how it all fits together.

First and foremost, the swift-java project consists of two general "approaches" to the problem. While we may yet rename and reorganize them around a bit, in general the two sides of the project are:

Swift-Java Components

Swift -> Java

JavaKit - a collection of Swift macros and a tool that generates Swift sources (with those @Java... annotations), which allow Swift code to call into Java. In general, it's based around those macros forming the necessary JNI calls.

Examples: com.example.swift.HelloSwift (Java) & the Swift side of this class, implementing some methods marked as native in Java as well as declaring the rest as @JavaMethods

Java -> Swift

jextract-swift tool -- that generates Java sources, by ingesting swift interface files. Our primary focus here right now is to leverage the newly released (in March 2024, as part of JDK22), JEP 454: Foreign Function & Memory API, also known as Project Panama.

jextract-swift is follows a similar idea to the JDK's jextract tool, which extracts accessors from C headers. However, instead of using C as a common ground, jextract-swift is directly importing *.swiftinterface files.

SwiftKit support library -- is a Java library that would include some common helpers and utilities for working with Swift types. Including a SwiftArena that can take care to not leak objects etc.

--

Another distinction between the two approaches is which side of the code needs zero changes. With javakit we're able to write all accessors in Swift, and with jextract we're able to import unmodified Swift libraries and expose them to Java.

--

jextract-swift discussion

In this post I'd like to discuss ideas about jextract-swift.

In general, one should keep in mind that jextract-swift is a Java source generator, so anything we can do to make the Java-experience of calling Swift good in the generator we should consider doing.

Runtime requirements

Currently we are focusing on JEP-454, because we'd like to utilize it to create an unique and high-performance Java/Swift interop story for server systems.

This aspect is quite important for calling Swift in hot-paths of services. You can imagine use-cases involving cryptography libraries implemented in Swift, which are a common use-case for JNI nowadays, and we'd like to make it both easier to use and more performant when the native side of such calls is Swift.

The use of JEP-454 means that this specific path is not open for legacy systems, when a deployment cannot use JDK22+ (which is not an LTS release), or if the target system is Android (or similar).

Since jextract-swift is just™ a source generator tool, we could offer it another mode that would generate the Java wrapper classes with downcalls implemented using JNI. In fact, here's the issue about this: JNI mode for jextract-swift #25.

The JNI mode would probably come with different import limitations (what kinds of Swift interfaces we can import/expose as Java), however it would be the most compatible wrt. JDK versions as well as platforms (e.g. Android).

TL;DR;

  • Right now the build requires JDK22+, but it won't have to. We'll untangle the build and allow using "what you can use" eventually. (Help on this front is more than welcome!)
  • javakit - does not require any "recent" JDK features, as it only uses plain-old-JNI
  • jextract-swift (panama) - will keep requiring JDK22+
  • jextract-swift (jni) - will need to be developed, and would support older JDKs and platforms. (Again, this might be an excellent area for contributions)

Swift interface enchancements

The jextract-swift approach relies on additions to .swiftinterface files that the Swift compiler is emitting.

Today Swift interfaces offer a description of public APIs exposed by a module, such as this:

// swift-interface-format-version: 1.0
// swift-compiler-version: Apple Swift version 6.0 effective-5.10 (swiftlang-6.0.0.7.6 clang-1600.0.24.1)
// swift-module-flags: -target arm64-apple-macosx15.0 -enable-objc-interop -enable-library-evolution -module-name MySwiftLibrary
import Swift
...

public func globalTakeInt(i: Int)

extension MySwiftClass {
      public func helloMemberInExtension()
}

public class MySwiftClass {
  public init(len: Swift.Int, cap: Swift.Int)
}

In order to support jextract, and other language integrations (!), we are going enhance swift interface files with additional information, such as mangled names of methods, types, and potentially more detailed information e.g. @layout(...) of struct/enums etc, such that other languages can be informed about the exact memory layout of how Swift decided to lay out certain types in memory.

For a start, we've started including mangled names when enabled with -abi-comments-in-module-interface, which show up as follows:

// MANGLED NAME: $s14MySwiftLibrary13globalTakeInt1iySi_tF
public func globalTakeInt(i: Int)

// MANGLED NAME: $s14MySwiftLibrary0aB5ClassCMa
public class MySwiftClass {
  // MANGLED NAME: $s14MySwiftLibrary0aB5ClassC3len3capACSi_SitcfC
  public init(len: Swift.Int, cap: Swift.Int)
}

This is a good step, however still not sufficient to form member method calls, as swift's calling conventions don't directly match C calling conventions, e.g. in how self or errors are passed.

Swift Accessor Thunks / Swift Calling Convention

Swift's calling convention differs from raw C-style calling conventions, making it difficult (or even not possible) to be directly invoked from other runtimes unless they also implement the calling convention.

Because implementing the calling conventions inside the JVM may prove difficult, we are instead going to investigate a form of "language interop" mode in which Swift will emit thunks that wrap existing public methods and offer a C calling convention compatible "thunk" that calls directly into the target Swift method.

For example, the self parameter may be passed using registers, and similar optimizations are done for throwing functions. The here proposed thunks, would take care of Swift's calling convention details, and offer an easier (or at least possible) to call using plain parameter passing which should be easier/possible to adopt via the JVM's foreign function interface.

The full extent and details of these thunks are not yet defined, but the general goal here is to make it possible to form calls from the JVM directly to these, without having to worry about when to use what register on the calling side.

This also serves as an opportunity to convert thrown errors in Swift into exceptions thrown in Java.

async is another interesting topic, though we've not explored it in depth yet. I won't go to deep into speculation mode about we could do there, but in theory Java's virtual threads and runtime based on continuations (jvmls2024) may lean towards something we could integrate with by entering async from an virtual thread, converting it to a Task "in-place" and swift runtime yields/resumes mounting/unmounting the carrier thread etc.

Imported types / Usability

As far as the user experience we're interested in offering with jextract-swift, it boils down to the following snippet from one of the sample apps:

// Import swift-extract generated sources
import static com.example.swift.generated.JavaKitExample.*;
import static com.example.swift.generated.MySwiftClass.*;

JavaKitExample.helloWorld();

// ... 
MySwiftClass obj = new MySwiftClass(2222, 7777);

SwiftKit.retain(obj.$memorySegment());
System.out.println("[java] obj ref count = " + SwiftKit.retainCount(obj.$memorySegment()));

obj.voidMethod();
obj.takeIntMethod(42);

Given a Swift module called JavaKitExample:

  • public global functions get imported as static methods on a class with the same name as the module
  • public types in the module get imported as their respective Java counterparts: a Java class wrapper around a memory segment which points directly to the memory location of the Swift allocated object.
    • the wrapper provides methods which "look and feel" like their Swift versions
    • it is possible to get the "pointer" by using .$memorySegment(),
    • and we offer some low level APIs in the SwiftKit library to work with those, for example we can retain/release/retainCount such classes. (Maybe we'll offer safer APIs for this, where we accept a SwiftHeapObject implementing Java object instead)

Details of importing methods is still being developer, however in general you can expect a throwing Swift function to be imported as throwing in Java (checked exception) etc.

Usability: callbacks (swift -> java upcalls)

We are able to pass callbacks across the language barrier as well (source synthesis not implemented yet). The general approach here is that a specific Swift closure signature, e.g. () -> () can be expressed as a java.lang.Runnable, so a method like this:

public func globalCallJavaCallback(callMe: () -> ()) { ... }

is possible to be exposed as the following Java signature:

public static void globalCallJavaCallback(Runnable callMe);

where we create an ad-hoc upcall handler which is able to store the Runnable's this and allow the Swift side to "just call the closure". The Java side then looks like this:

JavaKitExample.globalCallJavaCallback(() -> {
    num += 1;
});

which relies on the usual functional interface mechanisms of Java (a protocol with a single method can be expressed with a lambda matching the method's signature).

Further discussion is needed about handling @escaping closures, because it means we have to keep the Runnable "alive" as the upcall handle escapes.

For those curious how such calls are constructed, here's a snippet to provide a little bit of insight what kind of source generation we're dealing with here in general:

    private static class globalCallJavaCallback {
        public static final FunctionDescriptor DESC = FunctionDescriptor.ofVoid(
                ADDRESS // Runnable / () -> ()
        );

        public static final MemorySegment ADDR = ManualJavaKitExample.findOrThrow("$s14JavaKitExample010globalCallA8Callback6callMeyyyXE_tF");

        public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC);
    }


    public static void globalCallJavaCallback(Runnable callMe) {
        var mh$ = globalCallJavaCallback.HANDLE;

        try {
             // signature of 'void run()'
             FunctionDescriptor callMe_run_desc = FunctionDescriptor.ofVoid(
             );
             MethodHandle callMe_run_handle = MethodHandles.lookup()
                     .findVirtual(Runnable.class,
                             "run",
                             callMe_run_desc.toMethodType());
            callMe_run_handle = callMe_run_handle.bindTo(callMe); // set the first parameter to the Runnable as the "this" of the callback pretty much

            try (Arena arena = Arena.ofConfined()) {
                Linker linker = Linker.nativeLinker(); // the () -> () is not escaping, we can destroy along with the confined arena
                MemorySegment runFunc = linker.upcallStub(callMe_run_handle, callMe_run_desc, arena);
                mh$.invokeExact(runFunc);
            }

        } catch (NoSuchMethodException | IllegalAccessException e) {
            throw new RuntimeException(e);
        } catch (Throwable e) {
            throw new AssertionError(e);
        }
    }

The java.util.function package contains ~40 interfaces which represent various shapes of functions, such as (T) -> Bool (Predicate), (T) -> R (Function), and more. Including specialized versions accepting primitive types.

We should handle all those shapes and provide Java signatures for Swift methods accepting callbacks using the most appropriate signature. This includes picking primitive specialized versions of these interfaces which allows the Java runtime to avoid boxing primitives into wrapper objects (i.e. IntSupplier is more efficient than Supplier<Integer>).

Special types and challenges

While on the topic of special types such as functional interfaces.

We should provide efficient ways to pass arrays of bytes, byte buffers, Strings, and other common types between the language boundaries.

Specifically, a way to pass byte buffers to Swift without. Notably, passing arrays with JNI is rather painful, as the receiving side must receive it as the specialized j...Array type, which is a reference type and must be worked with through explicit JNI APIs:

// JNI, not Panama
JNIEXPORT jdoubleArray JNICALL Java_TestJNIPrimitiveArray_sumAndAverage
          (JNIEnv *env, jobject thisObj, jintArray inJNIArray) {
   // Step 1: Convert the incoming JNI jintarray to C's jint[]
   jint *inCArray = (*env)->GetIntArrayElements(env, inJNIArray, NULL);
...
}

In other words, a plain JNI implementation often is forced into copying data when crossing language boundaries.

With JEP-454, we gain the ability for fine grained control over native memory allocation and can even obtain (Java) ByteBuffer based directly on memory returned from foreign function calls.

We will want to investigate and offer ways to deal with bytes crossing language boundaries in a safe and more efficient way. Byte arrays are a very common type passed around between JVM and native–especially in high-performance network applications–and deserve extra attention, in order to allow improved performance for networking applications which aim to mix Swift and Java in the same application.

Our goal here that using jextract-swift would not only be more convenient but also more performant for many use cases.

Lifetimes

One of the problems bridging Java and Swift will be that lifetimes of objects don't inherently match eachother.

This was less of a problem for Swift and obj-c, because both systems use reference counting; and to some degree it is an issue with Swift/C++ as C++ types have various ways of managing lifetimes, however Swift/C++ interop offers ways to deal with C++ shared (or immortal) references.

Because the JVM is a managed runtime with a GC, there is an inherent mismatch in behavior of how memory is managed.

We need to discuss how the "Java wrapper around pointer to native Swift class" should manage their lifetime.

One idea includes explicit lifetimes by using a form of Arena, where a Swift object created in an Arena is tracked by it, and by the end of its lifetime should be count-down-to-zero and destroyed:

try (var arena = SwiftArena.ofConfined()) {
  var obj = new MySwiftObject(arena)
  hello(obj)
} // count down obj, assert that destroyed
// if obj refcount > 1, it was "leaked" 
// beyond lifetime of the arena which is a programming error

This would offer an explicit way of ensuring objects are disposed and not "leaked" accidentally.

The JVM does offer "finalizers" which are similar to Swift's deinit however they are a very well known source of indeterminism, performance issues and subtle runtime issues that are nigh impossible to work around once finalizers are introduced in a system.

Personally, I would argue we can only use finalizers to "assert the native object was already released" or similar leak detection in debug builds only, but can not rely on them for lifetime cleanup (i.e. just causing the refcount to be decremented). Instead, we have to provide APIs to make the lifetime management bearable, most likely through the SwiftArena concept.

Further performance optimizations

We can do a lot more with the JDK's new foreign function and memory APIs. For example, if we expose detailed object layout information in swift interfaces (e.g. using some @layout() annotation) then jextract-swift could be able to form MemoryLayout descriptions of Swift structs:

// example memory Layout
private static final GroupLayout $LAYOUT = MemoryLayout.structLayout(
    ManualJavaKitExample.SWIFT_INT.withName("len"),
    ManualJavaKitExample.SWIFT_INT.withName("cap")
).withName("$MySwiftType");

which we could use to form direct memory accesses to properties of Swift types, without calling through accessors. We could only do this if such property does not have side effects like willSet etc, however it may be an interesting future performance optimization.

Without these, we're still able to access properties through their accessors, which function the same as normal member methods in Swift.

Packaging and distribution

While we're yet quite far away from this stage, the end goal is to be able to provide SwiftPM and Gradle and/or Maven plugins that would automate building, extracting and packaging up Swift native code along side Java artifacts.

This may include the ability to include compiled Swift libraries inside a .jar and shipping it as such native code dependent jar, versioned for specific platforms etc.

For other libraries, e.g. for example for calling into frameworks provided by the SDK on macOS, we would only package up the source generated Java accessors.

The exact details of how far and what use-cases to prioritize here are up for discussion and we'd love to hear from you, and would encourage contributions in this space once we've figured out what we want to build here!

What about GraalVM?

We are currently not investigating native-image or GraalVM solutions.

However in they are an interesting thing to keep an eye on. If someone in the community has more experience with those, input and ideas would be most welcome.

As far as we know GraalVM currently does not support upcalls, and may have other limitations which could make the integration difficult.

Similarly, we are not currently exploring the direction of running Swift -> LLVM bitcode -> GraalVM LLVM Runtime. It may be an interesting avenue to explore, but currently we are going to focus on "plain old" Java interoperability. Again, if you have the expertise and/or time to investigate this avenue, you're more than welcome to.

Get involved!

This has been a bit of a dump of status and plans, but I hope it's useful to set the stage where we're at, and how to get involved.

Please watch the swift-java github repository and check out the "help wanted" and "good first issue".

Please feel free to discuss in this thread and/or create new threads. We're quite early in the development and input/ideas/help are very welcome.

And a special shout out to folks I had a chance to talk with during serversideswift conference last week as well, and I look forward to hearing from you all soon! As soon as video from the talk announcing java-swift is available we'll share it here as well.

~~ mini dictionary ~~

  • downcall -- calling "down" from the perspective of the JVM (to native Swift code)
  • upcall -- calling "up" into the JVM
  • panama -- umbrella name for the JDK's foreign function and memory APIs OpenJDK: Panama
  • jextract -- the JDK's "extract Java accessors to C interfaces from C headers" tool: GitHub - openjdk/jextract: https://openjdk.org/projects/code-tools, our jextract tool is a completely separate binary, unrelated to jextract (may need to consider renaming?)
  • jextract-swift -- the tool provided as part of swift-java, able to extract Java accessors to Swift code, based on imported swift interface files.
18 Likes

Thx for wrap-up! Looks awesome and great initiative from the team!

Answered some questions for myself, like memory management. Though wondering now—how things like Swift enums (in ADT way) would look like in Java? Will it be regular classes or things like records and sealed classes? But then have a follow up question—JDK22+ support is reasonable due to Panama project, but to what extend support older versions? E.g. virtual threads were just announced recently, and if team wants to build async on top of them—guess one should keep in mind "ok, this is older version of Java and I can't use async"?

:eyes:

1 Like

Tbh this just means for example being able to improve the C++ interop and maybe potentially moving some of it outside of the compiler etc. Today it's very very "inside the compiler" and not all of it has to be, but it'd likely need very similar information as our work over here.

Thus, personally, I like to call the Swift features we're developing here general "language interop features", there's nothing really too special about Java here.

Having that said, some things will not be expressible in the Java type system -- like the enums you mentioned, but may be expressible in other JVM languages. That's not a goal right now, but I know some folks on the serversideconference have signalled they'd be interested in helping out doing source gen into other target languages. We'll see what the community comes up with.

Swift enums should be able to import as a hierarchy of sealed interface + classes, that's right. And they're JDK17, which means we'd likely use them even in jextract-swift-jni (if and when it becomes a thing).

As far as records are concerned -- I'm not sure but probably not for jextract.

Records must have their complete state be represented in their initializer and this is the storage of the class. Types imported via jextract do not have storage in the Java object but have all the "fields" in the native allocated Swift object. And the Java object is just a wrapper around a memory pointer to that object. We'd have to have the memory segment in the constructor and nothing else, and this kind of makes it a bit weird, unfit for the "hide away the pointer unless you need it".

So records probably don't matter much here; But then again, records help to reduce boilerplate on the Java side -- which isn't something we're concerned about for end-users, since this is a source generator workflow anyway, we can generate the "lots of boilerplate" the classes necessitate.

Good callouts, thanks! Especially the enums I'll note in an issue on github :slight_smile:

2 Likes

I just merged a pull request that takes the first step here. Given Swift type metadata pointer (i.e., an Any.Type in the Swift world, represented as a MemorySegment on the Java side), we can now build a MemoryLayout on the Java side that captures the layout of an arbitrary Swift type. This provides the appropriate size/alignment/internal padding, and even works for types laid out at runtime in Swift. With a bit more work, we could find the types, offsets, and names of all of the fields to allow direct access.

Doug

9 Likes

Hi everyone,
this is a small update on the design of the tooling around exposing Swift libraries to java.

We previously relied on mangled names that we experimentally were emitting in swift interface files. This has hit a number of limitations (primarily because we cannot express Swift's calling convention in Java's new Foreign Function APIs), and instead we pursued a direction where we don't need these.

The new design: Gradle and SwiftPM collaboration

We've added a new example here: swift-java/Samples/SwiftKitSampleApp at main · swiftlang/swift-java · GitHub which showcases how this will work eventually, although there is still too much boilerplate which we'll hide in plugins eventually.

The new design approach from an user's perspective changes to rely more on a closer gradle (we can support other build tools eventually, for now we picked gradle because it seems most folks are using that), and swiftpm collaboration.

In short, a mixed language project that wants to expose Swift code to Java easily, would do the following:

  • have a Package.swift for the Swift project

And ensure the products we want to use from java are dynamic libraries:

  products: [
    .library(
      name: "MySwiftLibrary",
      type: .dynamic,
      targets: ["MySwiftLibrary"]
    ),

Targets that we want to expose to Java, would depend on the SwiftJava SwiftPM plugin. I believe we'll offer both a command and pre-build plugin here. (The example in the repository currently drives the sample using a command plugin.):


    .target(
      name: "MySwiftLibrary",
      dependencies: [
        .product(name: "SwiftKitSwift", package: "swift-java"),
      ],
      exclude: [
        "swift-java.config",
      ],
      swiftSettings: ...
      plugins: [
        .plugin(name: "JExtractSwiftPlugin", package: "swift-java"),
      ]

TODO: We're going to converge plugins and call them SwiftJavaPlugin most likely, but that's still work in progress.

This plugin is responsible for generating Swift and Java sources necessary for the bridging.

On the Java project side, the Gradle build is basically informed how to generate sources, and its compileJava depends on the new compileSwift task.

In essence, this means for multi language projects using Java, the way to develop would be "call the gradle build" as it would also invoke the Swift build when necessary.

Then, we just ./gradlew run and all those steps are triggered in order:

  • we build the swift plugins
  • we build the targets which have swift-java.config files
  • we invoke the swift package jextract <targets> --options with a number of options that the gradle build determined
  • this generates both swift thunks with "well known names" that we'll invoke from Java using FFM, as well as Java "wrappers" that target those thunks.
  • optionally: we rebuild the generated swift sources to make sure things are correct and build fine
  • now all dependency tasks of the compileJava task have completed and the normal Gradle "java" build builds the java sources
  • the run task also has automatically configured java.library.path dependent on the targets that we depend on.
  • the "java app" with swift dynamic library dependencies just works™

Such app's main looks like this:

package com.example.swift;

// Import swift-extract generated sources
import com.example.swift.MySwiftLibrary;
import com.example.swift.MySwiftClass;

// Import support libraries
import org.swift.swiftkit.*;

import java.util.Arrays;

public class HelloJava2Swift {

    public static void main(String[] args) {
        boolean traceDowncalls = Boolean.getBoolean("jextract.trace.downcalls");
        System.out.println("Property: jextract.trace.downcalls = " + traceDowncalls);

        System.out.print("Property: java.library.path = " +SwiftKit.getJavaLibraryPath());

        examples();
    }

    static void examples() {
        MySwiftLibrary.helloWorld();

        MySwiftLibrary.globalTakeInt(1337);

        // Example of using an arena; MyClass.deinit is run at end of scope
        try (var arena = SwiftArena.ofConfined()) {
            MySwiftClass obj = new MySwiftClass(arena, 2222, 7777);

            // just checking retains/releases work
            SwiftKit.retain(obj.$memorySegment());
            SwiftKit.release(obj.$memorySegment());

            obj.voidMethod();
            obj.takeIntMethod(42);
        }

        System.out.println("DONE.");
    }
}

You'll notice that the generated sources ended up in com.example.swift:

  • there is one type for the static and global Swift values and functions, with the same module as the target that we built
  • and wrapper types for every public type

Those wrapper types then expose "looks like normal methods" methods on those wrappers, and perform native calls.

Solution to Object Lifetimes: Swift Arenas

You'll also notice the previous snippet contained a SwiftArena type.

This actually is not implementing the JDK's Arena type, but it serves a very similar purpose: management of natively allocated memory / objects.

We leave the allocation of objects up to Swift so far, so for a class the allocating initializer is invoked.

The arena is a concept that manages the lifetime of the object, and this example has a Confined arena:

try (var arena = SwiftArena.ofConfined()) {
    MySwiftClass obj = new MySwiftClass(arena, 2222, 7777);
}

what this does, is that at the end of this try-with-resources scope the Java side asserts that "i should be able to destroy this object now", checks the reference count is just 1 and invokes the right destroy through the object's witness table. This is native integration into Swift by modeling and calling through Swift's witness tables (!).

This is a very good arena type for short lived objects when we know we will be destroying them at the end of a scope. It has predictable performance and it avoids leaking objects by accident.

We are also offering an AutoArena type (names here follow established names from the JDK), which relies on the GC to track these objects and can be used like this:

// java
final var arena = SwiftArena.ofConfined();
MySwiftClass obj = new MySwiftClass(arena, 2222, 7777);
obj = null; // obj no longer referenced!
// when it is GC-ed, we'll swift_release the underlying object

Because the wrapper MySwiftClass around the native instance of this class is no longer referenced in the Java runtime, as soon as the GC gets to checking it, we will use phantom references and queues to decrement the reference count of this object.

Note: This technique has performance implications on the JDK, as phantom references are costly to maintain for the GC, but it does offer a simple way of using Swift objects, especially long lived ones.

It also is not predictable anymore when we'll release the resource. So in general confined arenas are more predictable and should be preferred when possible.

Next steps for jextract

We'd like to converge the various plugins swift-java currently has accumulated. We currently have Java2Swift and jextract-swift which work in opposite "directions". Instead, we should offer a swift-java tool and plugins which just do the right thing depending on what sources and configuration they are used on.

We need to further develop the jextract mechanism to handle: enums, properties, throwing functions, and more.

We will provide a way to make a jar that includes the .dylib/.so file as well as the generated java sources. This way you will be able to publish your my-swift-library.jar and make it very easy to consume for java only developers which don't even have to know you wrote some of the library in swift.

If any of this sounds exciting help is most welcome. There's also much work on the JNI (JavaKit) side of the library, so don't be shy to join in. :slight_smile:

Future Gradle plugin work

We will eventually need to write a gradle plugin that encapsulates a lot of the logic that today is just done in the one example file: swift-java/Samples/SwiftKitSampleApp/build.gradle at main · swiftlang/swift-java · GitHub

The good news is that this is very possible, and eventually we'll just be able to add a plugin and get this "just works" workflow going.

If someone is more familiar with Gradle, help on this front would be more than welcome.


Hope you've enjoyed this update and we'll keep in touch about ongoing developments :slight_smile:

7 Likes

One consequence of this is that we no longer need to use development toolchains when exporting Swift APIs to Java, so makes swift-java easier to work with. Swift 6.0 has everything we need.

I feel like the "how we compile it" is a separate set of decisions from the central change here of saying that we'll general both Swift and Java sources as part of the binding.

This is interesting. We're building the Swift thunks into the library target itself. There's a use case here I'd love to ponder, and I don't know if it's possible, but... I could very much see the desire to take an existing package and say "hey, I'm going to create a dynamic library target whose only purpose is to expose the public API of one of that package's targets to Java." I don't recall if SwiftPM build plugins get to see the Swift source files from their dependencies (!!), but that would make this possible.

This same approach could be re-used if we wanted the ability to expose the same Swift APIs via JNI, for example if we also want to support a pre-FFM/Panama world or (say) allow exporting Swift APIs that were themselves built on top of Java APIs imported from Java (whew!). So many possibilities!

Sounds magical, I love it.

This is amazing, and generalizes to Swift value types (structs and enums) as well.

Yeah, I think this is important. We can drive all of the functionality for a target through a single swift-java.config file, and a single plugin can orchestrate the various build steps (compiling any Java code or dependencies fed into the target, generating Swift code for imported Java classes, generating the Swift side of the wrappers, generating the Java classes that call into the Swift side of the wrappers, compiling those Java classes) and deciding which of those are for SwiftPM to do vs. Gradle to do.

Doug

3 Likes

Meta-post, but the referenced examples in OP were removed.

Removal commit:

Yeah, I think there has to be a way to do this because it'll give us "expose Java wrapper to a system library (that is in Swift)", like CryptoKit for example.

So I think our plugin may become able to be configured as "this target has no sources, but it's used for exporting to java the <e.g. some other library, maybe system, maybe other package's target>".

It seems (at least from APIs, didn't run checks) that we do indeed get source modules of our dependencies, so that seems possible. Alternatively feeding it a list of swift interfaces may be a way? I'm thinking how we'll do the "export CryptoKit for me", perhaps that would be in a command plugin which just gets fed the swiftinterface.

Yeah that's good to highlight; people aiming to use JNI only should not get too hung up on the fact that it's FFM today -- we should invest into the +Printing files being able to render JNI versions of the imported APIs basically.


I think we're going to have a good time converging the plugins and configurations -- we'll be able to make this very nice to use I think.

On that note, JSON is quite annoying... I was wondering if we should do YAML or something else (i love the recently released pkl but dont want to pull too many dependencies). Something that at least survives comments in the configuration -- json is a pain for that hm.

What am I missing in SwiftHeapObjectCleanup where it asserts the retain count is 1? What if a long-running Swift object retains a reference, shouldn't the Runnable decrement the reference count instead of asserting?

1 Like

This is incomplete, in the confined arena case we want to assert but in the auto arena we want to just decrement.

We’ll do a flag to determine what the cleanup should do.

You’re right it’s not doing that yet, sorry for any confusion caused :-) it’s a simple change though.

And then we’ll do a different cleanup type for value types :-)

1 Like

Having a Gradle plugin that can invoke SPM will also be super useful for Android. Right now it’s a messy shell script sort of a situation.

Having that said it's just basic swift build invocations, but at least we "find out" all the right output paths of libraries etc. SwiftPM doesn't really offer a public API so we're using the plain old over command line invocations. I'm sure it'll be useful anyway though.

1 Like