Jan 31, 2021

Using GraalVM native-image-agent when porting a library to Quarkus

I pondered a bit how GraalVM’s native-image-agent can be leveraged when writing Quarkus extensions. Even if the configuration produced by it won’t be 100% complete and relevant, it may give you a good initial impression where to look and which kinds of problems you need to solve.

Background

Native compilation of Java using GraalVM’s native-image tool typically requires a lot of configuration. You need to list classes/methods/fields that require reflection, you need to register proxies, you need to declare which classes have to be initialized at runtime instead of build time, etc.

If you are on Quarkus, you usually do not need to care for these nitty-gritty details because Quarkus does it for you under the hood and mvn package -Dnative is all you need to know.

But say, that you are not so lucky to have a ready to use Quarkus extension for some library and you want to write one. Folks who are not able or not wanting to use Quarkus are actually in quite a similar situation.

Configuring native-image

So how can the native-image configuration be put together? Generally speaking there are three options:

A. Pure knowledge

You are a GraalVM guru and you know your complete class path so well that you can write your native-image configuration from top of your head.

Well, this is probably not your case, because you are reading this blog post 😜

B. Trial and error

You first try to compile your application with no configuration at all. If the compilation fails, you try to understand the error message and fix what’s necessary. If the message does not make any sense to you, you paste it to google and you hope to find a solution.

Once the compilation succeeds, you start testing the native executable. It may fail with some ClassNotFoundException if you forgot to register some class for reflection or with a NullPointerException if you forgot to embed some resources to the native image. It may also fail with some cryptic exception that you have no clue about. You google again and you either find a solution that works for you or you see only reports of frustration similar to your own.

C. native-image-agent

The official recommendation of GraalVM is to use their native-image-agent. You are supposed to put your application (with the agent attached) under a load that hits all execution paths that will be active also in production. Based on the runtime data, the agent outputs a configuration in a format acceptable for native-image.

This sounds very promising, but of course the main caveat is that if you are unable to simulate the production load properly during the test runs, then the produced configuration will not be complete. Your native application may thus fail in production.

Besides that, the agent records only the usage of some features (namely JNI, Reflection, Proxies and class path resources) while it does not help with configuring others, like delayed class initialization. So you still may have to assemble some parts of the configuration manually.

native-image vs. Quarkus

native-image-agent was designed for use on plain GraalVM, without Quarkus. How can we benefit from it when writing Quarkus extensions?

The main idea here is to attach it to the JVM running the application against which we run integration tests.

Write tests first

When porting a library or framework to Quarkus, I always start with writing some tests. Ideally, they should be integration tests. It means that there should be a real Quarkus application in src/main/java and the tests should communicate with it only via network protocols, like HTTP. The tests should cover all important use cases of the ported library.

Check Quarkus testing guide to learn how to write integration tests for Quarkus applications.

If I am extremely lucky, the tests just pass in native mode and the library can be considered working in native without any native image configuration.

But that’s very rare. The tests usually pass in JVM mode and they fail in native mode because some piece of native-image configuration is missing.

Run tests with native-image-agent attached

If you do not know the internals of the ported library well, native-image-agent may come in very handy. Even if the configuration produced by it won’t be 100% complete and relevat, it may give you a good initial impression where to look and which kinds of problems you need to solve.

How can you attach native-image-agent to your tests? Let me explain first how Quarkus JVM tests annotated with @QuarkusTest work.

When running the tests from Maven via mvn test there are two JVMs taking part in the game:

  1. The main Maven JVM

  2. A separate JVM started by surefire to run the tests

Should native-image-agent be attached to the second one?

While it is certainly the better of the two options, it is still not perfect. The problem is that this JVM is used not only to run the application under test (living under src/main), but also the test code living under src/test. Clearly, we do not intend to compile the tests to native. We only want to compile the application.

So, how can we split the second JVM into a test JVM and an application JVM?

I have found the following hammer-and-nails procedure (please let me know if you know a better one):

  1. Remove the @QuarkusTest annotation from all your test classes. Why? Because otherwise Quarkus JUnit extension would start the application in the test JVM and that’s exactly what we’d like to avoid.

  2. Package your application skipping the tests

    $ mvn clean package -DskipTests
  3. Start your application manually using GraalVM JRE (in which native-image-agent is available) with the agent attached and test Quarkus profile activated:

    $ export JAVA_HOME=/path/to/graalvm-ce-java11-...
    $ $JAVA_HOME/bin/java -agentlib:native-image-agent=config-output-dir=target/graal-config" -Dquarkus.profile=test -jar target/*-runner.jar
    ...
    2021-01-31 15:58:31,675 INFO  [io.quarkus] (main) Profile test activated.
    2021-01-31 15:58:31,676 INFO  [io.quarkus] (main) Installed features: [..., cdi, resteasy]
  4. Run the tests in another terminal:

    $ mvn test # or alternatively mvn surefire:test might suffice too
    ...
    [INFO] BUILD SUCCESS
  5. When all the tests have passed, terminate the application running in the first terminal by pressing CTRL+C.

    After that, the configuration for native-image should be stored in target/graal-config:

    $ tree target/graal-config
    target/graal-config
    β”œβ”€β”€ proxy-config.json
    β”œβ”€β”€ reflect-config.json
    β”œβ”€β”€ resource-config.json
    └── ...

Update 2021-02-17:

Oliver Weiler proposed another approach:

You can use the Surefire JVM, but ignore all test-related classes by supplying an access-filter.json file to the native-image-agent. See Access Filters docs.

Translate the generated config to Quarkus BuildItems

Once the native-image-agent has generated the configuration, you can study the content of the files and consider whether and how you need to translate the individual items to Quarkus BuildItems.

Check Quarkus for extension authors to learn Quarkus extensions basics.

You typically do not need to care for all configuration items because some of them are already covered by some core Quarkus extension, such as quarkus-netty, quarkus-jackson, quarkus-vertx, etc.

For the ones that are apparently related to the library you are porting, the mapping goes like the following:

Check Quarkus BuildItems reference for more details about the available BuildItems.

Example: If you have something like the following in your reflect-config.json

[
{
  "name":"com.azure.core.util.DateTimeRfc1123",
  "allDeclaredFields":true,
  "allDeclaredMethods":true,
  "allDeclaredConstructors":true
},
{
  "name":"com.azure.storage.blob.implementation.models.BlobsGetPropertiesResponse",
  "allDeclaredConstructors":true
}
]

this is how you would translate it to a Quarkus @BuildStep:

class AzureBuildSteps {

    @BuildStep
    void reflectiveClasses(BuildProducer<ReflectiveClassBuildItem> reflectiveClasses) {

        reflectiveClasses.produce(
                new ReflectiveClassBuildItem(
                        true, // allDeclaredMethods required
                        true, // allDeclaredFields required
                        "com.azure.core.util.DateTimeRfc1123"));

        reflectiveClasses.produce(
                new ReflectiveClassBuildItem(
                        false, // allDeclaredMethods not required
                        false, // allDeclaredFields not required
                        "com.azure.storage.blob.implementation.models.BlobsGetPropertiesResponse"));

    }
}

Is that all?

Not necessarily. As I have mentioned above, native-image-agent does not handle all aspects of the configuration. Running the tests in native mode may reveal more issues that may require additional BuildItems or even GraalVM substitutions. That’s already beyond the scope of this blog post where I primarily wanted to show how native-image-agent can be leveraged when writing Quarkus extensions.

Thanks for reading, stay tuned for more posts about Quarkus, GraalVM, Apache Camel and mvnd!

Edit this post

Related posts