Introducing Java Native Runtime (JNR) – Foreign (Function) Memory API
139. Introducing Java Native Runtime (JNR)
Java Native Runtime (JNR) is another open-source attempt to address JNI complexity. It is a serious competitor for JNA, having a more intuitive and powerful API than JNI. We can add it as a dependency as follows:
<dependency>
<groupId>com.github.jnr</groupId>
<artifactId>jnr-ffi</artifactId>
<version>2.2.13</version>
</dependency>
Let’s assume that we have the exactly same C method (sumTwoInt()) and the native shared library (math.dll) from Problem 138.We start by writing a Java interface containing the declarations of methods and types that we plan to call from Java and are defined in native code. We write the SimpleMath interface containing the sumTwoInt() declaration as follows:
public interface SimpleMath {
@IgnoreError
long sumTwoInt(int x, int y);
}
The @IgnoreError annotation instructs JNR to not save the errno value (https://www.geeksforgeeks.org/errno-constant-in-c/).Next, we have to instruct JNR to load the math.dll library and generate a concrete implementation of this interface so we can call its methods. For this, we need the LibraryLoader and the following intuitive code:
public class Main {
public static void main(String[] args) {
LibraryLoader<SimpleMath> loader =
FFIProvider.getSystemProvider()
.createLibraryLoader(SimpleMath.class)
.search(“./jnr/cpp”)
.map(“sumTwoInt”, “_Z9sumTwoIntii”);
loader = loader.map(“sumTwoInt”, “_Z9sumTwoIntii”);
if (Platform.getNativePlatform().getOS()
== Platform.OS.WINDOWS) {
SimpleMath simpleMath = loader.load(“math”);
long result = simpleMath.sumTwoInt(3, 9);
System.out.println(“Result: ” + result);
}
}
}
Via the LibraryLoader API, we prepare the playground. We instruct JNR that our library is located in jnr/cpp via the search() method. Moreover, we provide the proper mapping of the method’s names via the map() method (remember from Problem 138 that G++ renames the method via name mangling (or, name decoration) from sumTwoInt to _Z9sumTwoIntii).Finally, we load the library via the load() method and call the sumTwoInt() method.JNR provides many other features that you can exploit starting from https://github.com/jnr. You may also be interested in JavaCPP which is another alternative to JNI (https://github.com/bytedeco/javacpp).
140. Motivating and introducing the project Panama
Project Panama or Foreign Function & Memory (FFM) API is the elegant way to say goodbye to JNI. This project started in JDK 17 as JEP 412 (1st incubator). It continued in JDK 18 as JEP 419 (2nd incubator), in JDK 19 as JEP 424 (first preview), JDK 20 as JEP 434 (second preview), JDK 21 as JEP 442 (Third Preview). We stop here since this is the moment when this book was written.To understand the goals of this project, we have to talk about accessing off-heap memory from Java applications. By off-heap memory, we understand the memory that is outside the JVM heap and is not managed by the Garbage Collector.Surfing off-heap is the job of JNI, JNA, and JNR. In one way or other, these APIs can work in off-heap land for handling different tasks. Among these tasks, we can enumerate the following:
use native libraries (for instance, some common libraries are Open CL/GL, Cuda, Tensorflow, Vulkan, OpenSSL, V8, Blas, cuDNN, and so on)
share memory across different processes
serialize/deserialize memory content to the so-called mmaps
The Java de facto API for accomplishing these kinds of tasks is ByteBuffer, or better, the so-called allocated direct buffers which are more efficient in accessing off-heap memory. Alternatively, we can use JNI, or as you saw, third-party libraries such as JNA and JNR.But, ByteBuffer and JNI have a lot of shortcomings that make them useful only in a limited number of scenarios. A few of their drawbacks are listed below:
ByteBuffer
- Brittle and error-prone
- Unstable memory addresses
- Backed by an array that can be manipulated by the Garbage Collector
Allocated Direct Buffers
- Cannot scale when they are used as a general off-heap API
- Work well only if they are used by power-users that deeply understand their purposes
- No solution for deallocation/free memory
JNI
- As you saw in Problem 137, JNI is hard to use (even for simple cases)
- Brittle and error-prone
- Difficult/expensive to maintain
- Poor error checking
- Can crush the JVM
These shortcomings and much more are behind the reason for creating Project Panama. The goal of this project is to become the new de facto API for interoperating with foreign data, functions, and memory in Java. For accomplishing this goal, the Project Panama follows two main features:
A future-proof API (low-level, efficient, robust, and safe) to replace the old-school API based on byte buffers – this is referred to as the Memory Access API and it is capable to access on-heap and off-heap memory
A brand new paradigm replaces the JNI concepts and mechanisms, so now we have an intuitive, easy to use and robust solution for creating Java bindings for native libraries – this is referred to as the Foreign Linker API
In the next problems, we will dive deeper into this project.