Skip Top Navigation Bar
Current Tutorial
Troubleshoot Calls to Native Library Functions

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:

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 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:

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:

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:

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 or rb to open file for reading.
  • w or wb to truncate to zero length or create file for writing.
  • a or ab to append; open or create file for writing at end-of-file.
  • r+ or rb+ or r+b to open file for reading and writing.
  • w+ or wb+ or w+b to truncate to zero length or create file for update.
  • a+ or ab+ or a+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:

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.

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.

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 28, 2024


Current Tutorial
Troubleshoot Calls to Native Library Functions