Introduction
In the blog post Beginning Native Java Development, I presented a set of example projects. These projects allow us to experiment with different ways we can interact with native code from inside the JVM. This blog post demonstrates completing the cycle. It uses Graal VM to take us back to native binaries. The process uses both JNI and the new Foreign Function and Memory (FFM) API.
The code that accompanies this blog post can be found at native-java-experiments/beginning-native-java-development. As with the previous blog post, it is recommended to use this branch. The main branch may move on for other projects. This branch also contains some fixes for issues encountered since the last blog post. It is recommended for both running in a JVM and for building native images.
Graal VM
To compile the projects to native images, you will need Graal VM. It must be installed. To simplify my Java installations I make use of https://sdkman.io/ which enables me to select from a large number of JDKs to install.
At the time of writing this project was developed using Graal VM 25.0.2-graalce.
Prerequisite Builds
The last blog post covered each of the projects in the repository so I will not repeat those steps here, before proceeding to the Java native image builds the following projects will need to have been built and installed:
- simple-library
- simple-jni
- jni-library
Information on the build steps is also available in the projects Readme.md.
Java Native Interface (JNI) to Native Image
The JNI example is contained within the simple-jni project, the changes needed to the project were fairly minimal.
The Graal VM native image step is quite time consuming. Activation of this was added to a new Maven profile. It can be activated with the -Dnative property.
<profiles>
<profile>
<id>native</id>
<activation>
<property>
<name>native</name>
</property>
</activation>
This profile uses the Maven plugin for GraalVM Native Image to create the native image. It requires a small amount of configuration to specify the main class. It also enables native calls.
<configuration>
<mainClass>dev.lofthouse.App</mainClass>
<buildArgs>
<buildArg>--enable-native-access</buildArg>
<buildArg>ALL-UNNAMED</buildArg>
</buildArgs>
</configuration>
The project can be built with mvn clean package -Dnative. This results in a new binary simple-jni in the target directory. A simple run-app-native.sh script has been provided to run the binary. This ensures the LD_LIBRARY_PATH environment is set. This way, the libraries can be found.
Running the script results in output similar to before.
Here is our native image of our Java code. It calls out to the jni-library of our project. Then, it calls out to the simple-library.
Foreign Function and Memory (FFM) to Native Image
The updates to the simple-foreign project are initially very similar to the updates made for the simple-jni project. As before a new native Maven Profile is added to the pom to add the GraalVM Maven Plugin. And for when we get to the end a run-app-native.sh script is added to call the new native binary.
Before we can generate the native image, additional information needs to be provided to the GraalVM tooling. This information is provided in the form of a class implementing org.graalvm.nativeimage.hosted.Feature. The GraalVM tooling is able to handle most of the FFM APIs automatically. However, it needs additional information about the method signatures of the native functions that will be called.
@Override
public void duringSetup(DuringSetupAccess access) {
// Stub for: void say_hello(void)
RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.ofVoid());
// Stub for: int add_one(int)
RuntimeForeignAccess.registerForDowncall(FunctionDescriptor.of(JAVA_INT, JAVA_INT));
}
The first call to registerForDowncall specifies that we will call a function that takes no arguments and returns void. The second call specifies that we will call a function that takes an int and returns an int. These two registrations are not mapped to specific function names. If we call a number of functions that take and return a single int, the single registration will suffice. The complete ForeignRegistrationFeature class also contains some further context. It was also possible to leave this class with default visibility instead of public.
The next file needed is a native-image.properties in META-INF/native-image to let GraalVM know about our Feature implementation. The contents of this file are also fairly minimal. It specifies the name of our Feature implementation class and enables the FFM APIs.
After building with mvn clean package -Dnative the new native binary the run-app-native.sh can be used to execute.
Conclusion
This example used several smaller projects. They illustrate all the stages of an interaction from Java code to a target library. In many cases, you would not need to create all of these as new projects. For instance, you might prefer to call a native library that already exists rather than create the simple-library project.
The motivation for my investigations is to explore the use of Java applications on Raspberry Pi devices. These devices are often very constrained. I plan to provide an example and a blog post. These will show how these techniques can be used to create a Java application that interacts with GPIO. The application will be compiled to a Native Java image using GraalVM.
These projects were designed to become more familiar with the Foreign Function and Memory (FFM) APIs. The Java bindings were created manually. For future projects, jextract should be considered to automate most of this. It can generate the Java bindings directly from the header files.
