Using C Libraries in Java 2: Functions with Modifiable Parameters

Java's Foreign Function & Memory API offers much simpler access to functions in C libraries than the outdated JNI.

listen Print view
API symbol surrounded by numerous widgets

(Image: SWstock / Shutterstock.com)

8 min. read
By
  • Rudolf Ziegaus
Contents

Java's Foreign Function & Memory API (FFM) is used to access code in a shared library or DLL written in a programming language like C or Rust. However, the code must meet certain prerequisites. This three-part series of articles uses a demo library written in C to show how a Java application calls the library's functions, what preparations are necessary, and which rules must be observed.

Rudolf Ziegaus

Rudolf Ziegaus is a software developer, Java trainer, and managing director of IO Software GmbH. His favorite topics are PKI, cryptography, and low-level programming.

After the first part showed how to load a shared library written in C in Java and call simple functions of this shared library, we now turn to more complex scenarios. It shows how to call functions with modifiable parameters from Java and how to pass arrays and structures.

C-Libraries in Java nutzen

The examples so far have kept the calls to native functions simple. The Java application merely passed parameters and accepted the return value.

The next examples are different. First, there is the simple C function getVersion2, which, like the function getVersion from part 1, determines the library's version. However, the new function does not return the version number as a value but modifies a parameter. In C, this works by an application passing not the value itself but its address (Call by Reference) for a parameter. This construct looks like this in C:

EXPORT void   getVersion2(int* version);

The following Java code calls the function:

int version;
getVersion2(&version);

In C, the & indicates that the function uses the variable's address. Java does not allow this approach, so a return value is essential. The following Java method uses the C function with a reference:

public int getVersion2() throws Throwable
{
  MethodHandle method = getMethodHandle("getVersion2",   
     FunctionDescriptor.of(
				ValueLayout.JAVA_INT, 
				ValueLayout.ADDRESS
				));
  try (Arena arena = Arena.ofConfined())
  {
    MemorySegment versionSeg =   
      arena.allocate(ValueLayout.JAVA_INT.byteSize());
    method.invoke(versionSeg);
    int version = versionSeg.get(ValueLayout.JAVA_INT, 0);
    return version;
  }
}

First, as in the first part of the series, the code calls the method getMethodHandle() again. The call defines the FunctionDescriptor for the function getVersion2().

The specification ValueLayout.ADDRESS for the parameter indicates that the C function expects an address.

Videos by heise

Now comes the more exciting part: To pass an address, the Java application must reserve a memory area of four bytes (for the data type int) using the FFM API. This is done with an arena, which was already explained in the first part. Creating the arena with the try-with-resources statement ensures that the arena is automatically closed after the try block and the memory managed within it is automatically released. There are different types of arenas – the one created in the example via ofConfined ensures that the application can only access memory of the current thread. An arena created with ofConfined() – or the memory allocated with it – is therefore not thread-safe.

Next, the required memory area for the parameter version must be allocated. For this, the arena has the method allocate(). The size of the required memory can be determined by the function byteSize() for the variable. It should be noted again here that the value represents the size of the Java data type and does not necessarily say anything about the C data type. Since the C function accepts an int parameter, we are on the safe side, as int in C always comprises four bytes. In contrast, the size of a long value in C depends on the platform.

The memory area is represented by a MemorySegment, which must be passed to the C function when calling the method invoke.

Subsequently, the application can read the result. To achieve this, it calls the function get on the MemorySegment and passes it the memory layout (in this case, a JAVA_INT) and the offset for reading from the MemorySegment. For the example, the offset is zero. By specifying JAVA_INT, the function returns an int value that the application can further process.

The next task builds on the previous approach but does not just process one value; instead, it calculates the average of a list of int values. For this, it must pass an array of int values to the native function:

public double calcAverage(int [] values) throws Throwable
{
  MethodHandle calcAverage =  
     getMethodHandle("calcAverage"),
	    FunctionDescriptor.of(
	    ValueLayout.JAVA_DOUBLE,     // return value 
	    ValueLayout.ADDRESS,         // data values 
	    ValueLayout.JAVA_INT));      // number of elements

  try(Arena arena = Arena.ofConfined())  
  {
    long totalSize = ValueLayout.JAVA_INT.byteSize() * values.length;
    MemorySegment valueSegment = arena.allocate(totalSize);
    for (int i = 0; i < values.length; i++) 
    {
      valueSegment.setAtIndex(ValueLayout.JAVA_INT, i, values[i]); 
    }
    double result = (double) calcAverage.invoke(valueSegment, 
                                                values.length);
    return result;
  }
}

First, the code calculates the total memory size of the array (totalSize) and reserves the required memory with allocate(). Then, the code populates the memory with the method setAtIndex for the respective MemorySegment. The call is made for each element of the array.

Finally, the code calls the method invoke for the MethodHandle and passes the array and its length as parameters. It then returns the result of the C function.

Don't miss any news – follow us on Facebook, LinkedIn or Mastodon.

This article was originally published in German. It was translated with technical assistance and editorially reviewed before publication.