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
👋 Need help or want to chat? Say hi on Clojurians Slack in #graalvm.
We heartily welcome, and greatly appreciate, tips, tricks, corrections and improvements from you.
Hello world
Tips and tricks
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.
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"
).
Learn What's Being Included
When you add GraalVM's native-image
-H:+PrintAnalysisCallTree
option, under ./reports
you will learn what packages, classes and methods are
being included in your native image. 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:
- on macOS
command time -l
- on Linux
command time -v
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.
CLJ-1472
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)
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
deffers 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 oracle/graal#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)
...
Workarounds:
- Use a Java 8 version of GraalVM
- Patch
clojure.lang.Reflector
on the classpath with the conditional logic swapped out for non-conditional code which works on Java 11 (but not on Java 8). The patch can be found here. - If you require your project to support native image compilation on both Java 8
and Java 11 versions of GraalVM then use the patch found here.
This version does not respect any Java 11 module access rules and improper reflection
access by your code may fail. The file will need to be renamed to
Reflector.java
.
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:
- A java file is written defining a class. This class contains
public static native
methods defining the C functions you would like, their arguments and the return types. An example is here - A C header file with a
.h
extension is generated from this java file:- Java 8 uses a special tool
javah
which is called on the class file. You will need to first create the class file withjavac
and then generate the header file from that withjavah -o Library.h -cp directory_containing_class_file Library.class
- Java 11 bundled this tool into
javac
. You will javac on the.java
source file and specify a directory to store the header file in likejavac -h destination_dir Library.java
- Java 8 uses a special tool
- A C implementation file is now written with function definitions that match the prototypes
created in the
.h
file. You will need to#include
your generated header file. An example is here - The C code is compiled into a shared library as follows (specifying the correct path to the graal home instead of $GRAALVM):
- On linux, the compilation will take the form
cc -I$GRAALVM/include -I$GRAALVM/include/linux -shared Library.c -o liblibrary.so -fPIC
- On MacOS, the compilation will take the form
cc -I$GRAALVM/Contents/Home/include -I$GRAALVM/Contents/Home/include/darwin -dynamiclib -undefined suppress -flat_namespace Library.c -o liblibrary.dylib -fPIC
- On linux, the compilation will take the form
- Once the library is generated you can load it at clojure runtime with
(clojure.lang.RT/loadLibrary "library")
- The JVM will need to be able to find the library on the standard library path. This can be
set via
LD_LIBRARY_PATH
environment variable or via theld
linker config file (/etc/ld.so.conf
on linux). Alternately you can set the library path by passing-Djava.library.path="my_lib_dir"
to the java command line or by setting it at runtime with(System/setProperty "java.library.path" "my_lib_dir")
- Functions may be called via standard Java interop in clojure via the interface specified
in your
Library.java
file:(Library/method args)
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.
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.