Recently, I successfully migrated my Scala project to GraalVM’s native build. GraalVM is undeservedly unpopular in the Scala community and it is rare to see it mentioned anywhere. This article explains how to properly configure GraalVM and why you might want to try Scala on GraalVM.

What is GraalVM?

In short, GraalVM makes it easy to build a native binary file instead of a .jar. This results in faster startup and overall performance, and a slimmer and more secure application that does not require a JRE to run. For more information, visit their official website.

There are also some limitations, but they usually can be bypassed with just some additional configuration.

And finally, all of the above also applies to applications written in Scala, which is why we’re here.

How to configure GraalVM with Scala

Configuring a build that produces native binaries is relatively easy. If your project is simple enough, everything should work out-of-the-box. For more complex projects, additional configuration may be necessary.

So, let’s start from scratch. We create empty project with:

$ sbt new scala/scala-seed.g8

Next, we add plugin which provides GraalVM support:

// project/plugins.sbt
addSbtPlugin("org.scalameta" % "sbt-native-image" % "0.3.4")

Next, set up the final configuration. Actually, you can just enable NativeImagePlugin without any additional settings, but I recommend to add some which I listed below. You also can read more about parameters here.

// build.sbt
// ...
lazy val nativeBuildSettings = Seq(
  nativeImageOptions ++= Seq(
    // "fallback" means producing a file which requires JVM to run
    // GraalVM switches to a fallback if it's unable to generate JVM-free native image
    // This option disables such behavior and fails a build instead
    "--no-fallback",
    // Using static linking instead of dynamic one
    // Allows you to run your binary in even scratch containers
    "--static",
    // Makes image building output more verbose
    "--verbose",
    // Provides more detail if something goes wrong
    "-H:+ReportExceptionStackTraces"
  )
)

lazy val root = (project in file("."))
  .enablePlugins(NativeImagePlugin)
  .settings(
    // ...
  )
  .settings(nativeBuildSettings)

By the way, you don’t need to install GraalVM itself. The plugin will install it for you. However, if you want to use a pre-installed GraalVM, you can specify the environment value GRAALVM_INSTALLED as true.

Finally, running the build:

$ sbt nativeImage
...
[info] Native image ready!
[info] /home/seroperson/graalvm-hello/target/native-image/graalvm-hello
[success] Total time: 44 s, completed Aug 30, 2023 8:43:49 PM

Here is your native binary. You can run it and see the result:

$ ./target/native-image/graalvm-hello
hello

It is ready to be packed and shipped to some production.

What’s wrong with it

*TL;DR**: If your project is more than a basic “hello-world”, be prepared to spend some time fixing GraalVM-related problems.*

Well, it runs well until we have just a little hello-world project. However, when a project becomes larger, some problems may appear. Usually they are solvable, but still. So, here are trade-offs which I noticed:

  • Firstly, it is worth mentioning that GraalVM is not as popular in the Scala community as it is in the Java community. In the Java-world GraalVM support is implemented by leading web frameworks, such as Spring and Quarkus. Conversely, looks like in the Scala-world there are not so many people who at least ever heard about it. I haven’t come across any Scala framework with mentioned GraalVM support or something like that.
  • Build time increases significantly. Even roughly comparing plain-jar and native binary building, I got 3 sec vs 42 sec on empty project. My a slightly larger project, with around 5,000 lines of code, takes 15 minutes to build using GraalVM. That’s why you will probably need to configure plain-jar build for debugging purposes and native build for production. However, it is still necessary to test everything on a native build, as some functionality may work in plain-jar but not in native builds.
  • As mentioned earlier, there are some limitations to GraalVM’s native binary building, such as reflection, proxies, JNI and so on. If you are configuring a complex project, it probably won’t work out-of-the-box due to libraries using dynamic features. Mostly it is the main reason if something does not work after migrating to GraalVM. However, you can usually bypass these limitations by following the steps described here. These steps involve adding .json files (known as “metadata”) to the build, which describe the dynamic features used in the application. If something is misconfigured here, the application may not work properly.
    • There is a metadata repository that contains ready-to-use configurations for popular libraries. Java-world plugins automatically download files from this repository, but this feature is not yet implemented in sbt-native-image.
    • In addition, some metadata can be generated automatically using the trace agent. sbt-native-image plugin has the nativeImageRunAgent command, which starts tracing. However, in my experience, it does not work well. I am not sure if this is a Scala-related issue or if tracing is generally problematic.
    • SO, tracing is not always the perfect solution, repository fetching is not working at all, and as a result, you will mostly need to write and manage the .json metadata files manually when using GraalVM with Scala.

In summary, adding GraalVM to your Scala application can be quite challenging as for now (I hope in future it will be much easier). However, the benefits are so significant that still it’s worth the effort.

Conclusion

Thank you for reading this little article, I hope it will be useful to someone. Feel free to reach me if you have something to say.

And also take a look on the posts on similar topics: