Technology Blogs by SAP
Learn how to extend and personalize SAP applications. Follow the SAP technology blog for insights into SAP BTP, ABAP, SAP Analytics Cloud, SAP HANA, and more.
cancel
Showing results for 
Search instead for 
Did you mean: 
parttimenerd
Product and Topic Expert
Product and Topic Expert
1,182




The Foreign Function & Memory API (also called Project Panama) has come a long way since it started. You can find the latest version implemented in JDK 21 as a preview feature (use --enable-preview to enable it) which is specified by the JEP 454:


By efficiently invoking foreign functions (i.e., code outside the JVM), and by safely accessing foreign memory (i.e., memory not managed by the JVM), the API enables Java programs to call native libraries and process native data without the brittleness and danger of JNI.

JEP 454



This is pretty helpful when trying to build wrappers around existing native libraries. Other languages, like Python with ctypes, have had this for a long time, but Java is getting a proper API for native interop, too. Of course, there is the Java Native Interface (JNI), but JNI is cumbersome and inefficient (call-sites aren't inlined, and the overhead of converting data from Java to the native world and back is huge).



Be aware that the API is still in flux. Much of the existing non-OpenJDK documentation is not in sync.


Example




Now to my main example: Assume you're tired of all the abstraction of the Java I/O API and just want to read a file using the traditional I/O functions of the C standard lib (like read_line.c😞 we're trying to read the first line of the passed file, opening the file via fopen, reading the first line via gets, and closing the file via fclose.


#include "stdio.h"
#include "stdlib.h"

int main(int argc, char *argv[]) {
FILE* file = fopen(argv[1], "r");
char* line = malloc(1024);
fgets(line, 1024, file);
printf("%s", line);
fclose(file);
free(line);
}



This would have involved writing C code in the old JNI days, but we can access the required C functions directly with Panama, wrapping the C functions and writing the C program as follows in Java:


public static void main(String[] args) {
var file = fopen(args[0], "r");
var line = gets(file, 1024);
System.out.println(line);
fclose(file);
}



But do we implement the wrapper methods? We start with the FILE* fopen(char* file, char* mode) function which opens a file. Before we can call it, we have to get hold of its MethodHandle:


private static MethodHandle fopen = Linker.nativeLinker().downcallHandle(
lookup("fopen"),
FunctionDescriptor.of(/* return */ ValueLayout.ADDRESS,
/* char* file */ ValueLayout.ADDRESS,
/* char* mode */ ValueLayout.ADDRESS));



This looks up the fopen symbol in all the libraries that the current process has loaded, asking both the NativeLinker and the SymbolLookup. This code is used in many examples, so we move it into the function lookup:


public static MemorySegment lookup(String symbol) {
return Linker.nativeLinker().defaultLookup().find(symbol)
.or(() -> SymbolLookup.loaderLookup().find(symbol))
.orElseThrow();
}



The look-up returns the memory address at which the looked-up function is located.



We can proceed with the address of fopen and use it to create a MethodHandle that calls down from the JVM into native code. For this, we also have to specify the descriptor of the function so that the JVM knows how to call the fopen handle properly.



But how do we use this handle? Every handle has an invokeExact function (and an invoke function that allows the JVM to convert data) that we can use. The only problem is that we want to pass strings to the fopen call. We cannot pass the strings directly but instead have to allocate them onto the C heap, copying the chars into a C string:


public static MemorySegment fopen(String filename, String mode) {
try (var arena = Arena.ofConfined()) {
return (MemorySegment) fopen.invokeExact(
arena.allocateUtf8String(filename),
arena.allocateUtf8String(mode));
} catch (Throwable t) {
throw new RuntimeException(t);
}
}



We use a confined arena for allocations, which is cleaned after exiting the try-catch. The newly allocated strings are then used to invoke fopen, letting us return the FILE*.



Older tutorials might mention MemorySessions, but they are removed in JDK 21.



After opening the file, we can focus on the char* fgets(char* buffer, int size, FILE* file) function. This function is passed a buffer of a given size, storing the next line from the passed file in the buffer.



Getting a MethodHandle is similar to fopen:


private static MethodHandle fgets = Linker.nativeLinker().downcallHandle(
PanamaUtil.lookup("fgets"),
FunctionDescriptor.of(ValueLayout.ADDRESS,
ValueLayout.ADDRESS,
ValueLayout.JAVA_INT,
ValueLayout.ADDRESS));



Only the wrapper method differs because we have to allocate the buffer in the arena:


public static String gets(MemorySegment file, int size) {
try (var arena = Arena.ofConfined()) {
var buffer = arena.allocateArray(ValueLayout.JAVA_BYTE, size);
var ret = (MemorySegment) fgets.invokeExact(buffer, size, file);
if (ret == MemorySegment.NULL) {
return null; // error
}
return buffer.getUtf8String(0);
} catch (Throwable t) {
throw new RuntimeException(t);
}
}



Finally, we can implement the int fclose(FILE* file) function to close the file:


private static MethodHandle fclose = Linker.nativeLinker().downcallHandle(
PanamaUtil.lookup("fclose"),
FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS));

public static int fclose(MemorySegment file) {
try {
return (int) fclose.invokeExact(file);
} catch (Throwable e) {
throw new RuntimeException(e);
}
}



You can find the source code in my panama-examples repository on GitHub (file HelloWorld.java) and run it on a Linux x86_64 machine via


> ./run.sh HelloWorld LICENSE # build and run
Apache License



which prints the first line of the license file.


Errno




We didn't care much about error handling here, but sometimes, we want to know precisely why a C function failed. Luckily, the C standard library on Linux and other Unixes has errno:


Several standard library functions indicate errors by writing positive integers to errno.

CPP Reference



On error, fopen returns a null pointer and sets errno. You can find information on all the possible error numbers on the man page for the open function.



We only have to have a way to obtain the errno directly after a call, we have to capture the call state and declare the capture-call-state option in the creation of the MethodHandle for fopen:


try (var arena = Arena.ofConfined()) {
// declare the errno as state to be captured,
// directly after the downcall without any interence of the
// JVM runtime
StructLayout capturedStateLayout = Linker.Option.captureStateLayout();
VarHandle errnoHandle =
capturedStateLayout.varHandle(
MemoryLayout.PathElement.groupElement("errno"));
Linker.Option ccs = Linker.Option.captureCallState("errno");

MethodHandle fopen = Linker.nativeLinker().downcallHandle(
lookup("fopen"),
FunctionDescriptor.of(POINTER, POINTER, POINTER),
ccs);

MemorySegment capturedState = arena.allocate(capturedStateLayout);
try {
// reading a non-existent file, this will set the errno
MemorySegment result =
(MemorySegment) fopen.invoke(capturedState,
// for our example we pick a file that doesn't exist
// this ensures a proper error number
arena.allocateUtf8String("nonexistent_file"),
arena.allocateUtf8String("r"));
int errno = (int) errnoHandle.get(capturedState);
System.out.println(errno);
return result;
} catch (Throwable e) {
throw new RuntimeException(e);
}
}



To convert this error number into a string, we can use the char* strerror(int errno) function:


// returned char* require this specific type
static AddressLayout POINTER =
ValueLayout.ADDRESS.withTargetLayout(
MemoryLayout.sequenceLayout(JAVA_BYTE));
static MethodHandle strerror = Linker.nativeLinker()
.downcallHandle(lookup("strerror"),
FunctionDescriptor.of(POINTER,
ValueLayout.JAVA_INT));

static String errnoString(int errno){
try {
MemorySegment str =
(MemorySegment) strerror.invokeExact(errno);
return str.getUtf8String(0);
} catch (Throwable t) {
throw new RuntimeException(t);
}
}



When we then print the error string in our example after the fopen call, we get:


No such file or directory



This is as expected, as we hard-coded a non-existent file in the fopen call.


JExtract




Creating all the MethodHandles manually can be pretty tedious and error-prone. JExtract can parse header files, generating MethodHandles and more automatically. You can download jextract on the project page.



For our example, I wrote a small wrapper around jextract that automatically downloads the latest version and calls it on the misc/headers.h file to create MethodHandles in the class Lib. The headers file includes all the necessary headers to run examples:


#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <stdio.h>



For example the fgets function, jextract generates as an entry point the following:


public static MethodHandle fopen$MH() {
return RuntimeHelper.requireNonNull(constants$48.const$0,"fopen");
}
/**
* {@snippet :
* FILE* fopen(char* __filename, char* __modes);
* }
*/
public static MemorySegment fopen(MemorySegment __filename, MemorySegment __modes) {
var mh$ = fopen$MH();
try {
return (java.lang.foreign.MemorySegment)mh$.invokeExact(__filename, __modes);
} catch (Throwable ex$) {
throw new AssertionError("should not reach here", ex$);
}
}



Of course, we still have to take care of the string allocation in our wrapper, but this wrapper gets significantly smaller:


public static MemorySegment fopen(String filename, String mode) {
try (var arena = Arena.ofConfined()) {
// using the MethodHandle that has been generated
// by jextract
return Lib.fopen(
arena.allocateUtf8String(filename),
arena.allocateUtf8String(mode));
}
}



You can find the example code in the GitHub repository in the file HelloWorldJExtract.java. I integrated jextract via a wrapper directly into the Maven build process, so just mvn package to run the tool.


More Information




There are many other resources on Project Panama, but be aware that they might be dated. Therefore, I recommend reading JEP 454, which describes the newly introduced API in great detail. Additionally, the talk "The Panama Dojo: Black Belt Programming with Java 21 and the FFM API" by Per Minborg at this year's Devoxx Belgium is a great introduction:






As well as the talk by Maurizio Cimadamore at this year's JVMLS:





Conclusion




Project Panama greatly simplifies interfacing with existing native libraries. I hope it will gain traction after leaving the preview state with the upcoming JDK 22, but it should already be stable enough for small experiments and side projects.



I hope my introduction gave you a glimpse into Panama; as always, I'm happy for any comments, and I'll see you next week(ish) for the start of a new blog series.



This article is part of my work in the SapMachine team at SAP, making profiling and debugging easier for everyone. Thank you to my colleague Martin Dörr, who helped me with Panama and ported Panama to PowerPC. This article appeared first on my personal blog mostlynerdless.de.