• Stars
    star
    389
  • Rank 110,500 (Top 3 %)
  • Language
  • License
    Eclipse Public Li...
  • Created about 5 years ago
  • Updated 4 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Scripts and tips to get Clojure latest working with GraalVM latest

graal-docs

Rationale

This little repo’s goal is to collect scripts and tips on natively compiling Clojure programs with GraalVM.

GraalVM offers the ability to compile Java classes to native binaries. Because Clojure is hosted on the JVM, compiling Clojure programs to native binaries is also, to some extent, possible.

Native binaries offer fast startup times and are therefore an attractive option for command line tools that are used in scripting and editor integration. Popular examples are babashka and clj-kondo, see resources for many more examples.

We mostly keep our tips related to Clojure, but will sometimes we’ll add something just because we didn’t easily find it elsewhere, and we feel it would be helpful to others.

Unless otherwise indicated, when we refer to GraalVM in this repository, we are referring to the Graal Substrate VM. The Substrate VM is exposed as the GraalVM native compiler.

If you are trying to decide if GraalVM is for you, the trade-offs are nicely explained by Oleg Šelajev in the "AOT vs JIT" section of his "Maximizing Java Application Performance with GraalVM video".

Community

👋 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.

Style guidance:

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

Tips and tricks

Clojure version

Use the current Clojure v1.10 release, it includes several GraalVM specific fixes, including:

Interesting fixes slated for Clojure 1.11:

  • CLJ-2636 - Get rid of reflection on java.util.Properties when defining *clojure-version*

Other fixes of interest:

  • CLJ-2582 - Improve GraalVM native image size / compile time memory consumption when compiling clojure.pprint

  • TCHECK-157 - Randomization doesn’t work with GraalVM native-image

Runtime Evaluation

A natively compiled application cannot use Clojure’s eval to evaluate Clojure code at runtime. If you want to dynamically evaluate Clojure code from your natively compiled app, consider using SCI, the Small Clojure Interpreter. The ultimate example of evaluating Clojure with a natively compiled Clojure application is babashka.

Reflection

Make sure you put (set! *warn-on-reflection* true) at the top of every namespace in your project. This tells the Clojure compiler to report cases where Clojure is using reflection. You can address these via type hints.

Prior versions of Clojure’s own clojure.stacktrace made use of reflection (see JIRA CLJ-2502) but this not an issue for the current Clojure release.

You can include a reflect-config.json for classes that are reflected upon at runtime. E.g. for this program:

(ns refl.main
  (:gen-class))

(defn refl-str [s]
  (.length s)) ;; reflection on String happens here

(defn -main [& _]
  (println (refl-str "foo")))

you will need a config like:

[{
  "name":"java.lang.String",
  "allPublicMethods":true
}]

and use the argument -H:ReflectionConfigurationFiles=reflect-config.json during native compilation.

To let GraalVM configure 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"). You can get this name by calling (str (.getClass instance)) in a REPL.

See the GraalVM docs on reflection for details on the config format.

To automatically discover reflection, you can use assisted configuration driven by the native-image-agent.

To prevent false positives in the generated config, you can use a caller based filter.

filter.json:

{ "rules": [
  {"excludeClasses": "clojure.**"},
  {"includeClasses": "clojure.lang.Reflector"}
]
}

To invoke the agent you will need to run your program on the JVM and add the -agentlib:native-image-agent argument.

E.g.:

$ mkdir -p classes
$ clojure -M -e "(compile 'refl.main)"
refl.main
$ java -agentlib:native-image-agent=caller-filter-file=filter.json,config-output-dir=. -cp $(clojure -Spath):classes refl.main

This will output:

reflect-config.json

[
{
  "name":"java.lang.String",
  "allPublicMethods":true
},
{
  "name":"java.lang.reflect.Method",
  "methods":[{"name":"canAccess","parameterTypes":["java.lang.Object"] }]
},
{
  "name":"java.util.Properties",
  "allPublicMethods":true
}
]

The entry for java.lang.reflect.Method is expected. See here for an explanation.

It’s unclear where the reflection on java.util.Properties is made (perhaps here?). It is probably safe to leave it out and probably even recommended as this class will pull in XML libraries due to its storeToXML methods. To exclude this class, you can use an access filter.

Report what is being analyzed

Use GraalVM’s native-image -H:+PrintAnalysisCallTree to to learn what packages, classes and methods are being analyzed. These details are written under ./reports.

Note that this option will greatly slow down compilation so it’s better to turn it off in production builds.

Visualize what is in your native image

To visualize what is in your native image, you can use the GraalVM Dashboard, here’s an example screenshot:

GraalVM Dashboard Screenshot

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.

native-image compilation time

You can shorten the time it takes to compile a native image, and sometimes dramatically 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:

  • If you’re using Leiningen, add :jvm-opts ["-Dclojure.compiler.direct-linking=true"] to the profile you’re using for compilation (the same one that includes :aot :all)

  • If you’re using tools.deps via the Clojure CLI tools, add :jvm-opts ["-Dclojure.compiler.direct-linking=true"] to the alias you’re using for compilation

    • You can alternatively specify this property at the command line when invoking clojure: clojure -J-Dclojure.compiler.direct-linking=true -M -e "(compile 'my.ns)"

Class Initialization

In most cases, Clojure compiled classes must be initialized at build time for them to work with GraalVM native-image. If this has not been done, when you attempt to run your resulting native binary, you might see an exception that includes:

java.io.FileNotFoundException: Could not locate clojure/core__init.class, clojure/core.clj or clojure/core.cljc on classpath

Fortunately, the solution is easy, include clj-easy/graal-build-time on your native-image classpath. See graal-build-time docs for details.

ℹ️

The old trick was to use the --initialize-at-build-time option with native-image. This option has been deprecated in GraalVM v21 and is slated for removal in GraalVM v22.

Despair not, migrating to clj-easy/graal-build-time can be as easy for your project as it was for clj-kondo.

Optional Transitive Dependencies

A Clojure app that optionally requires transitive dependencies can be made to work under GraalVM with dynaload. You’ll want to follow its advice for GraalVM.

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.

Static linking with musl

Using musl for static builds is recommended by the official GraalVM docs. Usage of --static without specifying --libc=musl will use glibc instead, however while this may look like a fully statically binary, this will still load some libraries (using dlopen) at runtime, and may result in some segmentation fault errors related to glibc version mismatches. See this section in official glibc documentation for more information on why glibc "static" builds are not really static.

With --static --libc=musl, you will have truly static binaries equivalent to Go’s with CGO_ENABLED=0 or Rust compiled with musl. This libraries can be deployed almost anywhere and is also smaller than the glibc equivalent. However, keep in mind that musl builds still have some limitations:

  • Only works with Linux AMD64 on Java 11 for now

  • You will need to either use a distro that already have musl and zlib statically compiled in the repositories or compile it yourself.

  • There is a known issue with stack sizes in musl being really small by default and main thread not respecting stack size settings. This may cause some stack overflow errors during runtime

If supporting non-glibc distros are not an issue for you, there is also an option of building a mostly static native image that should work in any glibc distro. Those binaries are very similar to Go binaries without CGO_ENABLED=0 and Rust images build with glibc (the default).

Writing GraalVM specific code

While it would be nice to have the same clojure code run within a GraalVM image as on the JVM, there may be times where a GraalVM specific workaround may be necessary. GraalVM provides a class to detect when running in a GraalVM environment:

This class provides the following methods:

static boolean 	inImageBuildtimeCode()
Returns true if (at the time of the call) code is executing in the context of image building (e.g.

static boolean 	inImageCode()
Returns true if (at the time of the call) code is executing in the context of image building or during image runtime, else false.

static boolean 	inImageRuntimeCode()
Returns true if (at the time of the call) code is executing at image runtime.

static boolean 	isExecutable()
Returns true if the image is build as an executable.

static boolean 	isSharedLibrary()
Returns true if the image is build as a shared library.

Currently, the ImageInfo class is implemented by looking up specific keys using java.lang.System/getProperty. Below are the known relevant property names and values:

Property name: "org.graalvm.nativeimage.imagecode"
Values: "buildtime", "runtime"

Property name: "org.graalvm.nativeimage.kind"
Values: "shared", "executable"

JDK11 and clojure.lang.Reflector

For GraalVM v21 or later

If you are suffering NoSuchMethodError: java.lang.reflect.AccessibleObject.canAccess exceptions, GraalVM needs a little help. Include the following to your reflection.json file:

{"name": "java.lang.reflect.AccessibleObject",
 "methods" : [{"name":"canAccess"}]}
For older versions of GraalVM

GraalVM started supporting JDK11 in v19.3.0. GraalVM could get confused about a conditional piece of code in clojure.lang.Reflector. This code dispatches based on wether you are on Java v8 or a later major version.

Prior to GraalVM v21, compiling your Clojure code with JDK11 native image and then running it resulted in the following exception being thrown upon 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:

  • Use GraalVM v21 or later (recommended).

  • Use a JDK8 version of GraalVM.

  • Include clj-reflector-graal-java11-fix when compiling your Clojure code.

  • Use the --report-unsupported-elements-at-runtime option.

  • 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 with javac and then generate the header file from that with javah -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 like javac -h destination_dir Library.java

  • 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

  • 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 the ld 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.

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:

  • I don’t think we can do much on this issue. The problem is the inefficiency of the Apple dynamic linker/loader.

  • Yes, startup time is important, but correctness can of course never be compromised. You are correct that a more precise static analysis could detect that, but our current context insensitive analysis it too limited.

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

  • have measured a slowdown in startup time of your native-image produced app after moving to Graal v20

  • want to restore startup app to what it was on macOS prior v20 of Graal

  • are comfortable with a "caveat emptor" hack from the Graal team

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

Targeting a minimum macOS version

On macOS, GraalVM’s native-image makes use of XCode command line tools. XCode creates native binaries that specify the minimum macOS version required for execution. This minimum version can change with each new release of XCode.

To explicitly tell XCode what minimum version is required for your native binary, you can set the MACOSX_DEPLOYMENT_TARGET environment variable.

Bonus tip: to check the the minimum macOS version required for a native binary, you can use otool. Example for babashka native binary at the time of this writing:

> bb --version
babashka v0.2.0
> otool -l $(which bb) | grep -B1 -A3 MIN_MAC
Load command 9
      cmd LC_VERSION_MIN_MACOSX
  cmdsize 16
  version 10.12
      sdk 10.12

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.

License

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