13 Foreign Function and Memory API

The Foreign Function and Memory (FFM) API enables Java programs to interoperate with code and data outside the Java runtime. 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.

Note:

This is a preview feature. A preview feature is a feature whose design, specification, and implementation are complete, but is not permanent. A preview feature may exist in a different form or not at all in future Java SE releases. To compile and run code that contains preview features, you must specify additional command-line options. See Preview Language and VM Features.

For background information about the Foreign Function and Memory API, see JEP 424.

The FFM API is contained in the package java.lang.foreign.

Calling a C Library Function with the Foreign Function and Memory API

Consider the strlen C standard library function:

size_t strlen(const char *s);

It takes one argument, a string, and returns the length of the string. To call this function from a Java application, you 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 you allocated. See Dereferencing Off-Heap Memory.

    Note:

    Some methods enable you to perform these two steps with one method call; see Methods That Allocate and Populate Off-Heap Memory.
  3. Build and then call a method handle that points to the strlen function. See Linking and Calling a C Function.

The following example calls strlen with the Foreign Function and Memory (FFM) API:

    static long invokeStrlen(String s) throws Throwable {
        
        try (MemorySession session = MemorySession.openConfined()) {
            
            // 1. Allocate off-heap memory, and
            // 2. Dereference off-heap memory
            MemorySegment nativeString = session.allocateUtf8String(s);
        
            // 3. Link and call C function
        
            // 3a. Obtain an instance of the native linker
            Linker linker = Linker.nativeLinker();
        
            // 3b. Locate the address of the C function
            SymbolLookup stdLib = linker.defaultLookup();
            MemorySegment strlen_addr = stdLib.lookup("strlen").get();
        
            // 3c. Create a description of the C function signature
            FunctionDescriptor strlen_sig =
                FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.ADDRESS);
            
            // 3d. Create a downcall handle for the C function    
            MethodHandle strlen = linker.downcallHandle(strlen_addr, strlen_sig);            
            
            // 3e. Call the C function directly from Java
            return (long)strlen.invoke(nativeString);
        } 
    }

Allocating Off-Heap Memory

Off-heap data is data stored in memory outside the Java runtime and therefore not subject to garbage collection. Off-heap data is stored in off-heap memory, which is represented by a MemorySegment object. To invoke a C function from a Java application, its arguments must be in off-heap memory.

The following example create a memory segment off-heap to store the contents of a Java string:

String s = ...
MemorySegment nativeString =
    MemorySegment.allocateNative(s.length()+1, MemorySession.global());

This example allocates a block of off-heap memory of size s.length() + 1, which is large enough to hold the contents of the Java string, plus the trailing terminator character.

All memory segments are associated with a memory session, which determines when the memory segment is valid or can be accessed. A memory session is represented by a MemorySession object. In the previous example, the memory segment is associated with the global memory session, which is a memory session that cannot be closed. As a result, the off-heap memory will only be deallocated when the JVM process ends. If you want a closeable memory session, then you can use a confined memory session:

String s = ...  
try (MemorySession session = MemorySession.openConfined()) {
    MemorySegment.allocateNative(s.length()+1, session);
    // ...
} // nativeString is deallocated here

A confined memory session is a session that can only be accessed by the current thread. This example creates a confined memory session in a try-with-resources statement. At the end of the try-with-resources block, the confined memory session is closed and all the off-heap memory associated with it is released.

Dereferencing Off-Heap Memory

After creating an empty memory segment, nativeString, the next step is to copy the characters of the Java string into the segment:

for(int i = 0; i < s.length(); i++) {
    nativeString.set(ValueLayout.JAVA_BYTE, i, (byte)s.charAt(i));
}

// Add the string terminator at the end
nativeString.set(ValueLayout.JAVA_BYTE, s.length(),(byte)0);

The first argument of the MemorySegment::set method is of type ValueLayout.OfByte. A value layout models the memory layout associated with values of basic data types such as primitives. A value layout encodes the size, the endianness, the alignment of the piece of memory to be dereferenced, and the Java type to be used for the dereference operation. In this example, ValueLayout.JAVA_BYTE has a size of one byte and native endianness, although this is irrelevant as the example is only reading a single byte.

When using this layout, the API expects clients to encode dereferenced values in Java as the primitive type byte. The second argument of the MemoryLayout::set method is the index, relative to the address of the memory segment, where to write the byte value.

You can allocate and populate more complex data types in off-heap memory. See Memory Layouts and Structured Access.

Methods That Allocate and Populate Off-Heap Memory

The SegmentAllocator interface contains methods that both allocate off-heap memory and copy Java data into it. The following invokeStrlen(String s) example uses the class MemorySession, which implements the SegmentAllocator class. The example calls the method SegmentAllocator::allocateUtf8String, which converts a string into a UTF-8 encoded, null-terminated C string, then stores the result into a memory segment.

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

Linking and Calling a C Function

Calling a native function, such as the strlen C standard library function, involves the following steps:

  1. Obtaining an Instance of the Native Linker
  2. Locating the Address of the C Function
  3. Creating the Description of the C Function Signature
  4. Creating the Downcall Handle for the C Function
  5. Calling the C Function Directly from Java
Obtaining an Instance of the Native Linker

A linker provides access to foreign functions from Java code, and access to Java code from foreign functions. The native linker provides access to the libraries that adhere to the calling conventions of the platform in which the Java runtime is running. These libraries are referred to as "native" libraries.

            Linker linker = Linker.nativeLinker();
Locating the Address of the C Function

To call a native method such as strlen, you need a downcall method handle, which is a MethodHandle instance that points to a native function. This instance requires the address of the native function. The following statements obtain the address of the strlen function:

            Linker linker = Linker.nativeLinker();
            SymbolLookup libc = SymbolLookup.libraryLookup("libc.so.6", session);
            MemorySegment strlen_addr = libc.lookup("strlen").get();

SymbolLookup::libraryLookup(String, MemorySession) creates a library lookup, which locates all the symbols in a user-specified native library. It loads the native library and associates it with a MemorySession object. In this example, libc.so.6 is the file name of the C standard library for many Linux systems.

Because strlen is part of the C standard library, you can use instead the native linker's default lookup. This is a symbol lookup for symbols in a set of commonly used libraries (including the C standard library). This means that you don't have to specify a system-dependent library file name:

            // 3a. Obtain an instance of the native linker
            Linker linker = Linker.nativeLinker();
        
            // 3b. Locate the address of the C function
            SymbolLookup stdLib = linker.defaultLookup();
            MemorySegment strlen_addr = stdLib.lookup("strlen").get();
Creating the Description of the C Function Signature

A downcall method handle also requires a description of the native function's signature, which is represented by a FunctionDescriptor instance. A function descriptor describes the layouts of the native function arguments and its return value (if any). The following creates a function descriptor for the strlen function:

            // 3c. Create a description of the C function signature
            FunctionDescriptor strlen_sig =
                FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.ADDRESS);

The first argument of the FunctionDescriptor::of method is the layout of the native function's return value. Native primitive types are modeled using value layouts whose size matches that of the native primitive type. This means that a function descriptor is platform-specific. For example, size_t has a layout of JAVA_LONG on 64-bit or x64 platforms but a layout of JAVA_INT on 32-bit or x86 platforms.

The subsequent arguments of FunctionDescriptor::of are the layouts of the native function's arguments. In this example, it's ValueLayout.ADDRESS; all pointer types are modeled with this value layout. Note that struct types, which aren't shown here, are modeled with struct layouts. See Memory Layouts and Structured Access.

Creating the Downcall Handle for the C Function

The following statement creates a downcall method handle for the strlen function with its address and function descriptor.

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

A downcall method handle has a MethodType associated with it, which represents the arguments and return type accepted and returned by a method handle. In this example, the MethodType associated with strlen has one parameter, Addressable, and its return type is long. Call MethodHandle::type to obtain a method handle's MethodType. See Downcall method handles in the Linker interface JavaDoc API specification for information about how a method's type is derived.

Calling the C Function Directly from Java

The following statement calls the strlen function with a memory segment that contains the function's argument:

            // 3e. Call the C function directly from Java
            return (long)strlen.invoke(nativeString);

You need to cast a method handle invocation with the expected return type; in this case, it's long.

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

An upcall is a call from native code back to Java code. More specifically, it enables you to pass Java code as a function pointer to a foreign function.

Consider the standard C library function qsort, which sorts the elements of an array:

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

It takes four arguments:

  • base: Pointer to first element of the array to be sorted
  • nbemb: Number of elements in the array
  • size: Size, in bytes, of each element in the array
  • compar: Pointer to function that compares two elements

The following example calls the qsort function to sort an int array. However, this method requires a pointer to a function that compares two array elements. The example defines a comparison method (Qsort::qsortCompare), creates a method handle to represent this comparison method, and then creates a function pointer from this method handle.

    class Qsort {
        static int qsortCompare(MemoryAddress addr1, MemoryAddress addr2) {
            return Integer.compare(
                addr1.get(ValueLayout.JAVA_INT, 0),
                addr2.get(ValueLayout.JAVA_INT, 0));
        }
    }
    
    static int[] qsortTest (int[] unsortedArray) throws Throwable {
        
        int[] sortedArray;
    
        try (MemorySession session = MemorySession.openConfined()) {
            
            // Allocate off-heap memory and store unsortedArray in it
            MemorySegment array = session.allocateArray(
                ValueLayout.JAVA_INT,
                unsortedArray);            
            
            // Obtain instance of native linker
            Linker linker = Linker.nativeLinker();
            
            // Create downcall handle for qsort
            MethodHandle qsort = linker.downcallHandle(
                linker.defaultLookup().lookup("qsort").get(),
                FunctionDescriptor.ofVoid(
                    ValueLayout.ADDRESS,
                    ValueLayout.JAVA_LONG,
                    ValueLayout.JAVA_LONG,
                    ValueLayout.ADDRESS)
            );  
            
            // Create method handle for qsortCompare
            FunctionDescriptor compareFunc_sig = FunctionDescriptor.of(
                ValueLayout.JAVA_INT,
                ValueLayout.ADDRESS,
                ValueLayout.ADDRESS);

            MethodHandle comparHandle = MethodHandles.lookup().findStatic(
                Qsort.class, "qsortCompare", Linker.upcallType(compareFunc_sig));

            // Create function pointer for qsortCompare                    
            MemorySegment comparFunc = linker.upcallStub(comparHandle,
                compareFunc_sig, session);            
           
            // Call qsort
            qsort.invoke(array, (long) unsortedArray.length, 4L, comparFunc);

            // Dereference off-heap memory
            sortedArray = array.toArray(ValueLayout.JAVA_INT);
        }
        return sortedArray;    
    }

The following statement allocates off-heap memory, then stores the int array to be sorted in it:

            // Allocate off-heap memory and store unsortedArray in it
            MemorySegment array = session.allocateArray(
                ValueLayout.JAVA_INT,
                unsortedArray);  

The following statements create a downcall method handle for the qsort function:

            // Obtain instance of native linker
            Linker linker = Linker.nativeLinker();
            
            // Create downcall handle for qsort
            MethodHandle qsort = linker.downcallHandle(
                linker.defaultLookup().lookup("qsort").get(),
                FunctionDescriptor.ofVoid(
                    ValueLayout.ADDRESS,
                    ValueLayout.JAVA_LONG,
                    ValueLayout.JAVA_LONG,
                    ValueLayout.ADDRESS)
            ); 

The following class defines the Java method that compares two elements, in this case two int values:

    class Qsort {
        static int qsortCompare(MemoryAddress addr1, MemoryAddress addr2) {
            return Integer.compare(
                addr1.get(ValueLayout.JAVA_INT, 0),
                addr2.get(ValueLayout.JAVA_INT, 0));
        }
    }

In this method, the int values are represented by MemoryAddress objects. A memory address models a reference to a memory location. To obtain a value from a memory address, call one of its get methods. This example calls the get(ValueLayout.OfInt, long), where the second argument is the offset in bytes relative to the memory address's location. The second argument is 0 because the memory addresses in this example store only one value.

The following statement creates a method handle to represent the comparison method Qsort::qsortCompare:

            // Create method handle for qsortCompare
            FunctionDescriptor compareFunc_sig = FunctionDescriptor.of(
                ValueLayout.JAVA_INT,
                ValueLayout.ADDRESS,
                ValueLayout.ADDRESS);

            MethodHandle comparHandle = MethodHandles.lookup().findStatic(
                Qsort.class, "qsortCompare", Linker.upcallType(compareFunc_sig));

The MethodHandles.Lookup::findStatic method creates a method handle for a static method. It takes three arguments:

  • The method's class
  • The method's name
  • The method's type: The first argument of MethodType::methodType is the method's return value's type. The rest are the types of the method's arguments.

The following statement creates a function pointer from the method handle comparHandle:

            // Create function pointer for qsortCompare                    
            MemorySegment comparFunc = linker.upcallStub(comparHandle,
                compareFunc_sig, session);    

The Linker::upcallStub method takes three arguments:

  • The method handle from which to create a function pointer
  • The function pointer's function descriptor; in this example, the arguments for FunctionDescriptor.of correspond to the return value type and arguments of Qsort::qsortCompare
  • The memory session to associate with the function pointer (which is a memory segment)

The following statement calls the qsort function:

            // Call qsort
            qsort.invoke(array, (long) unsortedArray.length, 4L, comparFunc);

In this example, the arguments of MethodHandle::invoke correspond to those of the standard C library qsort function.

Finally, the following statement copies the sorted array values from off-heap to on-heap memory:

            // Dereference off-heap memory
            sortedArray = array.toArray(ValueLayout.JAVA_INT);

Memory Layouts and Structured Access

Accessing structured data using only basic dereferencing operations can lead to hard-to-read code that's difficult to maintain. Instead, you can use memory layouts to more efficiently initialize and access more complicated native data types such as C structures.

For example, consider the following C declaration, which defines an array of Point structures, where each Point structure has two members, Point.x and Point.y:

struct Point {
   int x;
   int y;
} pts[10];

You can initialize such a native array as follows:

        try (MemorySession session = MemorySession.openConfined()) {

            MemorySegment segment =
                MemorySegment.allocateNative(2 * 4 * 10, session);

            for (int i = 0; i < 10; i++) {
                segment.setAtIndex(ValueLayout.JAVA_INT, (i * 2),     i); // x
                segment.setAtIndex(ValueLayout.JAVA_INT, (i * 2) + 1, i); // y
            }
            // ...
        }

The MemorySegment::allocateNative method calculates the number of bytes required for the array. The MemorySegment::setAtIndex method calculates which memory address offsets to write into each member of a Point structure. To avoid these calculations, you can use a memory layout.

To represent the array of Point structures, the following example uses a sequence memory layout:

        try (MemorySession session = MemorySession.openConfined()) {
            
            SequenceLayout ptsLayout
                = MemoryLayout.sequenceLayout(10,
                    MemoryLayout.structLayout(
                        ValueLayout.JAVA_INT.withName("x"),
                        ValueLayout.JAVA_INT.withName("y")));

            VarHandle xHandle
                = ptsLayout.varHandle(PathElement.sequenceElement(),
                    PathElement.groupElement("x"));
            VarHandle yHandle
                = ptsLayout.varHandle(PathElement.sequenceElement(),
                    PathElement.groupElement("y"));            

            MemorySegment segment =
                MemorySegment.allocateNative(ptsLayout, session);
            
            for (int i = 0; i < ptsLayout.elementCount(); i++) {
                xHandle.set(segment, (long) i, i);
                yHandle.set(segment, (long) i, i);
            }
            // ...
        }

The first statement creates a sequence memory layout, which is represented by a SequenceLayout object. It contains a sequence of ten structure layouts, which are represented by GroupLayout objects. The method MemoryLayout::structLayout returns a GroupLayout object. Each structure layout contains two JAVA_INT value layouts named x and y:

            SequenceLayout ptsLayout
                = MemoryLayout.sequenceLayout(10,
                    MemoryLayout.structLayout(
                        ValueLayout.JAVA_INT.withName("x"),
                        ValueLayout.JAVA_INT.withName("y")));

The predefined value ValueLayout.JAVA_INT contains information about how many bytes a Java int value requires.

The next statements create two memory-access VarHandles that obtain memory address offsets. A VarHandle is a dynamically strongly typed reference to a variable, or to a parametrically-defined family of variables, including static fields, non-static fields, array elements, or components of an off-heap data structure.

            VarHandle xHandle
                = ptsLayout.varHandle(PathElement.sequenceElement(),
                    PathElement.groupElement("x"));
            VarHandle yHandle
                = ptsLayout.varHandle(PathElement.sequenceElement(),
                    PathElement.groupElement("y")); 

The method PathElement.sequenceElement() retrieves a memory layout from a sequence layout. In this example, it retrieves one of the structure layouts from ptsLayout. The method call PathElement.groupElement("x") retrieves a memory layout named x. You can create a memory layout with a name with the withName(String) method.

The for statement calls VarHandle::set to dereference memory like MemorySegment::setAtIndex. In this example, it sets a value (the second argument) at an index (the third argument) in a memory segment (the first argument). The VarHandles xHandle and yHandle know the size of the Point structure (8 bytes) and the size of its int members (4 bytes). This means you don't have to calculate the number of bytes required for the array's elements or the memory address offsets like in the setAtIndex method.

            MemorySegment segment =
                MemorySegment.allocateNative(ptsLayout, session);
            
            for (int i = 0; i < ptsLayout.elementCount(); i++) {
                xHandle.set(segment, (long) i, i);
                yHandle.set(segment, (long) i, i);
            }

Restricted Methods

Some methods in the Foreign Function and Memory (FFM) API are unsafe and therefore restricted. If used incorrectly, restricted methods can crash the JVM and may silently result in memory corruption.

If you run an application that invokes one of the following restricted methods, the Java runtime will print a warning message. To suppress this message, add the --enable-native-access=ALL-UNNAMED command-line option.

  • static Linker nativeLinker(), SymbolLookup.libraryLookup(String, MemorySession) and SymbolLookup.libraryLookup(Path, MemorySession): These methods are required to create a downcall method handle, which is intrinsically unsafe. A symbol in a foreign library does not typically contain enough signature information, such as arity and the types of foreign function parameters, to enable the linker at runtime to validate linkage requests. When a client interacts with a downcall method handle obtained through an invalid linkage request, for example, by specifying a function descriptor featuring too many argument layouts, the result of such an interaction is unspecified and can lead to JVM crashes.

    JVM crashes might occur with upcalls because they are typically invoked in the context of a downcall method handle invocation.

  • All dereferencing methods of MemoryAddress: Because a memory address does not feature temporal or spatial bounds, the runtime has no way to check the correctness of the memory dereference operation.
  • MemorySegment::ofAddress and VaList::ofAddress: Sometimes it's necessary to turn a memory address obtained from native code into a memory segment with full spatial, temporal and confinement bounds. To do this, clients can obtain a native segment unsafely from a memory address by providing the segment size as well as the segment session. This is a restricted operation because, for instance, an incorrect segment size could result in a JVM crash when attempting to dereference the memory segment.

Calling Native Functions with jextract

The jextract tool mechanically generates Java bindings from a native library header file. The bindings that this tool generates depend on the Foreign Function and Memory (FFM) API. With this tool, you don't have to create downcall and upcall handles for functions you want to invoke; the jextract tool generates code that does this for you.

Obtain the source code for jextract from the following site:

https://github.com/openjdk/jextract

This site also contains steps on how to compile and run jextract, additional documentation, and samples.

Run a Python Script in a Java Application

The following steps show you how to generate Java bindings from the Python header file, Python.h, then use the generated code to run a Python script in a Java application. The Python script prints the length of a Java string.

  1. Run the following command to generate Java bindings for Python.h:
    jextract -l <absolute path of Python shared library> \
      --output classes \
      -I <directory containing Python header files> \
      -t org.python <absolute path of Python.h>

    Note:

    • On Linux systems, to obtain the file name of the Python shared library, run the following command. This example assumes that you have Python 3 installed in your system.

      ldd $(which python3)
    • On Linux systems, if you can't find Python.h or the directory containing the Python header files, you might have to install the python-devel package.

    • If you want to examine the classes and methods that the jextract tool creates, run the command with the --source option. For example, the following command generates the source files of the Java bindings for Python.h:
      jextract --source \
        --output src \
        -I <directory containing Python header files> \
        -t org.python <absolute path of Python.h>
  2. In the same directory as classes, which should contain the Python Java bindings, create the following file, PythonMain.java:
    import java.lang.foreign.MemoryAddress;
    import java.lang.foreign.MemorySegment;
    import java.lang.foreign.MemorySession;
    import static org.python.Python_h.*;
    import org.python.*;
    
    public class PythonMain {
        
        public static void main(String[] args) {
            String myString = "Hello world!";
            String script = """
                         string = "%s"
                         print(string, ': ', len(string), sep='')
                         """.formatted(myString).stripIndent();
            
            Py_Initialize();
            
            try (MemorySession session = MemorySession.openConfined()) {
                MemorySegment nativeString = session.allocateUtf8String(script);
                PyRun_SimpleStringFlags(
                    nativeString,
                    MemoryAddress.NULL);
                Py_Finalize();
            }
            Py_Exit(0);
        }
    }
  3. Compile PythonMain.java with the following command:
    javac --enable-preview -source 19 \
      -classpath classes \
      PythonMain.java
  4. Run PythonMain with the following command:
    java -cp classes:. -Djava.library.path=<location of Python shared library> PythonMain

Call qsort Function from Java Application

As mentioned previously, qsort is a C library function that requires a pointer to a function that compares two elements. The following steps create Java bindings for the C standard library with jextract, create an upcall handle for the comparison function required by qsort, and then call the qsort function.

  1. Run the following command to create Java bindings for stdlib.h, which is the header file for the C standard library:
    jextract --output classes -t org.unix <absolute path to stdlib.h>

    The generated Java bindings for stdlib.h include a Java class named stdlib_h, which includes a Java method named qsort(Addressable, long, long, Addressable), and a Java interface named __compar_fn_t, which includes a method named allocate that creates a function pointer for the comparison function required by the qsort function. To examine the source code of the Java bindings that jextract generates, run the tool with the -source option:

    jextract --source --output src -t org.unix <absolute path to stdlib.h>
  2. In the same directory where you generated the Java bindings for stdlib.h, create the following Java source file, QsortMain.java:
    import static org.unix.__compar_fn_t.*;
    import static org.unix.stdlib_h.*;
    import java.lang.foreign.*;
    import java.lang.invoke.*;
    
    public class QsortMain {
        
        public static void main(String[] args) {
            
            int[] unsortedArray = new int[] { 0, 9, 3, 4, 6, 5, 1, 8, 2, 7 };
    
            try (MemorySession session = MemorySession.openConfined()) {
                
                // Allocate off-heap memory and store unsortedArray in it
                MemorySegment array = session.allocateArray(
                    ValueLayout.JAVA_INT,
                    unsortedArray);            
    
                // Create upcall for comparison function
                MemorySegment comparFunc = allocate(
                    (addr1, addr2) ->
                        Integer.compare(
                            addr1.get(ValueLayout.JAVA_INT, 0),
                            addr2.get(ValueLayout.JAVA_INT, 0)),
                        session);
               
                // Call qsort
                qsort(array, (long) unsortedArray.length, 4L, comparFunc);      
    
                // Dereference off-heap memory
                int[] sortedArray = array.toArray(ValueLayout.JAVA_INT);
    
                for (int num : sortedArray) {
                    System.out.print(num + " ");
                }
                System.out.println();        
            }
        }
    }

    The following statement creates an upcall, comparFunc, from a lambda expression:

                // Create upcall for comparison function
                MemorySegment comparFunc = allocate(
                    (addr1, addr2) ->
                        Integer.compare(
                            addr1.get(ValueLayout.JAVA_INT, 0),
                            addr2.get(ValueLayout.JAVA_INT, 0)),
                        session);

    Consequently, you don't have to create a method handle for the comparison function as described in Upcalls: Passing Java Code as a Function Pointer to a Foreign Function.

  3. Compile QsortMain.java with the following command:
    javac --enable-preview -source 19 -cp classes QsortMain.java  
  4. Run QsortMain with the following command:
    java --enable-preview -cp classes:. QsortMain