BT

Facilitating the Spread of Knowledge and Innovation in Professional Software Development

Write for InfoQ

Topics

Choose your language

InfoQ Homepage News JavaOne 2025: Function and Memory Access in Pure Java

JavaOne 2025: Function and Memory Access in Pure Java

Log in to listen to this article

Per-Åke Minborg, consulting member of technical staff, Java Core Libraries at Oracle, presented Function and Memory Access in Pure Java at JavaOne 2025. Minborg kicked off his presentation with an introduction to JEP 454, Foreign Function & Memory API, delivered in JDK 22 and under the auspices of Project Panama.

The Foreign Function & Memory API (FFM), having evolved from two JEPs, namely: JEP 393, Foreign-Memory Access API (Third Incubator); and JEP 389, Foreign Linker API (Incubator), both delivered in JDK 16, was designed to be a replacement for the Java Native Interface (JNI), a native programming interface to interoperate with applications and libraries written in other programming languages, such as C, C++ and Assembly. Problems with JNI include: a native-first programming model that was a fragile combination of Java and C; expensive to maintain and deploy; and passing data to/from JNI can be cumbersome and inefficient.

The JNI workflow process starts with defining a native Java method using the native modifier. Consider the following Java class.

    
/**
 * Getpid.java
 */
public class GetPid {

    static {
        System.loadLibrary("getpid");
    }

    native static long getpid();
    }
    

Now, using the javac -h command on the GetPid.java file, the required C header file is generated.

    
/**
 * getpid.h
 */
#include <jni.h>
#include <stdlib.h>

#ifndef _Included_GetPid
#define _Included_GetPid
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class: GetPid
 * Method: getpid
 * Signature: ()J
 */
jlong JNICALL Java_GetPid_getpid(JNIEnv *, jobject recv);

#ifdef __cplusplus
}
#endif
#endif
    

Then, the main C application, that implements the native method declared in the Java class, may now be written.

    
/**
 * getpid.c
 */
#include "GetPid.h"

jlong JNICALL Java_GetPid_getpid(JNIEnv *env, jobject recv) {
    return getpid();
    }
    

While the process works and has been available for quite some time, problems include: support for only primitive types and Java objects; no way to deterministically free memory; a limited addressing space of approximately two GB; and inflexible sequential or offset-based addressing options.

Frameworks that attempted to solve these problems included: Java Native Access; Java Native Runtime; and JavaCPP, but never gained any traction for various reasons. Minborg maintained that "a more direct, pure Java paradigm" is necessary.

Minborg introduced some of the interfaces that comprise the FFM API along with numerous code examples to demonstrate how to properly use it. All of the code examples referenced this two-dimensional data structure.

    
struct Point2d {
    double x;
    double y;
    }

point = { 3.0, 4.0 };
    

Foreign Memory API

Accessing flat memory is accomplished via the MemorySegment interface that provides access to a continuous region of 64-bit addressed memory with support for absolute memory addressing. These memory segments are controlled by:

  • Size: Out-Of-Bounds memory access is not allowed
  • Lifetime: Use-After-Free access is not allowed
  • Thread Confinement: the ability to see an object from a single thread

The lifecycle of native memory segments may be controlled by the Arena interface that provides flexible allocation and improved timely deallocation. An arena also provides a safety guarantee with no Use-After-Free access to memory. Arena types include:

  • Global: with an unbounded lifetime
  • Auto: with an automatic garbage-collected lifetime
  • Confined: with an explicitly-bounded lifetime
  • Shared: with an explicitly-bounded lifetime

All of these types, with the exception of the Confined type, offer multi-threaded access. It is also important to note that closing a Shared arena type triggers a thread-local handshake as defined in JEP 312, Thread-Local Handshakes, delivered in JDK 10. Custom arenas may be created by simply implementing the Arena interface.

The ValueLayout interface models values of basic data types. Three attributes, packaged in a value layout, are required to access memory segments:

  • Carrier Type: the value of the Java data type to read and write
  • Endianness: whether the dereference operation should swap bytes
  • Alignment: the alignment constraint on the address being dereferenced

Value layouts may be used to obtain an instance of the VarHandle class from the MemorySegment interface.

Consider this example, using an arena Auto type, that allocates memory and writes doubles, 3d and 4d, into the memory segment at the given offsets, 0 and 8, respectively, with the given value layout consistent with the C Point2d structure.

    
MemorySegment point = Arena.ofAuto().allocate(8 * 2);
point.set(ValueLayout.JAVA_DOUBLE, 0, 3d);
point.set(ValueLayout.JAVA_DOUBLE, 8, 4d);
...
point.get(ValueLayout.JAVA_DOUBLE, 16); // exception
    

This will result in an IndexOutOfBoundsException as it has violated the Out-Of-Bounds memory access. However, there is automatic deallocation and the offset needs to be manually computed.

In this next example, using an arena Confined type, it allocates memory and writes values in the same fashion as the previous example.

    
MemorySegment leakedPoint = null;
try (Arena offHeap = Arena.ofConfined()) {
    MemorySegment point = leakedPoint = offHeap.allocate(8 * 2);
    point.set(ValueLayout.JAVA_DOUBLE, 0, 3d);
    point.set(ValueLayout.JAVA_DOUBLE, 8, 4d);
    } // free
...
leakedPoint.get(ValueLayout.JAVA_DOUBLE, 0); // exception
    

This will result in an IllegalStateException as it has violated the Use-After-Free access due to the arena having been closed. This example also provides deterministic deallocation and the Out-of-Bounds access restriction remains the same. However, as with the previous two examples, the offset needs to be manually computed.

As manually computing the offset can be tedious and prone to errors, the MemoryLayout interface describes the contents of a memory segment in a structured fashion, such as point.y, as defined in the Point2d structure above. Memory layouts may be queried to obtain sizes, alignments and names.

In the following code snippet, the structLayout() method, defined in the MemoryLayout interface, returns a static instance of the StructLayout interface, a group layout whose member layouts are laid out one after the other.

    
MemoryLayout.structLayout(
    ValueLayout.JAVA_DOUBLE.withName("x"),
    ValueLayout.JAVA_DOUBLE.withName("y")
    );
    

In this example, using an arena Confined type, it allocates the memory and writes the values now using instances of the VarHandle class.

    
MemoryLayout POINT_2D = MemoryLayout.structLayout(
    ValueLayout.JAVA_DOUBLE.withName("x"),
    ValueLayout.JAVA_DOUBLE.withName("y")
    );
static final VarHandle XH = POINT_2D.varHandle(PathElement.groupLayout("x"));
static final VarHandle YH = POINT_2D.varHandle(PathElement.groupLayout("y"));
try (Arena offHeap = Arena.ofConfined()) {
    MemorySegment point = offHeap.allocate(POINT_2D);
    XH.set(point, 0L, 3d);
    YH.set(point, 0L, 4d);
    } // free
    

This example provides the benefits from the previous example, but now the offsets are derived from the memory layouts and declaring the instances of VarHandle as final is crucial to get the best performance.

Foreign Function API

Minborg also introduced jextract, a tool that mechanically generates Java bindings, built upon the FFM API, from native library headers.

The following example calls a native quick sort function.

    
$ jextract --output classes --target-package org.stdlib /usr/include/stdlib.h

import static org.stdlib.stdlib_h.*;
...

try (Arena offHeap = Arena.ofConfined()) {
    MemorySegment array =
        offHeap.allocateFrom(C_INT, 0, 9, 3, 4, 6, 5, 1, 8, 2, 7);
    var compareFunc = allocate((a1, a2) ->
    Integer.compare(a1.get(C_INT, 0), a2.get(C_INT, 0)), offHeap);
    qsort(array, 10L, 4L, comparFunc);
    int[] sorted = array.toArray(JAVA_INT);
    // [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
    }
    

Developers can start working with jextract by downloading the early-access builds.

Conclusion

In closing, Minborg provided the benefits of using the FFM API, namely that it provides: safe and efficient access to native memory, i.e., deterministic deallocation and layout API to enable structured access; general, direct and efficient access to native functions, i.e., 100% Java with no need to write and maintain native code; and the foundations of Project Panama interoperability, i.e., tooling (e.g. jextract) to generate layouts along with var and method handles.

About the Author

BT