Getty Images

Tip

Simplifying native interoperability in Java with JEP 454

Finalized in Java 22, JEP 454 helps developers safely and efficiently call native functions and manage memory outside of Java. Here's a rundown of how it works.

Java has long been a powerhouse programming language thanks to its portability, robustness and extensive ecosystem. However, one major challenge for Java developers is interoperation with native code. The Java Native Interface has been the traditional tool for this job, but it's complex and potentially risky, like using a sledgehammer to crack a nut.

That's where JEP 454 comes in. First previewed in Java 19 and delivered in Java 22, it introduces the Foreign Function & Memory (FFM) API, which is designed to address those limitations and usher in a new era of seamless native code integration. This article demonstrates how to access a native function without the rigid multistep process required with Java Native Interface (JNI).

What is JEP 454?

JEP 454 introduces two primary components: the Foreign Function API and the Foreign Memory API. Together, these provide a high-level, idiomatic way to interact with native code and memory, which significantly simplifies the development process and enhances safety.

Foreign Function API

This API enables Java programs to invoke functions written in other programming languages, specifically C/C++. By using the MethodHandle and FunctionDescriptor classes, Java developers can seamlessly call foreign functions without the boilerplate code and complexity associated with JNI.

Foreign Memory API

This API provides safe and efficient access to memory outside the Java heap. New concepts such as memory segments and memory layouts offer fine-grained control over memory allocation, access and deallocation and ensure memory safety.

How JEP 454 works: An example of non-Java native functions

To demonstrate how these new functions work, consider the following C program that returns the size of the physical memory of the computer:

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/sysctl.h>

uint64_t getMemorySize() {
   int mib[2];
   uint64_t memory;
   size_t len = sizeof(memory);

   // Set the MIB (Management Information Base) to get physical memory size
   mib[0] = CTL_HW;
   mib[1] = HW_MEMSIZE;

   // Get the memory size
   if (sysctl(mib, 2, &memory, &len, NULL, 0) == -1) {
       perror("sysctl");
       exit(EXIT_FAILURE);
   }

   return memory;
}

To access that basic code through Java, we must turn it into a library. (We'll assume that we use gcc as the C compiler on a Unix machine.)

gcc -shared -o libmemory.so -fPIC memoryinfo.c

This creates a shared library that we can access using the FFM API. However, to achieve this we must know a few classes. This API, part of the java.lang.foreign package, consists of a few core classes:

  • Linker: This interface provides mechanisms to link Java code with foreign functions in libraries that conform to a specific Application Binary Interface. It supports both downcalls to foreign functions and upcalls from foreign functions to Java code.
  • SymbolLookup: This interface is used to retrieve the address of a symbol, such as a function or global variable, in a specific library. It supports various types of lookups, including library lookups, loader lookups and default lookups provided by a linker.
  • MemorySegment: This interface provides access to a contiguous region of memory, either on the Java heap (heap segment) or outside it (native segment). It offers various access operations to read and write data while ensuring spatial and temporal bounds.
  • MethodHandle: This strongly typed class is a directly executable reference to an underlying method, constructor or field. It provides two special invoker methods (invokeExact and invoke) and is immutable with no visible state. A reference of MethodHandle can be obtained via Linker::downcallHandle() to invoke the method.
  • FunctionDescriptor: A function descriptor models the signature of a foreign function. Comprised of zero or more argument layouts and zero or one return layout, it is used to create downcall method handles and upcall stubs.
  • Arena: This interface in Java controls the lifecycle of native memory segments and provides methods for their allocation and deallocation within specified scopes. It comes in different types (global, automatic, confined and shared), each with unique characteristics regarding lifetime, thread accessibility and manual control.

Now, let's write the Java code to access the above C code.

import java.lang.foreign.*;
import java.lang.invoke.MethodHandle;
import java.nio.file.Path;

public class MemoryInfo {
   public static void main(String[] args) {
       try (Arena arena = Arena.ofConfined()) {
           Path path = Path.of("libmemory.so");
           SymbolLookup lookup = SymbolLookup.libraryLookup(path, arena);
           MemorySegment getMemorySize = lookup.findOrThrow("getMemorySize");

           Linker linker = Linker.nativeLinker();
           FunctionDescriptor functionDescriptor = FunctionDescriptor.of(ValueLayout.JAVA_LONG);

           MethodHandle getMemorySizeHandle = linker.downcallHandle(
                   getMemorySize,
                   functionDescriptor
           );

           long memorySize = (long) getMemorySizeHandle.invokeExact();
           System.out.println("Total memory: " + (memorySize / 1024 / 1024 / 1024) + " GB");
       } catch (Throwable e) {
           System.out.println("Error: " + e.getMessage());
       }
   }
}

In this code, we first load the shared library file ("libmemory.so") that contains a function named getMemorySize. The SymbolLookup tool locates the exact memory location of the getMemorySize function within the loaded library. Next, we create an instance of Linker, which we use to get the MethodHandle to invoke it. Then, we define the FunctionDescriptor, which essentially provides the foreign function's layout, inputs and outputs. After that we provide the memory location and function description to the linker's downcallHandle() method to get the MethodHandle. Finally, we invoke it.

JEP 454 benefits and improvements

The FFM API offers several significant benefits over JNI, including the following:

  1. Improved productivity. By providing high-level abstractions, JEP 454 reduces the boilerplate code and complexity involved in calling native functions and managing memory. Developers can focus on their application's logic rather than the intricacies of JNI.
  2. Enhanced performance. The new APIs are designed to allow efficient function calls and memory access. This is particularly beneficial for applications that require frequent interaction with native libraries.
  3. Safety and security. JEP 454 addresses common pitfalls of JNI, such as manual memory management and use-after-free bugs. The Foreign Memory API enforces memory safety which reduces the risk of security vulnerabilities.

JEP 454: Simpler interaction with native code

JEP 454 represents a significant step forward for Java, providing a modern, efficient and safe way to interact with native code and memory. By simplifying the integration process and enhancing safety, the FFM API opens new possibilities for Java developers, from scientific computing to high-performance applications, and opens the door to reuse existing machine learning libraries that are written in C/C++.

In the next article, we will dive deeper into the Foreign Function API, exploring its key components and real-world applications.

A N M Bazlur Rahman is a Java Champion and staff software developer at DNAstack. He is also founder and moderator of the Java User Group in Bangladesh.

Dig Deeper on Core Java APIs and programming techniques

App Architecture
Software Quality
Cloud Computing
Security
SearchAWS
Close