Invoke a C Library Function
Overview
As Foreign Function and Memory API can help you with invoking native code, this tutorial covers how to call a C library function from Java.
Consider the strdup
C standard library function:
char *strdup(const char *s);
This function takes one argument, a string s
, and returns a pointer to a null-terminated byte string.
The return type of this function is a duplicate of the string pointed to by s
. To call strdup
from a Java application you should follow these steps:
- Allocate off-heap memory for the
strdup
function's argument and store the Java string in the off-heap memory that you allocated. - Obtain an instance of the native linker.
- Locate the address of the C function and describe the C function signature.
- Build and then call a method handle that points to the
strdup
function.
Allocate Off-Heap Memory For C Function Arguments
You can use a MemorySegment
with a confined arena to allocate off-heap memory and copy the Java string into it.
String pattern = "********"
try (Arena arena = Arena.ofConfined()) {
MemorySegment nativeString = arena.allocateFrom(pattern);
}
Next, let's figure out how to pass the MemorySegment
to the native function.
Obtain an Instance of the Native Linker
To get access to the native C library you can utilize the native linker. The native linker has a double role: provide access to foreign functions from Java code and access to Java code from foreign functions.
Linker linker = Linker.nativeLinker();
To call a native method like strdup
, 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.
Locate the Address of the C Function
There are different ways to locate the address of a C function:
- You can locate all the symbols in a user-specified native library by using native linker's default lookup (
Linker.defaultLookup()
). - Call
SymbolLookup.loaderLookup(String)
to find symbols in libraries that are loaded withSystem.loadLibrary(String)
. - Call
SymbolLookup.libraryLookup(String, Arena)
to create a symbol lookup from the name of a library.
SymbolLookup stdLib = SymbolLookup.libraryLookup("libc.so.6", arena);
MemorySegment strlen_addr = stdLib.find("strdup").get();
The SymbolLookup.libraryLookup(String, Arena)
method loads the specified library and associates it with an arena, which controls the symbol lookup's lifetime.
Because strdup
is part of the C standard library, you don't have to specify a system-dependent library file name and use the native linker's default lookup:
// Locate the address of the C function signature
SymbolLookup stdLib = linker.defaultLookup();
MemorySegment strdup_addr = stdLib.find("strdup").get();
Now that you located the C function address, let's see how its signature translates to Java.
Describe the C Function Signature
The downcall method handle also requires a description of the native function's signature, expressed by a FunctionDescriptor
instance.
A function descriptor represents the layouts of the native function's arguments and its return value, if any.
Each layout in a function descriptor maps to a Java type, which is the type needed when invoking the resulting downcall method handle.
Most value layouts map to a Java primitive type. For example, ValueLayout.JAVA_FLOAT
maps to a float value.
Warning: Native primitive types are modeled using value layouts whose size matches that of such types. But, a function descriptor is platform-specific. For example,
size_t
has a layout ofJAVA_LONG
on 64-bit or x64 platforms but a layout of JAVA_INT on 32-bit or x86 platforms.
If you wish to identify the layout of a native primitive type that the native linker uses for your platform, call the method Linker.canonicalLayouts()
.
The subsequent arguments of FunctionDescriptor.of(MemoryLayout,MemoryLayout...)
are the layouts of the native function's arguments.
When dealing with pointers as arguments or return types, you should use ValueLayout.ADDRESS
.
Composite types such as struct and union types are modeled with the GroupLayout
interface, which is a supertype of StructLayout
and UnionLayout
.
To create a FunctionDescriptor
for char *strdup(const char *s)
you need:
- the return layout matching a pointer to a null-terminated byte string. This means that you need a
ValueLayout.ADDRESS
that can represent the byte string. Since you need to match a layout for the byte string, you can represent that withMemoryLayout.sequenceLayout(java.lang.Long.MAX_VALUE, JAVA_BYTE)
where the first parameter is the sequence element count, and the second one is the sequence element layout. - the argument layout similar to the return layout, so the same constructs will apply to it as well.
With the previously described elements, you can now create the function descriptor for the strdup
function:
var layout = MemoryLayout.sequenceLayout(Long.MAX_VALUE, JAVA_BYTE);
FunctionDescriptor strdup_sig = FunctionDescriptor.of(
ValueLayout.ADDRESS.withTargetLayout(layout),
ValueLayout.ADDRESS.withTargetLayout(layout)
);
Build a Method Handle that Points to the C Function
To create a downcall method handle for the strdup
function you have to pass its address and function descriptor as follows:
MethodHandle strdup_handle = linker.downcallHandle(strdup_addr, strdup_sig);
Call the C Function from Java
The final step is to invoke the method handle with the memory segment that contains the function's argument:
String pattern = "********"
try (Arena arena = Arena.ofConfined()) {
MemorySegment nativeString = arena.allocateFrom(pattern);
// ...
MemorySegment duplicatedAddress = (MemorySegment) strdup_handle.invokeExact(nativeString);
}
Warning: The
MethodHandle.invokeExact(Object...)
allows any caller type descriptor, but requiring an exact type match. The symbolic type descriptor when you callinvokeExact
must exactly match this method handle's type.
To get access to the entire duplicated String value you can further call MemorySegment.getString(offset)
where
the offset
value is 0.
String pattern = "********"
try (Arena arena = Arena.ofConfined()) {
MemorySegment nativeString = arena.allocateFrom(pattern);
// ...
MemorySegment duplicatedAddress = (MemorySegment) strdup_handle.invokeExact(nativeString);
duplicatedAddress.getString(0);
}
Let's gather all the steps from this tutorial in a snippet that prints a triangle:
import java.lang.foreign.*;
import java.lang.invoke.MethodHandle;
import static java.lang.foreign.ValueLayout.JAVA_BYTE;
public class NativeTriangle {
public static void main(String[] args) throws Throwable {
String pattern = (args.length > 0) ? args[0] : "********";
invokeStrdup(pattern);
}
private static void invokeStrdup(String pattern) throws Throwable {
try (Arena arena = Arena.ofConfined()) {
// Allocate off-heap memory and
// copy the argument, a Java string, into off-heap memory
MemorySegment nativeString = arena.allocateFrom(pattern);
// Obtain an instance of the native linker
Linker linker = Linker.nativeLinker();
// Locate the address of the C function signature
SymbolLookup stdLib = linker.defaultLookup();
MemorySegment strdup_addr = stdLib.find("strdup").get();
// Create a description of the C function
var layout = MemoryLayout.sequenceLayout(Long.MAX_VALUE, JAVA_BYTE);
FunctionDescriptor strdup_sig = FunctionDescriptor.of(
ValueLayout.ADDRESS.withTargetLayout(layout),
ValueLayout.ADDRESS.withTargetLayout(layout)
);
// Create a downcall handle for the C function
MethodHandle strdup_handle = linker.downcallHandle(strdup_addr, strdup_sig);
// Call the C function directly from Java
MemorySegment duplicatedAddress = (MemorySegment) strdup_handle.invokeExact(nativeString);
for (int i = pattern.length() - 1; i >= 0; i--) {
System.out.println(duplicatedAddress.getString(i));
}
}
}
}
Verify your work by running this program from the command line with:
java NativeTriangle.java
More Learning
Last update: December 17, 2024