Testing future Java APIs with Gradle

I’ve been eager to try out future Java features like Project Loom and Project Panama ever since I read the JEPs. Preview features, intended to collect feedback on experimental language constructs and APIs, bring exciting additions to the language. This post presents a guide on how to setup a Gradle project to use early-access OpenJDK builds that include these APIs.

Before we start, be adviced that Gradle doesn’t support enabling preview language features of future Java versions. The incremental compiler relies on hardcoded ASM versions that don’t understand these constructs yet. This post covers the use of preview APIs introduced in early-access builds.

Installing an early-access JDK build

The OpenJDK site provides pre-built binaries for early access builds. Select the version corresponding to your OS, and download it. At the time of writing the latest build for Project Panama is 16-panama+3-385. Thus,

$ wget -c -O - "https://download.java.net/java/early_access/panama/3/openjdk-16-panama+3-385_linux-x64_bin.tar.gz" | tar -xz

will download and extract the JDK into the ./panama-b385-jdk-16 directory. The repo also includes instructions on how to build the JDK from source.

Creating a new project

For the sake of ease, we will create a project based on the java-library plugin. The described features are applicable to all JVM plugins. I’m using Gradle 6.7.1, but most settings should remain the same in future releases. Run gradle init and select your preferred build script DSL and testing framework.

Setting up the build script

Gradle recently introduced the concept of Java Toolchains. A toolchain encompasses the tools that execute tasks on the JVM. These include the compiler (javac), the executable (java), and documentation generator (javadoc). By default, Gradle uses the same Java version to execute workers and build projects. We can change this behavior by setting the language version:

java {
  toolchain {
    languageVersion = JavaLanguageVersion.of(16)
  }
}

You can also specify different versions for each task. Gradle automatically detects local JRE and JDK installations, and will even try to download missing versions through the AdoptOpenJDK API. However, it’s not capable of downloading EA builds (yet? the API returns successful responses for these builds). Right now it requests a JDK 16 binary, which hasn’t been released yet.

We can disable this behavior and tell Gradle it’s being too clever by creating a gradle.properties file and asking it to consider the EA installation when looking for a Java 16-compatible toolchain:

Replace /home/hugo by your install path. Note the home directory tilde ~ is not resolved.

org.gradle.java.installations.paths=/home/hugo/panama-b385-jdk-16

An alternative way of specifying the EA installation directory is to define it in an environment variable (e.g. JDK16) and replace the previous property by org.gradle.java.installations.fromEnv=JDK16. Even if more cumbersome, I prefer this method because it allows for customizable paths across different developer machines. Furthermore, it reminds me that I’m running an experimental version.

Finally, your IDE needs to know about the new APIs. I don’t know about other applications, but importing the EA JDK is a two-step process in IntelliJ IDEA:

  1. Navigate to Project Settings → SDKs, and select “Add JDK…”.
  2. Select the Project tab and set the Project SDK to the just-imported JDK.

Using the experimental APIs

Skip these 2 steps if you prefer not to use Java modules.Let’s build a toy module to test our setup. First, enable module path inference on your build.gradle script:

java {
  modularity.inferModulePath = true
  // ...
}

Then, create a src/main/java/module-info.java file to specify the module requires the foreign memory access API:

module me.hugmanrique.fancylibrary {
  exports me.hugmanrique.fancylibrary;
  requires jdk.incubator.foreign;
}

Next, the actual library code:

package me.hugmanrique.fancylibrary;

import jdk.incubator.foreign.MemoryAccess;
import jdk.incubator.foreign.MemorySegment;

public class OverengineeredArrayPrinter {

  public void print(final byte[] array) {
    var segment = MemorySegment.ofArray(array);

    for (int i = 0; i < array.length; i++) {
      byte value = MemoryAccess.getByteAtOffset(segment, i);
      System.out.println(value);
    }
  }
}

Running ./gradlew build will use the javac binary from the early-access JDK installation. If all goes well, you will find the packaged JAR file in the build/libs directory.

Have fun testing all the new features!