Troubleshoot Calls to Native Library Functions
Invoke Foreign Functions that Return Pointers
Sometimes foreign functions allocate a region of memory, then return a pointer to that region. This is the case of the C standard library function void *malloc(size_t)
that allocates the requested amount of memory, in bytes, and returns a pointer to it.
Invoking a C standard library function like malloc
follows the same steps as in the previous tutorials:
import java.lang.foreign.*;
import java.lang.invoke.*;
String s = "Panama project is cool!";
// Obtain an instance of the native linker
Linker linker = Linker.nativeLinker();
// Locate the address of malloc()
var malloc_addr = linker.defaultLookup().find("malloc").orElseThrow();
// Create a downcall handle for malloc()
MethodHandle malloc = linker.downcallHandle( malloc_addr,
FunctionDescriptor.of(ValueLayout.ADDRESS, ValueLayout.JAVA_LONG)
);
long byteSize = ValueLayout.JAVA_CHAR.byteSize() * (s.length() + 1);
// Invoke malloc(), which returns a pointer
MemorySegment segment = (MemorySegment) malloc.invokeExact(byteSize);
// The size of the memory segment created by malloc()
System.out.println(
"Size, in bytes, of memory segment created by calling malloc.invokeExact(" +
byteSize + "): " + segment.byteSize());
//...
If you run the snippet above in a jshell
session, the output will contain:
Size, in bytes, of memory segment created by calling malloc.invokeExact(48): 0
When you invoke a native function like malloc
, that returns a pointer, the Java runtime has no insight into the size or the lifetime of the memory segment the pointer points to.
In consequence, the FFM API uses a zero-length memory segment to represent the pointer returned by malloc
. The zero-length memory segments are common to represent the following:
- Pointers returned from a foreign function,
- Pointers passed by a foreign function to an upcall,
- Pointers read from a memory segment.
Moreover, if you try to access the content of a zero-length memory segment, the Java runtime will throw an IndexOutOfBoundsException
.
That occurs because the JVM cannot safely access or validate any access operation of a region of memory whose size is unknown.
While not directly accessible, the goal of a zero-length memory segments is to pass them to other pointer-accepting foreign functions.
From a C standard library perspective you can use free
, a function to deallocate memory:
import java.lang.foreign.*;
import java.lang.invoke.*;
String s = "Panama project is cool!";
// Obtain an instance of the native linker
Linker linker = Linker.nativeLinker();
// Locate the address of malloc()
var malloc_addr = linker.defaultLookup().find("malloc").orElseThrow();
// Create a downcall handle for malloc()
MethodHandle malloc = linker.downcallHandle( malloc_addr,
FunctionDescriptor.of(ValueLayout.ADDRESS, ValueLayout.JAVA_LONG)
);
long byteSize = ValueLayout.JAVA_CHAR.byteSize() * (s.length() + 1);
// Invoke malloc(), which returns a pointer
MemorySegment segment = (MemorySegment) malloc.invokeExact(byteSize);
// The size of the memory segment created by malloc()
System.out.println(
"Size, in bytes, of memory segment created by calling malloc.invokeExact(" +
byteSize + "): " + segment.byteSize());
// Locate the address of free()
var free_addr = linker.defaultLookup().find("free").orElseThrow();
// Create a downcall handle for free()
MethodHandle free = linker.downcallHandle(free_addr, FunctionDescriptor.ofVoid(ValueLayout.ADDRESS));
Consumer<MemorySegment> cleanup = s -> {
try {
free.invokeExact(s);
} catch (Throwable e) {
throw new RuntimeException(e);
}
};
Yet, in the example of the JVM the zero-length memory segments are associated with a fresh scope that's always alive. So if the JVM cannot manage the lifetime of a zero bytes memory segment, how can you further work with it?
You can use MemorySegment.reinterpret
methods to safely access zero-length memory segments and attach them to an existing arena.
The arena manages automatically the lifetime of the region of memory backing the segment. A complete code snippet that allocates off-heap memory with malloc
looks like below:
import java.lang.foreign.*;
import java.lang.invoke.*;
String s = "Panama project is cool!";
try (Arena arena = Arena.ofConfined()) {
// Obtain an instance of the native linker
Linker linker = Linker.nativeLinker();
// Locate the address of malloc()
var malloc_addr = linker.defaultLookup().find("malloc").orElseThrow();
// Create a downcall handle for malloc()
MethodHandle malloc = linker.downcallHandle( malloc_addr,
FunctionDescriptor.of(ValueLayout.ADDRESS, ValueLayout.JAVA_LONG)
);
long byteSize = ValueLayout.JAVA_CHAR.byteSize() * (s.length() + 1);
// Invoke malloc(), which returns a pointer
MemorySegment segment = (MemorySegment) malloc.invokeExact(byteSize);
// The size of the memory segment created by malloc()
System.out.println(
"Size, in bytes, of memory segment created by calling malloc.invokeExact(" +
byteSize + "): " + segment.byteSize());
// Locate the address of free()
var free_addr = linker.defaultLookup().find("free").orElseThrow();
// Create a downcall handle for free()
MethodHandle free = linker.downcallHandle(free_addr, FunctionDescriptor.ofVoid(ValueLayout.ADDRESS));
Consumer<MemorySegment> cleanup = s -> {
try {
free.invokeExact(s);
} catch (Throwable e) {
throw new RuntimeException(e);
}
};
var nativeText = segment.reinterpret(byteSize, arena, cleanup);
}
In this example, the MemorySegment.reinterpret(long,Arena,Consumer)
method requires three arguments:
- The number of bytes to resize the memory segment.
- The arena with which to associate the memory segment.
- The action to perform when the arena is closed. In this case, the action is to deallocate the memory referenced by a pointer returned by
malloc
.
To further check how the example works, you can enhance it to access the off-heap memory of nativeText
and run it in a jshell
session:
import java.lang.foreign.*;
import java.lang.invoke.*;
String s = "Panama project is cool!";
try (Arena arena = Arena.ofConfined()) {
// Obtain an instance of the native linker
Linker linker = Linker.nativeLinker();
// Locate the address of malloc()
var malloc_addr = linker.defaultLookup().find("malloc").orElseThrow();
// Create a downcall handle for malloc()
MethodHandle malloc = linker.downcallHandle( malloc_addr,
FunctionDescriptor.of(ValueLayout.ADDRESS, ValueLayout.JAVA_LONG)
);
long byteSize = ValueLayout.JAVA_CHAR.byteSize() * (s.length() + 1);
// Invoke malloc(), which returns a pointer
MemorySegment segment = (MemorySegment) malloc.invokeExact(byteSize);
// The size of the memory segment created by malloc()
System.out.println(
"Size, in bytes, of memory segment created by calling malloc.invokeExact(" +
byteSize + "): " + segment.byteSize());
// Locate the address of free()
var free_addr = linker.defaultLookup().find("free").orElseThrow();
// Create a downcall handle for free()
MethodHandle free = linker.downcallHandle(free_addr, FunctionDescriptor.ofVoid(ValueLayout.ADDRESS));
Consumer<MemorySegment> cleanup = s -> {
try {
free.invokeExact(s);
} catch (Throwable e) {
throw new RuntimeException(e);
}
};
var nativeText = segment.reinterpret(byteSize, arena, cleanup);
// Access off-heap memory
for (int i = 0; i < s.length(); i++ ) {
nativeText.setAtIndex(ValueLayout.JAVA_CHAR, i, s.charAt(i));
}
// Add the string terminator at the end
nativeText.setAtIndex(ValueLayout.JAVA_CHAR, s.length(), Character.MIN_VALUE);
// Print the string
for (int i = 0; i < s.length(); i++ ) {
System.out.print((char)nativeText.getAtIndex(ValueLayout.JAVA_CHAR, i));
}
}
The out in jshell
should be similar to:
s ==> "Panama project is cool!"
Size, in bytes, of memory segment created by calling malloc.invokeExact(48): 0
Panama project is cool!
Check for Native Errors with errno
The code examples from these series do not throw any errors, but some C standard library functions indicate those by setting the value of the library macro errno
.
The value of errno
is also accessible via FFM API.
To capture errno
value as defined by the C standard library you can use Linker.Option.captureCallState(String)
, which you can use to capture certain thread-local variables.
The Linker.Option.captureCallState(String)
saves portions of the execution state immediately after calling a foreign function associated with a downcall method handle.
A C standard library function that sets errno
is fopen
, which opens the file whose pathname is the string pointed to by filename, and associates a stream with it.
FILE *fopen(const char *filename, const char *mode);
The argument mode
points to a string beginning with one of the following values:
r
orrb
to open file for reading.w
orwb
to truncate to zero length or create file for writing.a
orab
to append; open or create file for writing at end-of-file.r+
orrb+
orr+b
to open file for reading and writing.w+
orwb+
orw+b
to truncate to zero length or create file for update.a+
orab+
ora+b
to append; open or create file for update, writing at end-of-file.
If the file is successfully opened, fopen
returns a pointer to the object controlling the stream.
Otherwise, a null pointer is returned, and errno
is set to indicate the error.
To get the error message, you can invoke the C standard library function strerror
, which returns a textual description of the errno
value.
To showcase how errno
and strerror
work, let's call the fopen
function that uses captureCallState("errno")
to obtain error messages:
import java.lang.foreign.*;
import java.lang.invoke.*;
import static java.lang.foreign.MemoryLayout.*;
static void invokeFopen(String path, String mode) throws Throwable {
// Setup handles
Linker.Option ccs = Linker.Option.captureCallState("errno");
StructLayout capturedStateLayout = Linker.Option.captureStateLayout();
VarHandle errnoHandle = capturedStateLayout.varHandle(PathElement.groupElement("errno"));
// log C Standard Library function
Linker linker = Linker.nativeLinker();
SymbolLookup stdLib = linker.defaultLookup();
MethodHandle fopen =linker.downcallHandle(stdLib.find("fopen").orElseThrow(),
FunctionDescriptor.of(ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS),
ccs);
// strerror C Standard Library function
MethodHandle strerror = linker.downcallHandle(
stdLib.find("strerror").orElseThrow(),
FunctionDescriptor.of(ValueLayout.ADDRESS, ValueLayout.JAVA_INT));
// Actual invocation
try (Arena arena = Arena.ofConfined()) {
MemorySegment capturedState = arena.allocate(capturedStateLayout);
MemorySegment location = arena.allocateFrom(path);
MemorySegment openMode = arena.allocateFrom(mode);
var result = (MemorySegment) fopen.invokeExact(capturedState, location, openMode);
if (result.address() == 0) {
// Get more information by consulting the value of errno:
int errno = (int) errnoHandle.get(capturedState, 0);
System.out.println("errno: " + errno); // 2
// Convert errno code to a string message:
String errrorString = ((MemorySegment) strerror.invokeExact(errno))
.reinterpret(Long.MAX_VALUE).getString(0);
System.out.println("errno string: " + errrorString);
}
}
}
Copy and paste the snippet above in a jshell
session and then invoke it with a couple of values.
If you try to open a file at a given location using mode r
, and that file doesn't exist, the fopen
native function will set the value of errno
to 2.
jshell> invokeFopen("duke.txt", "r"); //open a file that doesn't exist
WARNING: A restricted method in java.lang.foreign.Linker has been called
WARNING: java.lang.foreign.Linker::downcallHandle has been called by REPL.$JShell$15 in an unnamed module
WARNING: Use --enable-native-access=ALL-UNNAMED to avoid a warning for callers in this module
WARNING: Restricted methods will be blocked in a future release unless native access is enabled
errno: 2
errno string: No such file or directory
jshell> invokeFopen("duke.txt", "w"); // create a file for writing
jshell> invokeFopen("duke.txt", "r"); // try again to open a file
While intercepting errno
helps you with understanding feedback when using native C functions, you probably noticed the presence of some warnings
when running the previous code snippet. Next section explores the cause of those warnings and their importance.
Restricted Methods
There is another set of methods in the FFM API that are unsafe and therefore restricted. If you run an application that needs to invoke restricted methods, the Java runtime will print a warning message.
jshell> invokeFopen("duke.txt", "r"); //open a file that doesn't exist
WARNING: A restricted method in java.lang.foreign.Linker has been called
WARNING: java.lang.foreign.Linker::downcallHandle has been called by REPL.$JShell$15 in an unnamed module
WARNING: Use --enable-native-access=ALL-UNNAMED to avoid a warning for callers in this module
WARNING: Restricted methods will be blocked in a future release unless native access is enabled
errno: 2
errno string: No such file or directory
If code in a module M
needs to use these restricted methods or any unsafe methods without warnings, you need to enable access to them
by specifying the --enable-native-access=M
command-line option. In case you have multiple modules that require access to restricted methods, use a comma-separated list.
To enable warning-free use for all code on the class path, specify the --enable-native-access=ALL-UNNAMED
option.
Alternatively, you can achieve the same for an executable JAR if you specify in its manifest the attribute Enable-Native-Access: ALL-UNNAMED
.
Warning: You cannot specify a module name as the value of the
Enable-Native-Access
attribute.
Method | Why this method is restricted |
---|---|
SymbolLookup.libraryLookup(String, Arena) , SymbolLookup.libraryLookup(Path, Arena) |
Loading a library always causes execution of native code. For example, on Linux, they can be executed through dlopen hooks. |
MemorySegment.reinterpret(long) , MemorySegment.reinterpret(long, Arena, Consumer) , MemorySegment.reinterpret(Arena, Consumer) |
These methods allows you to change the size and lifetime of an existing segment by creating a new alias to the same region of memory. For example, an application might overestimate the size of the region and use MemorySegment.reinterpret(long,Arena,Consumer) to obtain a segment that's 100 bytes long. Later, this might result in attempts to dereference memory outside the bounds of the region, which might cause a JVM crash or, even worse, result in silent memory corruption. |
Linker.upcallStub(MethodHandle, FunctionDescriptor, Arena, Linker.Option...) |
When you create downcall handles, the linker can't check whether the function pointer you are creating is the correct one for the for the downcall you are passing it to. |
Linker.downcallhandle(FunctionDescriptor, Linker.Option...) , Linker.downcallhandle(MemorySegment, FunctionDescriptor, Linker.Option...) |
Creating a downcall method handle is intrinsically unsafe. A linker has no way to verify that the provided function descriptor is compatible with the function being called. |
AddressLayout.withTargetLayout(MemoryLayout) |
Once you have an address layout with a given target layout, you can use it in a dereference operation (eg. MemorySegment.get(AddressLayout, long) ) to resize the segment being read, which is unsafe. |
java.lang.ModuleLayer.Controller.enableNativeAccess(Module) |
The method enables native access for the specified module if the caller's module has native acces. This method is restricted because it propagates privileges to call restricted methods. |
Last update: December 17, 2024