GraalVM Native Image for Mobile Development | Philip K. by Han | October, 2022

Discussion of the future of native code

Unsplash. photo by Francesco on

GraalVM Native Image generates native machine code from Java byte code. It is primarily intended for applications or tasks that require fast startup times, i.e. microservices. It uses AOT (advance time) compilation.

In this article, we will briefly explore the current state of mobile development on GraalVM.

And in a follow-up article, we’ll build a shared core component that can run Android and iOS apps. The demo app will contain all the ideas introduced in this article and consider them in more detail.

We’ll see if the benefits of shared logic between Android and iOS outweigh the overhead of the interop layer connecting the two worlds.

From above, I will cite the biggest barrier to entry in this endeavor: c.

Yes that is correct. C.

In completing this project, I’ve learned that most, if not all, interop features on languages ​​are at the C layer. If you think about it more it makes sense. What else can be used?

The GraalVM C API produces a machine-executable or library with C function entry points. Although it can be written in Java or Kotlin, it worked well only in Java but not with Kotlin for me. You can write your app code in Kotlin, but C interop code has to be written in Java. Maybe, it is possible in Kotlin, but I haven’t managed it yet.

Finally, the consumer, Swift, has to use C code in this case. Fortunately, Swift has a very nice C interop layer. No Objective-C was required in this project. Of course, Objective-C can also be used if desired.

In short, here’s how it goes:

JVM Components <-> GraalVM C API <-> Swift C API <-> Swift

JVM components can be written in any JVM language, i.e. Java, Kotlin, Clojure, etc.

Lastly, you should take care of heap allocation and deallocation in certain situations.

The architecture should be platform agnostic to maximize code sharing between platforms. The viewmodel on the host platform should wrap around your multi-platform component and platform-specific changes. We’ll call this component a core component for the rest of this article.

The core component contains all the app code that is similar between Android and iOS. This can vary between cases but is most likely domain, view model (in a platform agnostic way), data access and networking, etc.

In addition, it requires abstraction to access the host platform’s infrastructure services, such as Bluetooth, speech recognition, or user preferences. This is accomplished by exposing the host platform’s services as C function pointers and C data structures.

Ideally, this component should be platform agnostic. This will promote good architecture and provide the added benefit of targeting other environments, e.g., desktop (Windows, MacOS, Linux) and possibly WebAssembly.

This component can be written in Kotlin or Java as can be used in Android, and can also be compiled into an iOS object file for Xcode. Note that the platform infrastructure implementation must be provided by the host and wired up at run time.

If you are familiar with Java’s threading and memory model then you are pretty much ready. No other paradigm you will need to learn. Java Threads, ExecutorService, and Kotlin coroutines work fine. No special libraries are required.

Also, when Project Loom will receive benefits immediately or soon after it ships. Or as Kotlin coroutines include Loom if you are using coroutines.

At the C interop layer, however, something called isolate is required. Isolate is a small Java runtime environment for JVM code to be executed as machine code and reside on the thread where it was created. It would need to be initialized and passed in by all exported functions by the callers.

Isolates can cause problems with something like coroutines because when the continuation is resumed, they may not be on the same dispatcher thread. When this happens, the isolate will disappear or not be correct.

In theory, all Java libraries written to date are compatible. No modification is required, but you will need to trace reflective operations and provide a configuration file to maintain the necessary classes for your final object or executable.

Fortunately, GraalVM AOT provides a mechanism to solve this problem.

The first is the agent. Instrumentation monitors the execution of your code, records all echo calls, and generates a configuration file for you. You must execute your code and ensure that all relevant code is reached during this instrumented run.

The second is features. It allows you to insert operations during the AOT compilation phases. You can include, among other things, classes that are otherwise excluded by the compiler here. So, it is more of a manual operation than an agent. But with these two mechanisms, most libraries can be adapted to your project.

It’s also worth mentioning that Oracle recently built a repository of reflection configurations of libraries. And hopefully, the library’s authors will start incorporating it into their distribution, as a select few have already.

Here are the libraries I’ve tested so far and have issues with reference to mobile in parentheses:

  • Java 11 HttpClient (Android hasn’t included it yet, works in iOS)
  • Moshi (works fine including reflective binding, file tweak needed to be made)
  • Kotlin (built-in requires a configuration file)
  • Ktor (no problem)
  • Kotlin Serialization (works fine including reflective binding)
  • Coin (no problem)
  • Kotlin coroutines/flow/channels (no problem, main dispatcher is lacking)
  • SQLDelight (works with H2, not SQLite)
  • JDBC – SQLite (Lacks native working iOS, but works on macOS target)
  • JDBC – H2 / Hikari (no problem)
  • OkHttp (exit has a 60 second delay, ie isolate tear down)
  • Retrofit (Problem with OkHttp, may work with other HTTP clients?)

Object files must be relocatable, meaning all functions have zero load addresses, then final linking and assembly is done in Xcode to produce executable code. GraalVM provides AOT compiler H:+ExitAfterRelocatableImageWrite option for this purpose. You will need to optimize your build environment and a few other feature flags to automate this process.

The Java static libraries for iOS-Aarch64 and iOS-x86_64, which are used in linking against the final framework, are not readily available. It must be downloaded from a private company that makes it available for its multi-platform offering based on JavaFX.

The only supported IDE at this time is VS Code. Oracle provides the GraalVM Extension Pack plugin to support development. This includes it’s own Java language server (Apache Netbeans Language Server), so you will need to disable any other Java language servers that you are using.

The Apache NetBeans language server does not provide symbol resolution for Kotlin. This breaks auto imports and code completion when writing Java interop code that references any Kotlin components. It’s annoying. The refactoring is partially broken because of a symbol resolution issue.

The plugin also manages GraalVM installation and plugins within GraalVM, e.g., native images, the LLVM toolchain, Python, and other supported languages.

It comes with a debugger in native mode, but I haven’t used it yet.

Lastly, it provides integration with Maven and Gradle build environments.

The GraalVM core image is quite uncertain at the moment in terms of mobile development.

Oracle doesn’t have much interest in this area yet. It is heavily focused on Enterprise and Microservices. And the mobile community around it is almost nil, although there is an enterprise community, for example, Micronaut and Quarkus.

Java itself is evolving. More specifically, Project Panama, which brings foreign function and memory APIs, will overlap the current C API and may replace the current implementation. Oracle recently said that Project Panama will allow access to foreign memory & functions other than c. But it will not be distributed in Java 19 as a Foreign Function and Memory API Preview. And whether Swift will be included as a target language is completely unknown at this point, and very unlikely, in my opinion.

Then there’s Project Valhalla and Loom, which will bring value types and virtual threads to Java.

In short, the interop code you’re writing today may be obsolete tomorrow.

Kotlin Multiplatform (KMM) has a lot more to it than GraalVM Mobile.

First, KMM is backed by a solid company, JetBrains. Oracle isn’t showing any enthusiasm for the mobile sector. Their official offering is still, believe it or not, Java ME.

There is a healthy and enthusiastic community around KMM. Since JetBrains has lifted the ban on the KMM memory manager, library support is likely to increase.

KMM has a direct Objective-C interface, which means you don’t have to deal with the C interface, although that option is available. Swift interface is being worked on.

In my experience the reflection support on KMM is not as good as that on GraalVM.

The same questions remain about the development of Java/JVM for KMM. How will it adopt virtual threads, value types and FFI?

The advantage of having a single codebase for the core component is undeniable. At the same time, the burden of C interfaces on both sides is real.

Previous attempts at C/C++-based cores for Android/iOS have failed because the overhead was becoming more complex and costly as time went on.

To be fair, GraalVM is far from sharing native image C++ code. Nevertheless, some of the conclusions from the article apply here as well, IMHO.

I’ve had a lot of fun researching and implementing a demo app. I wanted to do this project purely out of curiosity. And I knew I would learn a lot along the way.

With the upcoming changes to KMM and Java, the future looks increasingly original. Long-running processes will still be dominated by the JIT, at least partially, but there’s certainly a place for native ones.

I encourage any future Java/Kotlin developers to look more carefully at native code, i.e. C, C++, and LLVM, perhaps.

LLVM is at the heart of enabling KMM, and GraalVM AOT also has an LLVM backend as an option.

Leave a Comment