• Uncategorised

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:

  1. Memory allocation: A native method (allocateNativePoint) allocates memory on the native heap for the Point structure.
  2. Data placement: Separate JNI wrapper methods (not shown) are used to set x and y values in the allocated memory.
  3. Function call: The native function takes a long pointer to the allocated memory (pointPtr).
  4. Memory deallocation: The native function explicitly frees the allocated memory using free. A finally block ensures deallocation in Java using freeNativePoint.

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 the DoubleMathFunction 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.

You may also like...