Skip to main content
Skip table of contents

The Road to GraalVM Native Image Compatibility

Introduction

Support for GraalVM Native Image in the open-source iText Core library was realized with the release of iText Suite version 8.0.4 in May 2024, and was extended to also include the pdfHTML add-on in the iText Suite 9.1 release. However, if you’re wondering how we got here, the seeds were sown by a chance meeting between Apryse representatives Raf Hens and Michael Demey, and Josh Long at the PDF Association’s https://pdfa.org/event/pdf-week-fall-2023/ event in San Francisco. Josh is a prominent Spring Developer Advocate, and he had one question for them - “Do you know about GraalVM?”

Indeed they did; though for those that don’t, GraalVM is an high-performance Java Development Kit (JDK) which offers the ability to build Native Image executables. These are standalone binaries which are compiled ahead-of-time, as opposed to the just-in-time compilation of traditional JDKs. While modern virtual machine performance can be close to native in many scenarios, native executables still have certain advantages, especially when it comes to scalable containerized applications. They require no time to spin up, and can be highly optimized to reduce memory usage.

Josh told them that Spring offered GraalVM wrappers and builders for a number of libraries, and also had many iText users asking if they had a GraalVM configuration available. As the leader of Apryse’s Research Team, and a long-time iText developer, Michael was inspired. He immediately began a working on a proof-of-concept implementation using iText 5. Being a monolithic library, rather than iText 7 and subsequent major versions, Michael correctly assumed implementing iText 5 support would be a relatively simple task.

Once he had a prototype up and running, Michael passed on what he’d learned to the iText development team to work on an implementation of Native Image support for the current version of iText Core. Which is where I come in.

Getting Started with GraalVM

GraalVM provides a JDK, the Graal compiler, and a tool to build native executables. The way Native Image compilation works is that rather than computing the dynamically-accessed program elements at runtime (as with the JVM), static analysis to determine those dynamic features is performed when building a native binary. In some cases it works out of the box, though for some advanced cases like resource loading or reflection usage, they provide a way to configure your application/library so that it still can be used to build native applications. See the Native Image Reachability Metadata documentation for details.

These configuration files (metadata) are in JSON format and can be provided in several ways:

  • (Preferable) By putting them directly into the library’s jar files.

  • By contributing to the oracle/graalvm-reachability-metadata repository.

  • (Less user-friendly) Users can generate their own by tracking the execution paths of their application.

We determined the best approach for us and iText’s users was to both add the config files into the required jars, and also contribute to the reachability repository. And so we set to work.

Step 1 - Trying it Out

We began working from Michael’s development branch on our GitHub. Since our goal was to add continuous support for iText Core, we added a possibility to trace our tests to look for all the places in code that GraalVM couldn’t handle automatically. This tracing creates the metadata config files and puts them into the corresponding jars for the modules which make up the iText Core library. For example, the kernel module requires a reflect-config file, while the io module also requires a resource-config file.

We use the JUnit framework to execute tests, which adds a lot of data not related to iText into metadata files of its own. At the time we were using JUnit 4, however we have since upgraded to JUnit 5.

With all that in place, now we could create our own application using GraalVM’s Native Image Maven plugin. As an demonstration, here is an example of a profile you can use:

Native build profile example
XML
<profiles>
  <profile>
    <id>native</id>
    <build>
      <plugins>
        <plugin>
          <groupId>org.codehaus.mojo</groupId>
          <artifactId>exec-maven-plugin</artifactId>
          <version>3.0.0</version>
          <executions>
            <execution>
              <id>java-agent</id>
              <goals>
                <goal>exec</goal>
              </goals>
              <configuration>
                <executable>java</executable>
                <workingDirectory>${project.build.directory}</workingDirectory>
                <arguments>
                  <argument>-classpath</argument>
                  <classpath/>
                  <argument>com.itextpdf.jumpstart.HelloWorld</argument>
                </arguments>
              </configuration>
            </execution>
            <execution>
              <id>native</id>
              <goals>
                <goal>exec</goal>
              </goals>
              <configuration>
                <executable>${project.build.directory}/HelloWorld</executable>
                <workingDirectory>${project.build.directory}</workingDirectory>
              </configuration>
            </execution>
          </executions>
        </plugin>
        <plugin>
          <groupId>org.graalvm.buildtools</groupId>
          <artifactId>native-maven-plugin</artifactId>
          <version>0.10.0</version>
          <extensions>true</extensions>
          <executions>
            <execution>
              <id>build-native</id>
              <goals>
                <goal>build</goal>
              </goals>
              <phase>package</phase>
            </execution>
          </executions>
          <configuration>
            <fallback>false</fallback>
            <buildArgs>
              <arg>-H:-CheckToolchain -H:+AllowDeprecatedBuilderClassesOnImageClasspath</arg>
            </buildArgs>
            <metadataRepository>
              <enabled>true</enabled>
              <localPath>f:/itext/repos/graalvm-reachability-metadata/metadata</localPath> <!-- This part is not required if config files are all in corresponding jars -->
            </metadataRepository>
            <agent>
              <enabled>true</enabled>
            </agent>
          </configuration>
        </plugin>
      </plugins>
    </build>
  </profile>
</profiles>

As an output, you get a native executable with the functionality you implemented.

For another example of a project configuration using Maven, the GraalVM Reference Manual provides this pom.xml on their GitHub.

Step 2 - Create Clean Config Files and Create a Pull Request

Our next task was to create (and test) clean metadata configuration files, and then create a pull request to add them to the GraalVM Reachability Metadata repository.

We followed the steps described in the https://github.com/oracle/graalvm-reachability-metadata/blob/master/CONTRIBUTING.md on how to add metadata and tests into the repository, and submitted our pull request to add our metadata and tests into the repository.

Step 3 - Continuous integration

We added the same metadata files into our own iText Core repository and into the jar artifacts. Then we let the Oracle team know we distribute and test our metadata files ourselves with this commit.

The result of our efforts can be seen in GraalVM’s table of libraries and frameworks tested with Native Image, which shows the iText Core modules rated as two stars. This is the highest test level, and designates iText Core as being continuously tested for compatibility by its maintainers.

GraalVM Supported Libraries and Frameworks Table

As of the release of iText Suite 8.0.4, the Core modules have the highest rating. Since the release of pdfHTML 6.1.0 with iText Suite 9.1, the html2pdf module is also listed in the table.

Issues we Encountered

Of course, things rarely work first time without any issues. Here are some issues we had to overcome, or work around:

  1. GraalVM JDK requires Visual Studio 2022 on Windows, though it does work with VS 2019 providing you to switch off the compatibility check by setting -H:-CheckToolchain as a command-line argument while building.

  2. You need to use the latest Gradle; or at least 8.5 to be compatible with Java 21.

  3. We could not register a security provider at runtime. So, the provider should have been registered before (see JCA Security Services in Native Image) or, if you need the Bouncy Castle security provider for example, you can provide GraalVM with the code to register the provider at build time. This is an example of such code:

JAVA
package com.itextpdf.jumpstart;
import java.security.Security;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.graalvm.nativeimage.hosted.Feature;
import org.graalvm.nativeimage.hosted.RuntimeClassInitialization;
public class BouncyCastleFeature implements Feature {
    public BouncyCastleFeature() {
    }
    @Override
    public void afterRegistration(AfterRegistrationAccess access) {
        RuntimeClassInitialization.initializeAtBuildTime("org.bouncycastle");
        Security.addProvider(new BouncyCastleProvider());
    }
}

In addition, you need to provide some extra build arguments:

CODE
                  --features=com.itextpdf.jumpstart.BouncyCastleFeature
                  --initialize-at-run-time=org.bouncycastle.crypto.prng.SP800SecureRandom
                  --initialize-at-run-time=org.bouncycastle.jcajce.provider.drbg.DRBG$NonceAndIV
                  --initialize-at-run-time=org.bouncycastle.jcajce.provider.drbg.DRBG$Default
  1. Note that I couldn’t build with Bouncy Castle FIPS using the same approach as above. However, in the official GraalVM documentation they suggest to instead use Jipher JCE for FIPS compliant apps with Native Image.

The above security provider issues were resolved in iText Core version 9.1.0 as we introduced the new SecurityProviderProxy class. This class handles the security provider registration at runtime, or at compile (build) time, depending on the environment. When building with GraalVM, the provider will be registered at compile time.

Nonetheless, the above information is helpful if you’re building with an earlier version of iText Core which supports GraalVM Native Image compilation.

Known Limitations

  • java.awt cannot be used

When trying java.awt.Image awtImage = java.awt.Toolkit.getDefaultToolkit().createImage(imagePath);, it throws the following exception:

TEXT
java.lang.NoSuchMethodError: java.awt.Toolkit.getDefaultToolkit()Ljava/awt/Toolkit

This is not a significant problem in most cases, though you should note that com.itextpdf.io.image.ImageDataFactorycan take java.awt.Image as a parameter. There are multiple complaints about it on the web, but as yet it seems there is no good solution.

Conclusion

We successfully achieved Native Image support with the release of iText Core version 8.0.4, and all subsequent releases thanks to the continuous support implementation. In addition, support was increased to include the pdfHTML add-on with pdfHTML 6.1.0, which was included in our 25th anniversary release of iText Suite 9.1. However, my GraalVM story doesn’t stop there.

Having implemented support for GraalVM Native Image compilation, the obvious next step would be to create a native image application with iText Core, right? In an follow-up article, I’ll show you how I created a sample AWS Lambda service to process PDFs, as a real-world example of using iText Core and GraalVM Native Image.

Written by

Vitali Prudnikovich, Software Engineer, iText SDK

JavaScript errors detected

Please note, these errors can depend on your browser setup.

If this problem persists, please contact our support.