clj-graal-docs

Rationale

GraalVM offers the ability to compile Java classes to native binaries. This is possible to some extent with Clojure programs as well. This approach works well for command line tools that require fast startup so they can be used for scripting and editor integration.

This little repo's goal is to collect scripts and tips to GraalVM-ify Clojure code.

When we refer to GraalVM in this repository, we mean SubstrateVM, the native compiler, unless otherwise indicated.

Community

:wave: Need help or want to chat? Say hi on Clojurians Slack in #graalvm.

This is a team effort. We heartily welcome, and greatly appreciate, tips, tricks, corrections and improvements from you. Much thanks to all who have contributed.

The current curators of this repository are: @borkdude and @lread.

Hello world

Tips and tricks

Clojure version

Use clojure "1.10.2-alpha1". This release contains several graalvm specific fixes.

Reflection

Make sure you put (set! *warn-on-reflection* true) at the top of every namespace in your project to get rid of all reflection. There is a patch to make clojure.stacktrace work with GraalVM in JIRA CLJ-2502, which is currently available in the Clojure 1.10.2 test release.

To let Graal config the reflector for an array of Java objects, e.g. Statement[] you need to provide a rule for [Lfully.qualified.class (e.g. "[Ljava.sql.Statement").

Report what is being analyzed

When you add GraalVM's native-image -H:+PrintAnalysisCallTree option, under ./reports you will learn what packages, classes and methods are being analyzed. Note that this option will likely slow down compilation so it's better to turn it off in production builds.

native-image RAM usage

GraalVM's native-image can consume more RAM than is available on free tiers of services such as CircleCI. To limit how much RAM native-image uses, include the --no-server option and set max heap usage via the "-J-Xmx" option (for example "-J-Xmx3g" limits the heap to 3 gigabytes).

If you are suffering out of memory errors, experiment on your development computer with higher -J-Xmx values. To learn actual memory usage, prefix the native-image command with:

These time commands report useful stats in addition to "maximum resident set size".

Actual memory usage is an ideal. Once you have a successful build, you can experiment with lowering -J-Xmx below the ideal. The cost will be longer build times, and when -J-Xmx is too low, out of memory errors.

native-image compilation time

You can shorten the time it takes to compile a native image, and possibly reduce the amount of RAM required, by using direct linking when compiling your Clojure code to JVM bytecode.

This is done by setting the Java system property clojure.compiler.direct-linking to true.

The most convenient place for you to set that system property will vary depending on what tool you’re using to compile your Clojure code:

CLJ-1472

:tada: Update: The recommended patch from CLJ-1472 resolves this issue. This patch is included in the Clojure 1.10.2 test release. We strongly encourage you to try it out with your projects and report back any issues to the Clojure core team.

Clojure 1.10 introduced locking code into clojure.spec.alpha that often causes GraalVM's native-image to fail with:

Error: unbalanced monitors: mismatch at monitorexit, 96|LoadField#lockee__5436__auto__ != 3|LoadField#lockee__5436__auto__
Call path from entry point to clojure.spec.gen.alpha$dynaload$fn__2628.invoke():
    at clojure.spec.gen.alpha$dynaload$fn__2628.invoke(alpha.clj:21)

This issue will also cause builds that utilize extend, extend-type, and extend-protocol to fail.

The reason for this is that the bytecode emitted by the locking macro fails bytecode verification. The relevant issue on the Clojure JIRA for this is CLJ-1472. We document how to apply patches from this issue and several other workarounds here.

Initialization

Unlike the early days the current native-image defers the initialization of most classes to runtime. For Clojure programs it is often actually feasible (unlike in a typical Java program) to change it back via --initialize-at-build-time to achieve yet faster startup time. You can still defer some classes to runtime initialization using --initialize-at-run-time.

Static linking vs DNS lookup

If you happen to need a DNS lookup in your program you need to avoid statically linked images (at least on Linux). If you are builing a minimal docker image it is sufficient to add the linked libraries (like libnss*) to the resulting image. But be sure that those libraries have the same version as the ones used in the linking phase.

One way to achieve that is to compile within the docker image then scraping the intermediate files using the FROM scratch directive and COPY the executable and shared libraries linked to it into the target image.

See https://github.com/oracle/graal/issues/571

JDK11 and clojure.lang.Reflector

JDK11 is supported since GraalVM 19.3.0. GraalVM can get confused about a conditional piece of code in clojure.lang.Reflector which dispatches on Java 8 or a later Java version.

Compiling your clojure code with JDK11 native image and then running it will result in the following exception being thrown apon first use of reflection:

Exception in thread "main" com.oracle.svm.core.jdk.UnsupportedFeatureError: Invoke with MethodHandle argument could not be reduced to at most a single call or single field access. The method handle must be a compile time constant, e.g., be loaded from a `static final` field. Method that contains the method handle invocation: java.lang.invoke.Invokers$Holder.invoke_MT(Object, Object, Object, Object)
    at com.oracle.svm.core.util.VMError.unsupportedFeature(VMError.java:101)
    at clojure.lang.Reflector.canAccess(Reflector.java:49)
    ...

See the issue on the GraalVM repo.

Workarounds:

Interfacing with native libraries

For interfacing with native libraries you can use JNI. An example of a native Clojure program calling a Rust library is documented here. Spire is a real life project that combines GraalVM-compiled Clojure and C in a native binary.

To interface with C code using JNI the following steps are taken:

JNI API bugs

JNI contains a suite of tools for transfering datatypes between Java and C. You can read about this API here for Java 8 and here for Java 11. There are a some bugs (example) in the GraalVM implementations of some of these functions in all versions up to and including GraalVM 20.0.0. Some known bugs have been fixed in GraalVM 20.1.0-dev. If you encounter bugs with these API calls try the latests development versions of GraalVM. If bugs persist please file them with the Graal project.

Startup performance on macOS

@borkdude noticed slower startup times for babashka on macOS when using GraalVM v20. He elaborated in the @graalvm channel on Clojurians Slack:

The issue only happens with specific usages of certain classes that are somehow related to security, urls and whatnot. So not all projects will hit this issue.

Maybe it's also related to enabling the SSL stuff. Likely, but I haven't tested that hypothesis.

The Graal team closed the issue with the following absolutely reasonable rationales:

Apple may fix this issue in macOS someday, who knows? If you:

then you may want to try incorporating this Java code with @borkdude's tweaks into your project.

Here's how @borkdude applied the fix to babashka.

GraalVM development builds

Development builds of GraalVM can be found here. Note that these builds are intended for early testing feedback, but can disappear after a proper release has been made, so don't link to them from production CI builds.

Testing Strategies

External resources

Curated collection of projects, articles, etc.

License

Distributed under the EPL License, same as Clojure. See LICENSE.