Better Programming

Advice for programmers.

Follow publication

The State of GraalVM Native Image for Mobile Development

Philip K. Han
Better Programming
Published in
7 min readOct 31, 2022
Photo by Francesco on Unsplash

GraalVM Native Image produces native machine code from Java byte code. It is mainly intended for applications or functions that require fast startup time, i.e., microservices. It uses AOT (ahead-of-time) compilation.

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

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

We’ll see whether the benefit of shared logic between Android and iOS is greater than the overhead of the interop layer bridging the two worlds.

Inter Op

Right off the top, I’ll cite the biggest barrier to entry in this endeavor: C.

Yes, that’s right. C.

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

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

Lastly, the consumer, Swift, in this case, has to use the C code. Luckily, Swift has a pretty good C interop layer. No Objective-C was needed in this project. Of course, Objective-C can be used as well if desired.

To summarize, here is how it goes:

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

The JVM component could be written in any JVM language, i.e., Java, Kotlin, Clojure, etc.

Lastly, you must take care of heap allocation and deallocations in some situations.

Architecture

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

Core Component

The core component consists of all app code common between Android and iOS. It may vary between cases but most likely contain the domain, view models (in a platform agnostic way), data access and networking, etc.

In addition, it needs abstractions for accessing the host platform’s infrastructural 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. It will promote good architecture and offers the added benefit of targeting other environments, e.g., Desktops (Windows, macOS, Linux) and possibly WebAssembly.

This component could be written in Kotlin or Java to be used in Android as-is, and it could also be compiled into iOS object file for Xcode. Note that Platform Infrastructure implementations need to be provided by the host and wired at run time.

Threading

You’re pretty much set if you’re familiar with Java’s threading and memory model. No other paradigms you’ll need to learn. Java Threads, ExecutorService, and Kotlin coroutines work just fine. No special libraries are required.

Furthermore, it will gain benefits immediately or soon when Project Loom ships after. Or as soon as Kotlin coroutines incorporate Loom if you’re using coroutines.

On C interop layer, however, requires something called Isolate. Isolate is a miniaturized Java runtime environment for JVM code executing as machine code and resides on the thread where it was created. It will need to be initialized and passed to all exported functions by the callers.

Isolates can cause problems with something like coroutines because when Continuations resume, they may not be on the same dispatcher thread. When that happens, the isolate will be missing or not be the correct one.

Third-Party Libraries

In theory, all Java libraries written to date are compatible. No modifications are required, but you’ll have to figure out reflective operations and provide a configuration file to retain needed classes for your final object or executable.

Luckily, GraalVM AOT provides mechanisms to address this problem.

First is Agent. The instrumentation monitors the execution of your code, records all reflective calls, and generates a configuration file for you. You’ll need to execute your code and ensure that all relevant code is reached during this instrumented run.

Second is Features. This allows you to insert operations during the AOT compilation phases. You may include classes otherwise excluded by the compiler here, among other things. So, this is more of a manual operation than an Agent. But with these two mechanisms, most libraries could be compatible with your project.

Also worth mentioning is that Oracle recently created a repository of reflection configurations of libraries driven by users. And hopefully, library authors will begin to include it in their distributions, as a select few have already.

Here are the libraries that I’ve tested so far and issues in the context of mobile in parentheses:

  • Java 11 HttpClient (Android does not include it yet, works in iOS)
  • Moshi (works fine, including reflective bindings, needs build file tweak)
  • Kotlin (requires configuration file for built-ins)
  • Ktor (no issues)
  • Kotlin Serialization (works fine, including reflective bindings)
  • Koin (no issues)
  • Kotlin coroutines/Flow/Channel (no issues, lacks the main dispatcher)
  • SQLDelight (works with H2, not SQLite)
  • JDBC — SQLite (lacks iOS native lib, but works on macOS target)
  • JDBC — H2/Hikari (no issues)
  • OkHttp (exit is delayed by 60 secs, i.e., Isolate tear down)
  • Retrofit (issue with OkHttp, may work with other HTTP clients?)

Build Issues

Object files must be relocatable, which means all the functions have null load addresses, then final linking and assembly are done in Xcode to produce executable code. GraalVM AOT compiler provides H:+ExitAfterRelocatableImageWrite option for this purpose. You’ll need to customize your build environment and some other feature flags to automate this process.

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

Development Environment

The only supported IDE at the moment is VS Code. Oracle provides GraalVM Extension Pack plugin to support development. It includes its own Java language server (Apache NetBeans Language Server), so you’ll need to disable any other Java language server you may be using.

The Apache NetBeans Language Server does not provide symbol resolution for Kotlin. It makes auto import and code completion break when writing Java interop code that references any Kotlin components. It is annoying. Refactoring is partially broken because of the symbol resolution issue.

The plugin also manages GraalVM installations and plugins within GraalVM, e.g., Native Image, LLVM Toolchain, Python, and other supported languages.

It comes with a debugger in native mode, but I’ve not used it at all so far.

Finally, it offers integration with Maven and Gradle build environments.

Future

GraalVM Native Image in the context of mobile development is quite uncertain at this point.

Oracle does not yet have much interest in this area. It is very much focused on Enterprise and Microservices. And the mobile community around it is almost nil, although there is an enterprise community, e.g., Micronaut and Quarkus.

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

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

In short, the interop code you write today could be obsolete tomorrow.

Comparison to Kotlin Multiplatform

Kotlin Multiplatform (KMM) has much more going for it than GraalVM mobile.

First, KMM has the backing of a solid company, JetBrains. Oracle is not displaying any enthusiasm for the mobile arena. Their official offering is still, believe it or not, Java ME.

KMM has a healthy and enthusiastic community around it. Since JetBrains has lifted restrictions on the KMM memory manager, library support will likely grow.

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

Reflection support on KMM is not as good as GraalVM, in my experience.

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

Conclusion

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

Past attempts at C/C++-based core for Android/iOS have failed because the overhead was getting more complex and expensive as time went on.

To be fair, GraalVM Native Image is far from sharing C++ code. Still, some conclusions from the article apply here as well, IMHO.

Afterthoughts

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

Along with KMM and coming changes to Java, the future looks increasingly native. At least partially, long-lived processes will still be dominated by JIT, but there’s definitely a niche for native.

I’d encourage any Java/Kotlin developers with an eye toward the future to look perhaps more carefully into native code, i.e., C, C++, and LLVM.

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

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

Responses (1)

Write a response