Java 19 Foreign Function and Memory API: A Preview of the Future of Java Interoperability

{getToc} $title={Table of Contents}


Java 19 Foreign Function and Memory API: A Preview of the Future of Java Interoperability


Introduction


Java 19 introduces a preview feature that aims to improve Java interoperability with native code and data: the Foreign Function and Memory (FFM) API. This API enables Java programs to call native libraries and process native data without the brittleness and danger of JNI. 
The API invokes foreign functions, code outside the JVM, and safely accesses foreign memory, memory not managed by the JVM. 

In this blog, we will explore how to use the FFM API to perform various interoperability tasks, such as:
  • Calling C library functions, 
  • Passing Java code as a function pointer to a foreign function, 
  • Describing and accessing complex data structures in foreign memory, 
  • Performing unsafe operations on foreign memory, 
  • And generating Java bindings for native libraries automatically from header files. 

We will also compare the FFM API with JNI and other alternatives and discuss its benefits and limitations. 

If you are interested in learning more about the FFM API, you can refer to JEP 424, which provides background information and design details about this preview feature. 

You can also check out the Oracle documentation, which contains examples and tutorials on how to use the FFM API.



Body


Calling a C Library Function with the FFM API

One of the most common interoperability tasks is to call a foreign function, that is, a function defined in a native library. 

The FFM API makes this task easy and safe by allowing Java programs to construct and invoke method handles that target foreign functions. 

For example, consider the strlen C standard library function:

size_t strlen (const char *s);


It takes one argument, a pointer to a null-terminated string, and returns the length of the string. 
To call this function from a Java application, we would follow these steps:
  1. Allocate off-heap memory, which is memory outside the Java runtime, for the strlen function’s argument. See Allocating Off-Heap Memory.
  2. Store the Java string in the off-heap memory that we allocated. See Dereferencing Off-Heap Memory.
  3. Locate the address of the strlen function. See Locating a Foreign Function.
  4. Create a description of the strlen function signature. See Creating a Function Descriptor.
  5. Create a downcall handle for the strlen function. See Creating a Downcall Handle.
  6. Call the strlen function directly from Java. See Calling a Foreign Function.

The following example calls strlen with the FFM API:

static long invokeStrlen (String s) throws Throwable { 
 try (MemorySession session = MemorySession.openConfined ()) { 
  // 1. Allocate off-heap memory 
  MemorySegment nativeString = session.allocateUtf8String (s);

 // 2. Store the Java string in the off-heap memory
 // This is done by allocateUtf8String

 // 3. Locate the address of the strlen function
 Linker linker = Linker.nativeLinker ();
 SymbolLookup stdLib = linker.defaultLookup ();
 MemorySegment strlen_addr = stdLib.lookup ("strlen").get ();

 // 4. Create a description of the strlen function signature
 FunctionDescriptor strlen_sig = FunctionDescriptor.of (ValueLayout.JAVA_LONG, ValueLayout.ADDRESS);

 // 5. Create a downcall handle for the strlen function
 MethodHandle strlen = linker.downcallHandle (strlen_addr, strlen_sig);

 // 6. Call the strlen function directly from Java
 return (long)strlen.invoke (nativeString);
 } 
}

As we can see, using the FFM API to call foreign functions is straightforward and does not require any native code or tools. Moreover, it provides safety guarantees that prevent memory leaks or crashes that could occur with JNI.



Upcalls: Passing Java Code as a Function Pointer to a Foreign Function

Another important aspect of interoperability is the ability to pass Java code as a function pointer to a foreign function. This is useful for scenarios where native code needs to invoke Java code, such as callbacks. 

The FFM API supports this feature by allowing Java methods to be converted into upcall handles, which can then be passed as arguments to foreign functions. 

For example, consider the qsort C standard library function:

void qsort (void *base, size_t nitems, size_t size, int (*compar)(const void , const void));

It takes four arguments: a pointer to an array of elements, the number of elements in the array, the size of each element, and a pointer to a comparison function that determines the order of the elements. 

To call this function from a Java application, we would follow these steps:

  1. Create a description of the Java method signature that matches the comparison function.
  2. Create an upcall handle for the Java method that implements the comparison logic.
  3. Locate the address of the qsort function.
  4. Create a description of the qsort function signature.
  5. Create a downcall handle for the qsort function.
  6. Allocate off-heap memory for an array of elements that can be sorted by qsort.
  7. Populate the array with some values.
  8. Call the qsort function with the array and the upcall handle as arguments.
  9. Verify that the array is sorted according to the comparison logic.
The following example calls qsort with the FFM API to sort an array of doubles in ascending order:

static void invokeQsort (double[] arr) throws Throwable { 
 try (MemorySession session = MemorySession.openConfined ()) 
 { 
 // 1. Create a description of the Java method signature 
  FunctionDescriptor compar_sig = FunctionDescriptor
				 .of (ValueLayout.JAVA_INT, 
				      ValueLayout.ADDRESS, 
				      ValueLayout.ADDRESS);

 // 2. Create an upcall handle for the Java method
  MethodHandle compar = MethodHandles.lookup ()
  .findStatic (TestQsort.class, "compareDoubles", 
	      MethodType.methodType (int.class, 
				     MemorySegment.class, 
				     MemorySegment.class));
  MemorySegment compar_addr = session
				.allocateUpcallStub (compar, compar_sig);

 // 3. Locate the address of the qsort function
  Linker linker = Linker.nativeLinker ();
  SymbolLookup stdLib = linker.defaultLookup ();
  MemorySegment qsort_addr = stdLib.lookup ("qsort").get ();

 // 4. Create a description of the qsort function signature
  FunctionDescriptor qsort_sig = FunctionDescriptor
				.ofVoid (ValueLayout.ADDRESS,
					ValueLayout.JAVA_LONG,
					ValueLayout.JAVA_LONG,
                                        ValueLayout.ADDRESS);

 // 5. Create a downcall handle for the qsort function
  MethodHandle qsort = linker.downcallHandle (qsort_addr, qsort_sig);

 // 6. Allocate off-heap memory for an array of doubles
  MemorySegment array = session.allocateArray (ValueLayout.JAVA_DOUBLE, arr);

 // 7. Populate the array with some values
  for (int i = 0; i < arr.length; i++) {
    array.setAtIndex (ValueLayout.JAVA_DOUBLE, i, arr[i]);
  }

 // 8. Call the qsort function with the array and the upcall handle as arguments
  long size = ValueLayout.JAVA_DOUBLE.byteSize ();
  long length = arr.length;
  qsort.invokeExact (array, length, size, compar_addr);

 // 9. Verify that the array is sorted
  System.out.println ("Sorted array:");
  for (int i = 0; i < arr.length; i++) {
   System.out.println (array.getAtIndex (ValueLayout.JAVA_DOUBLE, i));
  }
 }
}

// The Java method that implements the comparison logic 
static int compareDoubles (MemorySegment x, MemorySegment y) { 
 double x_val = x.get(ValueLayout.JAVA_DOUBLE); 
 double y_val = y.get(ValueLayout.JAVA_DOUBLE); 
 return Double.compare(x_val, y_val); 
}

As we can see, using upcalls with the FFM API is straightforward and does not require any native code or tools.



Memory Layouts and Structured Access

Sometimes, we need to interact with foreign memory that contains complex data structures, such as structs, unions, or arrays. 

The FFM API provides a way to describe and access these data structures using memory layouts. A memory layout is an object that specifies the size, alignment, byte order, and type of a memory region. 

Memory layouts can be composed to form more complex layouts that describe nested or repeated data structures. 

For example, consider the following C declaration of a struct that represents a point in two-dimensional space:

struct Point { int x; int y; };

To describe this struct in Java, we can use the following memory layout:

MemoryLayout pointLayout = MemoryLayout
			    .structLayout ( ValueLayout.JAVA_INT.withName (“x”), 
					    ValueLayout.JAVA_INT.withName (“y”));

This layout specifies that a point struct consists of two int values, named x and y, that are laid out sequentially in memory. 

We can use this layout to allocate off-heap memory for a point struct and access its fields using MemoryAccess methods or VarHandle objects. 

For example:

try (MemorySession session = MemorySession.openConfined ()) { 
 // Allocate off-heap memory for a point struct 
 MemorySegment point = session.allocate (pointLayout);

 // Access and modify the fields of the point struct 
 MemoryAccess.setInt (point.select (“x”), 10); // set x to 10
 
 MemoryAccess.setInt (point.select (“y”), 20); // set y to  20 
 int x = MemoryAccess.getInt (point.select (“x”)); // get x 
 int y = MemoryAccess.getInt (point.select (“y”)); // get y

 // Alternatively, use VarHandle objects to access and modify the fields 
 VarHandle xHandle = pointLayout
			.varHandle (int.class, 
				    PathElement.groupElement (“x”)); 

 VarHandle yHandle = pointLayout
			.varHandle (int.class, 
				    PathElement.groupElement (“y”)); 

 xHandle.set (point, 30); // set x to 30 
 yHandle.set (point, 40); // set y to 40 
 x = (int)xHandle.get (point); // get x 
 y = (int)yHandle.get (point); // get y 

}

We can also create an array layout for point structs and allocate off-heap memory for an array of point structs. For example:

// Create an array layout for point structs 
 MemoryLayout arrayLayout = MemoryLayout
				.sequenceLayout (10, pointLayout);

// Allocate off-heap memory for an array of point structs 
 MemorySegment array = session.allocate (arrayLayout);

// Access and modify the elements of the array 
 for (int i = 0; i < 10; i++) { 
   MemorySegment element = array.select (PathElement.sequenceElement (), i); 
   MemoryAccess.setInt (element.select (“x”), i * 10); // set x to i * 10 
   MemoryAccess.setInt (element.select (“y”), i * 20); // set y to i * 20 
 }

As we can see, using memory layouts with the FFM API allows us to describe and access complex data structures in foreign memory without any native code or tools. 

Moreover, it provides safety guarantees that prevent memory leaks or crashes that could occur with JNI.



Restricted Methods


Some operations on foreign memory are considered unsafe or verbose and are not supported by the FFM API by default. These operations include copying data between segments, converting between segments and byte buffers or arrays, and accessing memory regions with unknown size or alignment. 

To perform these operations, the FFM API provides a set of restricted methods. These methods are marked with the @jdk.incubator.foreign.annotation.ForeignAccess annotation and require explicit opt-in via command-line options or annotations. 

For example, to use the MemorySegment::copyFrom method, which copies data from one segment to another, we need to specify the --enable-native-access command-line flag:

java --enable-native-access=ALL-UNNAMED TestCopy

Alternatively, we can use the @jdk.incubator.foreign.annotation.NativeAccess annotation on the class that contains the restricted method call:

@NativeAccess public class TestCopy { 
 public static void main (String[] args) { 
  try (MemorySession session = MemorySession.openConfined ()) { 
   // Allocate two segments with different contents and sizes 
      MemorySegment s1 = session.allocateArray (ValueLayout.JAVA_INT, 
						new int[] {1, 2, 3}); 
      MemorySegment s2 = session.allocateArray (ValueLayout.JAVA_INT, 
						new int[] {4, 5});
   // Copy data from s1 to s2 using a restricted method
      s2.copyFrom (s1);

   // Verify that the data is copied correctly
      System.out.println ("Mismatch index: " + s2.mismatch (s1)); // prints -1
  }
 }
}

Restricted methods are not recommended for general use and may be removed in future versions of the FFM API. They are provided for convenience and compatibility purposes only. 
Users should prefer using safer and more expressive alternatives whenever possible.



Calling Native Functions with Jextract

One of the challenges of using the FFM API is to write the Java code that links and calls native functions. This code requires knowing the exact names, addresses, and signatures of the native functions, as well as creating the corresponding memory layouts and method handles. 

This can be tedious and error-prone, especially for large or complex native libraries. Fortunately, the FFM API comes with a tool that can generate Java bindings for native libraries automatically from header files. This tool is called jextract.

Jextract is a command-line tool that takes one or more header files as input and produces a Java source file and a native library as output. The Java source file contains classes and interfaces that wrap the native functions, constants, macros, structs, unions, enums, and typedefs declared in the header files.

The native library contains helper functions that are used by the Java bindings to perform upcalls and other operations. The generated Java bindings use the FFM API under the hood to link and call native functions and access native memory.

For example, suppose we want to call some math functions from libc, such as sin, cos, tan, etc. We can use jextract to generate Java bindings for these functions from the math.h header file:

jextract -t com.example.math -l m /usr/include/math.h

This command will produce two files: com/example/math/math.java and libmathHelper.so
The math.java file contains a class named math that has static methods for each math function declared in math.h. For example:

public static double sin(double x) { 
 try { 
     return (double)MATH_sin.invokeExact(x); 
 } catch (Throwable ex) { 
     throw new AssertionError(ex); 
   } 
}

private static final 
	MethodHandle MATH_sin = RuntimeHelper
				.downcallHandle( 
				LIBRARIES.get(“m”), 
				“sin”, “(D)D”, 
				FunctionDescriptor
				.of(C_DOUBLE, C_DOUBLE));

The libmathHelper.so file contains some helper functions that are used by the Java bindings internally.

To use the generated Java bindings, we just need to add them to our classpath and library path. 
For example:

java -cp . -Djava.library.path=. com.example.math.TestMath

public class TestMath { 
	public static void main(String[] args) { 
	// Call math functions from libc using Java bindings 
	System.out.println("sin(0) = " + math.sin(0)); 
	System.out.println("cos(0) = " + math.cos(0)); 
	System.out.println("tan(0) = " + math.tan(0)); 
	} 
}

So, using jextract with the FFM API makes it easy and convenient to call native functions from Java without writing any native code or tools.



Conclusion

In this blog, we have learned how to use the Foreign Function and Memory (FFM) API to interoperate with native code and data from Java. 

We have seen how to use the FFM API to call foreign functions and access foreign memory, how to use memory layouts to describe and access complex data structures in foreign memory, how to use restricted methods to perform unsafe operations on foreign memory, and how to use jextract to generate Java bindings for native libraries automatically from header files. 

We have also compared the FFM API with JNI and other alternatives and discussed its benefits and limitations.

The FFM API is a preview feature in Java 19, which means that it is not yet a permanent part of the Java platform. It is subject to change or removal in future releases, based on feedback from users and developers. 

If you are interested in trying out the FFM API and providing feedback, you can download a JDK 19 early-access build that includes this feature from https://jdk.java.net/19/. You can also refer to the following resources for more information about the FFM API:

  • JEP 424: Foreign Function & Memory API (Preview), which provides background information and design details about this preview feature.
  • Oracle documentation: Foreign Function and Memory API, which contains examples and tutorials on how to use the FFM API.
  • GitHub repository: openjdk/panama-foreign, which hosts the source code and tests for the FFM API and its related tools.
We hope you enjoyed this blog and found it useful. Please feel free to leave your comments and questions below. Thank you for reading!



Previous Post Next Post