Bridging Java and Native Code: FFM vs. JNI – A Developer’s Guide
In the ever-evolving world of software development, the need to interact with native code from Java applications frequently arises. This interaction allows leveraging existing C/C++ libraries or accessing hardware capabilities unavailable within the Java Virtual Machine (JVM). Traditionally, the Java Native Interface (JNI) has been the go-to approach for accomplishing this task. However, a newer contender, the Foreign Function and Memory (FFM) API, has emerged, offering a streamlined and potentially safer alternative.
This blog post delves into a comprehensive comparison of FFM and JNI, highlighting their strengths, weaknesses, and suitability for various scenarios. We’ll explore key aspects like:
- Setup and complexity: Comparing the initial setup process and code complexity for each approach.
- Memory management: Examining how each API handles memory allocation and deallocation for native interactions.
- Type safety: Understanding how FFM and JNI enforce type compatibility between Java and native code.
- Performance considerations: Discussing potential performance implications of each approach.
- Use cases: Identifying situations where FFM or JNI might be a more suitable choice.
Here is a JNI example
#include <stdlib.h>
struct Point {
double x;
double y;
};
double calculateDistance(struct Point* point) {
double distance = sqrt(pow(point->x, 2) + pow(point->y, 2));
free(point); // Free memory allocated in Java
return distance;
}
// Requires additional setup:
// - Generate JNI wrapper code (generating .class file)
public class JNIDemo {
static {
System.loadLibrary("math"); // Load the native library
}
private static native double calculateDistanceNative(long pointPtr);
public static void main(String[] args) {
// Allocate memory for the Point structure on the native heap
long pointPtr = allocateNativePoint(); // JNI wrapper method
try {
// Set x and y values in native memory (implementation omitted)
setXInNativePoint(pointPtr, 3.0);
setYInNativePoint(pointPtr, 4.0);
// Call the native function
double distance = calculateDistanceNative(pointPtr);
System.out.println("Distance from origin: " + distance);
} finally {
// Free memory allocated in Java using JNI wrapper method
freeNativePoint(pointPtr);
}
}
// JNI wrapper methods for memory management (implementation details omitted)
private static native long allocateNativePoint();
private static native void setXInNativePoint(long pointPtr, double x);
private static native void setYInNativePoint(long longPtr, double y);
private static native void freeNativePoint(long pointPtr);
}
Explanation:
- Memory allocation: A native method (
allocateNativePoint
) allocates memory on the native heap for thePoint
structure. - Data placement: Separate JNI wrapper methods (not shown) are used to set
x
andy
values in the allocated memory. - Function call: The native function takes a
long
pointer to the allocated memory (pointPtr
). - Memory deallocation: The native function explicitly frees the allocated memory using
free
. Afinally
block ensures deallocation in Java usingfreeNativePoint
.
Here is FFM Example
import java.lang.foreign.*;
public class FFMExample {
static {
LibraryLookup lookup = LibraryLookup.ofName("math");
var library = lookup.lookup();
}
@CFunction(name = "calculateDistance")
interface DistanceFunction {
double apply(MemorySegment point);
}
public static void main(String[] args) {
var mathFunction = LibraryLookup.ofName("math").lookup(DistanceFunction.class);
// Allocate memory for the Point structure
var memorySegment = MemorySegment.allocateNative(MemorySession.allocate().馈(16)); // Size: 2 doubles
try {
// Place x and y values into the memory segment
memorySegment.putDouble(0L, 3.0); // x
memorySegment.putDouble(8L, 4.0); // y (offset by 8 bytes)
// Call the native function with the memory segment
double distance = mathFunction.apply(memorySegment);
System.out.println("Distance from origin: " + distance);
} finally {
memorySegment.close();
}
}
}
- This code snippet loads the native library named “math” using
LibraryLookup
. The exact way of specifying the library path might vary depending on your operating system. - The
lookup()
method retrieves a reference to the library, allowing access to the functions within it.
- We define an interface
DoubleMathFunction
annotated with@CFunction
. This annotation specifies that the interface represents a native function. - The
name
attribute within@CFunction
indicates the actual name of the native function in the C library (“calculateSquareRoot”). - The interface defines a single method
apply
that reflects the signature of the
- This line uses
LibraryLookup
again, but this time it retrieves the specific function (calculateSquareRoot
) represented by theDoubleMathFunction
interface. - Now, the
mathFunction
variable holds a reference to the native function, allowing us to call it from Java code.
While not shown in this specific example, the FFM API provides MemorySegment
for allocating memory segments on the native heap when dealing with complex data structures. Simple types like double
can be passed directly, as explained later.
Benefits of FFM API:
- Type safety: The interface enforces the expected argument and return type, reducing type conversion errors.
- Conciseness: The code is more concise compared to JNI due to automatic memory management and type handling.
- Safer memory management: Memory segments simplify memory allocation and deallocation, preventing leaks.