Arek Jurasz

Software Engineer

Build a native image with GraalVM

June 08, 2019

Last month first production-ready version of GraalVM with number 19.0 was released. GraalVM is a virtual machine capable to run application written in JavaScript, Python, Ruby, R, LLVM-based languages like C, C++ and of course our beloved JVM-based languages like Java, Scala, Kotlin, Groovy and Clojure. Some of the main goals for this new virtual machine are:

  • improve the performance of applications build with JVM-based languages
  • reduce startup time by usage of AOT (ahead-of-time) compilation
  • write polyglot applications
  • compile JVM-based code to a standalone executable a.k.a. native image.

With this post, I would like to focus on the last goal and build a native image. The assumption is very simple, take JsonPath library, write some Java code to interact with this library and then from terminal quickly test any JsonPath expression you can think of, like

curl -s https://api.github.com/users/ajurasz | json_path "$.created_at"

Native image?

As already mentioned one of the main goals of GraalVM is the possibility to produce native images which are executables that do not require JRE (java runtime environment) to be present on the system. At first, when I heard about this feature I thought that the code instead to be compiled to bytecode will be directly compiled to machine code but I was wrong. During the process of creating a native image, all classes of our application and their dependencies are statically analyses to know which part of that code is reachable during runtime. These static analyses take JDK code under consideration as well. When we know all classes and methods used in the runtime then GraalVM compiles it ahead-of-time. To make this AOT compiled code to run we still need some runtime on which our program can be run. The produced native image includes something called Substrate VM which is an embeddable virtual machine containing components like memory management, thread scheduling, de-optimizer or even garbage collector. Having an initial idea about what native image is let’s install GraalVM, native-image utility and write some code.

Installation

First, let’s install GraalVM through sdkman

sudo apt update
sudo apt install unzip zip
curl -s "https://get.sdkman.io" | bash
source "/home/ubuntu/.sdkman/bin/sdkman-init.sh"
sdk install java 19.0.0-grl

To use native-image command beside native image utility there are some prerequisites

gu install native-image
sudo apt install build-essential
sudo apt install libz-dev

Code

As shown above, we want to pipe output from curl command to our application as input. To get access to this input from application level we need to read it from System.in. JsonPath expression will be simply passed as an argument. In the end, we just need to evaluate the expression against received json.

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.stream.Collectors;

import com.jayway.jsonpath.JsonPath;

public class Application {
    public static void main(String[] args) {
        String expression = extractExpression(args);
        String json = readInput();

        System.out.println(evaluate(json, expression));
    }

    private static String extractExpression(String[] args) {
        if (args.length != 1) {
            throw new IllegalArgumentException("Single JsonPath expression is required.");
        }
        return args[0];
    }

    private static String readInput() {
        return new BufferedReader(new InputStreamReader(System.in))
                .lines()
                .collect(Collectors.joining("\n"));
    }

    private static String evaluate(String json, String expression) {
        JsonPath jsonPath = JsonPath.compile(expression);
        return jsonPath.read(json);
    }
}

Compilation

First, we need to compile our java code with javac command

javac -cp "libs/*" Application.java

libs directory contains all 3rd party dependencies required for this application to run

tree
.
├── Application.java
└── libs
    ├── accessors-smart-1.2.jar
    ├── asm-5.0.4.jar
    ├── json-path-2.4.0.jar
    ├── json-smart-2.3.jar
    ├── slf4j-api-1.7.25.jar
    └── slf4j-jdk14-1.7.25.jar

1 directory, 8 files

After successful ran of compile command Application.class should be produced.

Build native image

Command to build native image is very similar to compiling java file, it requires only java class and -cp parameter in case if we use external dependencies which we do.

native-image -cp ".:libs/*" -H:Name=json_path  Application 

-H:Name is used just to give a name to produced executable. But running above command will fail with the following message

Warning: Aborting stand-alone image build. com.oracle.svm.hosted.substitute.DeletedElementException: Unsupported method java.lang.ClassLoader.defineClass(String, byte[], int, int) is reachable: The declaring class of this element has been substituted, but this element is not present in the substitution class
To diagnose the issue, you can add the option --report-unsupported-elements-at-runtime. The unsupported element is then reported at run time when it is accessed the first time.
Detailed message:
Trace:
        at parsing net.minidev.asm.DynamicClassLoader.defineClass(DynamicClassLoader.java:86)
Call path from entry point to net.minidev.asm.DynamicClassLoader.defineClass(String, byte[]):
        at net.minidev.asm.DynamicClassLoader.defineClass(DynamicClassLoader.java:81)
        at net.minidev.asm.BeansAccessBuilder.bulid(BeansAccessBuilder.java:313)
        at net.minidev.asm.BeansAccess.get(BeansAccess.java:111)
        at net.minidev.json.reader.BeansWriterASM.writeJSONString(BeansWriterASM.java:17)
        at net.minidev.json.JSONValue.writeJSONString(JSONValue.java:586)
        at net.minidev.json.reader.JsonWriter$5.writeJSONString(JsonWriter.java:113)
        at net.minidev.json.reader.JsonWriter$5.writeJSONString(JsonWriter.java:1)
        at net.minidev.json.JSONArray.writeJSONString(JSONArray.java:75)
        at net.minidev.json.JSONArray.toJSONString(JSONArray.java:52)
        at net.minidev.json.JSONArray.toJSONString(JSONArray.java:102)
        at net.minidev.json.JSONArray.toString(JSONArray.java:113)
        at java.lang.String.valueOf(String.java:2994)
        at java.lang.StringBuilder.append(StringBuilder.java:131)
        at com.oracle.svm.core.amd64.AMD64CPUFeatureAccess.verifyHostSupportsArchitecture(AMD64CPUFeatureAccess.java:179)
        at com.oracle.svm.core.JavaMainWrapper.run(JavaMainWrapper.java:131)
        at com.oracle.svm.core.code.IsolateEnterStub.JavaMainWrapper_run_5087f5482cc9a6abc971913ece43acb471d2631b(generated:0)

Native images don’t support dynamic class loading and this is understandable due to the nature of AOT compilation where all classes and bytecodes that are ever reachable needs to be known at compile time. To see the full list of native image limitation see https://github.com/oracle/graal/blob/master/substratevm/LIMITATIONS.md. But we still can take advantage of the suggested solution and postpone any errors resulting from dynamic class loading to runtime by using --report-unsupported-elements-at-runtime option. Let’s make the second attempt to build a native image

 native-image -cp ".:libs/*" -H:Name=json_path --report-unsupported-elements-at-runtime  Application

This time it worked and json_path executable was created. To make our life easier let’s register this executable to be accessible globally in our system

sudo ln -s /home/ubuntu/app/json_path /usr/local/bin/json_path

and let’s give it a try

curl -s https://api.github.com/users/ajurasz | json_path "$.created_at"
2013-03-23T12:56:53Z

Works like a charm but what about some more complex expressions

curl -s https://www.anapioficeandfire.com/api/books/1 | json_path "$.characters.length()"

Exception in thread "main" com.jayway.jsonpath.InvalidPathException: Function of name: length cannot be created
        at com.jayway.jsonpath.internal.function.PathFunctionFactory.newFunction(PathFunctionFactory.java:75)
        at com.jayway.jsonpath.internal.path.FunctionPathToken.evaluate(FunctionPathToken.java:38)
        at com.jayway.jsonpath.internal.path.PathToken.handleObjectProperty(PathToken.java:81)
        at com.jayway.jsonpath.internal.path.PropertyPathToken.evaluate(PropertyPathToken.java:79)
        at com.jayway.jsonpath.internal.path.RootPathToken.evaluate(RootPathToken.java:62)
        at com.jayway.jsonpath.internal.path.CompiledPath.evaluate(CompiledPath.java:53)
        at com.jayway.jsonpath.internal.path.CompiledPath.evaluate(CompiledPath.java:61)
        at com.jayway.jsonpath.JsonPath.read(JsonPath.java:181)
        at com.jayway.jsonpath.JsonPath.read(JsonPath.java:345)
        at com.jayway.jsonpath.JsonPath.read(JsonPath.java:329)
        at Application.evaluate(Application.java:30)
        at Application.main(Application.java:12)
Caused by: java.lang.InstantiationException: Type `com.jayway.jsonpath.internal.function.text.Length` can not be instantiated reflectively as it does not have a no-parameter constructor or the no-parameter constructor has not been added explicitly to the native image.
        at java.lang.Class.newInstance(DynamicHub.java:740)
        at com.jayway.jsonpath.internal.function.PathFunctionFactory.newFunction(PathFunctionFactory.java:73)
        ... 11 more

So a new instance of Length type cannot be created. Quick sneak peek on failing part (PathFunctionFactory.java:75)

public static PathFunction newFunction(String name) throws InvalidPathException {
    Class functionClazz = FUNCTIONS.get(name);
    if(functionClazz == null){
        throw new InvalidPathException("Function with name: " + name + " does not exist.");
    } else {
        try {
            return (PathFunction)functionClazz.newInstance();
        } catch (Exception e) {
            throw new InvalidPathException("Function of name: " + name + " cannot be created", e);
        }
    }
}

That’s true, it looks like there is a set of predefined function which then are dynamically created. Fortunately, this can be fixed by introducing reflection configuration file. This file is used to inform Substrate VM about reflectively accessed program elements. To know more see https://github.com/oracle/graal/blob/master/substratevm/REFLECTION.md. To solve above issue we need to put one entry in graal.json

[
  {
    "name": "com.jayway.jsonpath.internal.function.text.Length",
    "methods": [
      { "name": "<init>", "parameterTypes": [] }
    ]
  }
]

After the rebuild of json_path executable, previous commands started to work

curl -s https://www.anapioficeandfire.com/api/books/1 | json_path "$.characters.length()"
434
        
curl -s https://www.anapioficeandfire.com/api/books | json_path "$.[?(@.name == 'A Game of Thrones')].characters.length()"
[434]

Conclusion

I see the big potential in these native images one of them is light and fast docker images https://blog.softwaremill.com/small-fast-docker-images-using-graalvms-native-image-99c0bc92e70b as pointed out by Adam Warski. What he also mention is that in Scala reflection is almost unused. For me compiling a very simple program to the native image was a few hours of research and at this point, I don’t see how I could do the same with small Spring application which in contrast to Scala ecosystem use reflection heavily.

Share This Post