{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:
- Allocate off-heap memory, which is memory outside the Java runtime, for the strlen function’s argument. See Allocating Off-Heap Memory.
- Store the Java string in the off-heap memory that we allocated. See Dereferencing Off-Heap Memory.
- Locate the address of the strlen function. See Locating a Foreign Function.
- Create a description of the strlen function signature. See Creating a Function Descriptor.
- Create a downcall handle for the strlen function. See Creating a Downcall Handle.
- 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:
- Create a description of the Java method signature that matches the comparison function.
- Create an upcall handle for the Java method that implements the comparison logic.
- Locate the address of the qsort function.
- Create a description of the qsort function signature.
- Create a downcall handle for the qsort function.
- Allocate off-heap memory for an array of elements that can be sorted by qsort.
- Populate the array with some values.
- Call the qsort function with the array and the upcall handle as arguments.
- 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!