Android, Java, and SwiftJNI

The Android SDK is implemented in Java, and so to meaningfully interact with the platform from Swift, some mechanism for communicating with the Java world is required. The standard for this communication is the Java Native Interface (JNI), which has two halves:

  1. Invocation API: how C/C++/Swift code loads and invokes Java code
  2. Native Method Interface: how Java loads and invokes native C/C++/Swift code

Over the years, dozens of Swift-Java bridging frameworks have sprouted up, such as:

This last project is part of the Java Interoperability effort and has the potential to become the standard way for Swift to interoperate with Java in desktop and server environments, but because it requires JDK 22+ for its Foreign Function & Memory API, it isn't suitable for Android, which is stuck on an ancient version of Java 8 and will likely never adopt the newer FFI standard (see below).

In addition, different bridging frameworks will satisfy different needs: swiftlang/swift-java requires running an external tool to create bridging code between Swift and Java, whereas Skip's bridging automatically generates a bridge between Swift and Kotlin with its build plugin, and handles bridging Kotlin-exclusive concepts like tuples, lambdas, and coroutines (async), as well integration with the Jetpack Compose runtime.

That all mostly affects the bridging generation and the Native Method Interface half of the equation, but the Invocation API is another story: the standard jni.h is ubiquitous and largely unchanged since Java 2, and everyone has their own bespoke Swift wrapper around the various C functions (FindClass, CallObjectMethod, NewString, NewGlobalRef/DeleteGlobalRef, etc.), all of which do mostly the same stuff. For example, contrast skiptools/swift-jni/Sources/SwiftJNI/SwiftJNI.swift and readdle/java_swift/Sources/JNICore.swift and swift-java/Sources/JavaKit/JavaObject+MethodCalls.swift and it's mostly the same kind of jockeying of wrappers, references, and adding Swift niceties atop the invocation API.

This parochial approach to JNI plumbing reduces the amount of sharing that can be done between libraries that are built on top of them. For example, if someone builds some neat library on top of the Skip JNI implementation (like, say, a mechanism to globally access an Android Context instance like in swift-android-native), it cannot be used by swiftlang/swift-java or readdle/swift-java because of different underlying conventions and types.

So I would propose the addition of a new swiftlang/swift-jni project that satisfies the needs of all these projects. It would be a simple and un-opinionated wrapper around the standard JNI C interface that could work equally well in all environments (server, desktop, mobile, embedded, etc.) and enable the sharing of projects that are built on top of them. It would ideally be something that all the above mentioned projects could migrate to using without too much refactoring. In addition, it could be included by default with the Android SDK so developers could just import(SwiftJNI) without needing to add any additional dependencies.

I can sketch up a more detailed proposed if there is sufficient interest and agreement that this would be a useful project.

8 Likes

I’m traveling right now so just a short response: it is fully expected for swift-java to also offer the JNI path rather than FFM.

It is not true that swift-java requires Panama (JDK22+) because it also includes macro based JNI approach as well (JavaKit) which is all just plain old JNI. Only the jextract tool currently requires JDK22+, JavaKit macros don’t use these APIs. It was the plan to also offer jextract-jni but we’ve just not gotten to it yet, swift-java is a pretty young project.

The tool and library is just in its very early stages and both build plugins, and JNI aspects are things that are yet to be built.

So the “swift jni” you are proposing is “just” work that needs to be done in swift-java, there’s no new projects to spin up here or change of directions really. But maybe we should chat about the details here, I’m not opposed to more packages but what you’re pitching sounds like part of the swift-java work, wherever it’ll end up in.

It would be great to get together interested folks and get a working group together working on those missing pieces. I think we could do a kind of community call to try to organize the work and help out getting to a good and completed user experience quicker this way :slight_smile:

Sounds like you’d be interested in helping develop these bits, help is certainly welcome, and it’s a great time to start contributing since there’s so many missing pieces!

If you’d like to catch up how to setup a good collaboration cycle here I could make time to catch up about it sometime soon, wdyt? I was also thinking of setting up a community open call like embedded swift did recently, but maybe in a few weeks when wwdc preparations calm down a bit.

7 Likes

I knew that the @_cdecl approach had been discussed, but I didn't realize that it had been implemented. I stand corrected.

But regardless of swiftlang/swift-java's potential for pre-JDK22 support, different technologies may still need different bridging mechanisms to handle different needs, such as Skip's Kotlin-specific bridging that is required for Jetpack Compose integration. I believe there is value in creating a simple and un-opinionated shared module – either as a separate package product in swiftlang/swift-java, or as a separate repository in swiftlang/swift-jni. The latter has the benefit of potentially reaching a 1.0 release pretty quickly, since – as I say – everyone pretty much does the same stuff when wrapping JNI in Swift types. In this scenario, the JavaKit target in swiftlang/swift-java would be refactored to rely on swiftlang/swift-jni for the lower-level JNI interactions, while still providing the same macro-based bridging conveniences that are used by the rest of the targets in that package.

I think that would be great! If there were at least one representative from the three separate Java-encumbered cohorts (Android, server, and embedded), we could probably all hammer out some minimal set of requirements for a shared module we could all build on top of.

2 Likes

Yeah I’m not opposed to doing some shared packages like that if it would help, let’s hash out what the “shared baseline” would be and then see about the “where” of it. We could definitely use more folks collaborating on the Java things so overall this seems like a good way to get us going on that :)

Let me figure out how to organize that sync in the coming week or two, meanwhile we can chat some ideas here already. I’m back from holiday on Monday and will try to drop some more notes too

5 Likes

swiftlang/swift-java is one approach as mentioned, and this PR contains changes for Android. I do need to update it to link rather than load libnativehelper.so. That's on my list, just snowed under. :frowning:

Edit: not saying that @marcprux's points aren't valid, just that swift-java works well for our needs and we've discussed how to resolve some of the issues (e.g. managing Swift object lifetimes without Panama) in-package.

1 Like