JVM Advent

The JVM Programming Advent Calendar

The FFM API: How OpenJDK Changed the Game for Native Interactions (And Made Pi4J Better!)

The Pi4J project is a Java library that allows you to control the GPIO pins and electronic components connected to a Raspberry Pi with pure Java code. It removes the complexity of using native libraries and the Java Native Interface (JNI), allowing you to focus on your application logic.

Image

In the Java Advent of 2020, I published “Light up your Christmas lights with Java and Raspberry Pi“, using Java 11 and Pi4J V1.2. Wow, things have changed a lot: this article uses Java 25 and a snapshot of the soon-to-be-released Pi4J V4!

I became a contributor to Pi4J while working on my book “Getting Started with Java on the Raspberry Pi” around 2020. But even after many years of working on Pi4J’s code, I get puzzled when I dive deep into its sources. Please help me, do you understand what’s happening in this piece of code?

JNIEXPORT jobject JNICALL Java_com_pi4j_library_gpiod_internal_GpioD_c_1gpiod_1chip_1open
  (JNIEnv* env, jclass javaClass, jstring path) {
    struct gpiod_chip* chip;
    const char* nativeString = (*env)->GetStringUTFChars(env, path, NULL);
    chip = gpiod_chip_open(nativeString);
    (*env)->ReleaseStringUTFChars(env, path, nativeString);
    if(chip == NULL) {
      return NULL;
    }
    jclass cls = (*env)->FindClass(env, "java/lang/Long");
    jmethodID longConstructor = (*env)->GetMethodID(env, cls, "<init>", "(J)V");
    return (*env)->NewObject(env, cls, longConstructor, (jlong) (uintptr_t) chip);
}

It’s one of the “connection points” between Java and the native libraries to communicate with the GPIO pins. Using JNI and Java Native Access (JNA), Docker build environments are used to compile native libraries for use from Java. This complexity makes it easy for the end user to interact with Java, but difficult for the Pi4J developers to maintain and debug.

In this post, I’ll explain how the Foreign Function & Memory API (FFM API) has revolutionized how Java developers interact with native libraries and memory, significantly simplifying the Pi4J project. This article is based on a talk I gave at the Devoxx and JFall conferences about the history and evolution of this addition to OpenJDK.

A Quick History Lesson

My Java journey started 15 years ago when I switched from C# to Java and never looked back. That was right in the middle of Java’s 30-year history, and I’ve been following its evolution closely ever since. I even had the chance this year to talk to James Gosling, the “Father of Java”, for the Foojay Podcast.

In recent years, we have seen many evolutions in the Java language and virtual machine, such as improved switch-case, virtual threads, performance improvements, and more. One of the most significant recent developments has been Project Panama and the Foreign Function & Memory (FFM) API that emerged from it.

Foreign Function & Memory (FFM) API

The FFM API was officially released in Java 22 as a finalized feature. It represents years of work within Project Panama and has three main goals:

  1. Memory safety: Safe access to off-heap memory while maintaining proper cleanup.
  2. Easy interaction: Simple ways to call native libraries.
  3. High performance: Matching or exceeding JNI’s performance.

The Problem With JNI

JNI has been around since Java 1.1, and while it works, it has a few critical drawbacks:

  • Manual memory management with a steep learning curve.
  • Complex implementation requiring C headers and compilation steps.
  • Hard to use by design: As I learned from Simon Ritter, a Sun engineer once said, JNI was deliberately made difficult to discourage people from using it!
Image

There were attempts to improve this situation with libraries such as JNA and Java Native Runtime (JNR), but they came with their own overhead and limitations.

How The FFM API Evolved

The development of the FFM API is a fascinating story that shows how OpenJDK evolves through careful iteration. The project was broken down into separate JEPs (JDK Enhancement Proposals), which started being delivered in Java 14. As you will see, most of the JEPs were incubator or preview features, which can only be used with the --enable-preview flag, as you can learn in this detailed explanation.

Foreign Memory Access API

As a first step, the OpenJDK team created a new API to access off-heap memory, enabling safe, efficient access to foreign memory. None of these were finalized, as they got integrated into OpenJDK as incubator features, preparing for something bigger.

Foreign Linker API

In the next step, statically typed pure-Java access to native code got integrated, again as an incubator feature.

Foreign Function & Memory API

Finally, the FFM API was finalized in Java 22. It’s a combination of the two previous APIs, and it went through incubator and preview phases across multiple Java releases.

This iterative approach allowed the OpenJDK team to gather community feedback and ensure the API was stable, performant, and truly useful.

Simple Code Examples

Let me show you how much simpler things have become with a few examples.

Accessing Memory Directly

Here’s a basic example of accessing memory directly:

void main() {
    // Open a confined Arena that manages off-heap memory
    // and will release it automatically.
    try (Arena arena = Arena.ofConfined()) {
        // Allocate 5 ints in the arena
        MemorySegment segment = arena
           .allocate(ValueLayout.JAVA_INT, 5);

        // Fill the segment with random values for each int.
        System.out.print("Setting values: ");
        for (int i = 0; i < 5; i++) {
            int randomValue = new Random().nextInt(100);
            segment.setAtIndex(ValueLayout.JAVA_INT, 
               i, randomValue);
            System.out.print(randomValue + " ");
        }
        System.out.println("");

        // Print the values back from memory.
        System.out.print("Reading values: ");
        for (int i = 0; i < 5; i++) {
            System.out.print(segment
              .getAtIndex(ValueLayout.JAVA_INT, i) + " ");
        }
        System.out.println("");
    }
}

Notice a few things about this code:

  • I execute it with Java 25, which means I can use the new simplified main method introduced by JEP 512: Compact Source Files and Instance Main Methods. As a result, I don’t need to use a class and package, and a lot of the main method “clutter” is no longer needed in a simple example like this.
  • The Arena manages memory cleanup automatically in the try block.
  • The MemorySegment is a simple wrapper around a native memory address.
  • No manual memory management is needed.
$ java ArenaDemo.java
Setting values: 16 7 27 50 80 
Reading values: 16 7 27 50 80

A more extended example is available here in FFMMemoryManagement.java, with a Java 11 example here Java11MemoryManagement.java illustrating how much more complex this was before the FFM API. The Java 11 version must be executed with JBang as described in the comments, to force the use of Java 11.

Calling Native Functions

To illustrate how to call a native library function, we’ll make the most complex String.length() implementation 😉 A perfect example of how this can be done with FFM API within one simple-to-read method:

void main() throws Throwable {
    // The text we will use in the demo.
    var text = "Hello JVM Advent!";

    // Obtain a Linker that knows how to call
    // native (C) functions on this platform.
    Linker linker = Linker.nativeLinker();

    // Create a SymbolLookup that can find symbols
    // (like C functions) in the default native libraries.
    SymbolLookup lookup = linker.defaultLookup();

    // Create a MethodHandle to call the native C function
    MethodHandle strlen = linker.downcallHandle(
        // Look up the address of the `strlen` symbol,
        // or throw if it isn't found.
        lookup.find("strlen").orElseThrow(),
        // Describe the C function:
        // - returns long
        // - takes one pointer (address) argument
        FunctionDescriptor.of(ValueLayout.JAVA_LONG,
          ValueLayout.ADDRESS)
    );

    // Open a confined Arena that manages off-heap memory
    // and will release it automatically.
    try (Arena arena = Arena.ofConfined()) {
        // Allocate native memory for a C-style string
        // and copy "Hello World!" into it.
        MemorySegment str = arena.allocateFrom(text);

        // Call the native `strlen` function 
        // via the MethodHandle,
        // passing the string's memory address,
        // and cast the result to a long.
        long length = (long) strlen.invoke(str);

        // Print the result.
        System.out.println("Length with native library: " 
          + length);
    }

    System.out.println("Length with String.length(): " 
      + text.length());
}

No JNI headers, no C-compilation, just pure Java code!

$ java LinkerDemo.java

Length with native library: 17
Length with String.length(): 17

Also, for this example, you can find a more extended example here in FFMNativeCalls.java, with a Java 11 in Java11NativeCalls.java.

Performance Comparison Example

I like demos that have a visual output. So I created a simple benchmark that generates moving gradients, which involves many memory writes. In both Java 11 and 25, I try to refresh the gradient every 5 milliseconds.

Image

The results speak for themselves (tested on a MaOS M2 with Azul Zulu 25):

$ jbang Java11PixelBuffer.java
[jbang] Building jar for Java11PixelBuffer.java...
Interval: 57 - Generated 250000 pixels
Interval: 56 - Generated 250000 pixels

$ java FFMPixelBuffer.java
Interval: 5 - Generated 250000 pixels
Interval: 5 - Generated 250000 pixels

That’s up to 11x performance improvement just by avoiding the overhead of Java object management!

Why the FFM API Matters for Raspberry Pi Projects

Let me first answer a more general question I get asked a lot: “Why Java on a Raspberry Pi?” The answer is simple: Java is the language that allows me to do everything. I can build user interfaces with JavaFX, make API calls to services, and leverage the extensive library ecosystem. When I started experimenting with Java on Raspberry Pi over five years ago, I didn’t want to learn a new language. I wanted to learn how to use and interact with electronic components, using the tools I already knew and loved.

This is where the Pi4J project comes in: it’s a Java library that lets you control the GPIO pins and electronic components connected to a Raspberry Pi. But here’s the catch: this library has always relied on native C/C++ code to communicate with the hardware. Until now, that meant dealing with the complexity of JNI and JNA.

Pi4J Architecture

Pi4J uses a plugin architecture in which different “providers” handle communication with GPIO pins. Previously, these providers relied on native libraries and JNI/JNA, which meant:

  • Complex multi-layered code with up to 5 levels before executing GPIO operations.
  • Need for Docker builds to compile native libraries.
  • Cryptic JNI header generation and wizard-like C code.

These plugins get loaded dynamically at runtime, in the “black bar” in this architecture diagram:

Image

The FFM Transformation

Pi4J now has an almost-ready FFM-based provider, which will be available in V4. The improvements are dramatic:

  • Performance Benchmarks
    • Getting GPIO input state: 10x faster!.
    • SPI communication: Significant improvement, though less dramatic.
  • Code Simplification
    • Direct Java-to-kernel communication.
    • No more complex JNI layers.
    • Readable, maintainable Java code.
    • No need for native library compilation with a complex Docker build process.

A Community Success Story

What makes this even better is that the FFM implementation came from the community. Nick Gritsenko (aka @DigitalSmile) had already created a Java 22 library for GPIO interaction using FFM. When I discovered his work, I asked if we could use it. But even better, he contributed it directly to Pi4J himself and made it fit perfectly into the plugin architecture! This resulted in a major pull request that’s now merged into the Pi4J V4 snapshot.

I’m very proud that this happens again and again. In the past, Alexander Liggesmeyer added support for the Raspberry Pi 5 with a new plugin. And at this moment, Stefan HausteinStephen MoreTom Aarts, and others are actively working on a new Pi4J drivers library and example implementations to make it much easier for everyone to create applications that interact with more complex electronic components, such as joysticks, LCD screens, LED strips, and more…

Beyond Raspberry Pi

The FFM-based implementation opens up exciting possibilities. Since it’s based on standard Linux kernel methods available in Debian, we believe Pi4J could potentially work on:

  • Orange Pi boards and other Linux-based SBCs (Single Board Computers).
  • RISC-V processors running Debian.

I’m looking forward to experimenting with these different hardware platforms! Maybe you’ll read more about that in next year’s JVM Advent…

Pi4J Examples Using the FFM API

I often demo with a CrowPi – a neat kit with a Raspberry Pi and pre-wired components in a single box. It’s great for learning because you can’t wire things incorrectly!

Here’s a simplified example using JBang to blink an RGB LED, using a snapshot build of Pi4J V4. Once this version is released, you can remove the line with //REPOS and update the version.

/// usr/bin/env jbang "$0" "$@" ; exit $?

//REPOS mavencentral,pi4j-snapshots=https://oss.sonatype.org/content/repositories/snapshots
//DEPS com.pi4j:pi4j-core:4.0.0-SNAPSHOT
//DEPS com.pi4j:pi4j-plugin-linuxfs:4.0.0-SNAPSHOT

import com.pi4j.Pi4J;
import com.pi4j.io.gpio.digital.*;

/**
 * Execute with `jbang Pi4JExample.java`
 */
void main() throws InterruptedException {
    var pi4j = Pi4J.newAutoContext();

    var red = pi4j.digitalOutput().create(17);
    var green = pi4j.digitalOutput().create(27);
    var blue = pi4j.digitalOutput().create(22);

    // Blink pattern
    for (int i = 0; i < 10; i++) {
        red.high();
        Thread.sleep(500);
        red.low();
        green.high();
        Thread.sleep(500);
        green.low();
        blue.high();
        Thread.sleep(500);
        blue.low();
    }
}

More examples like this, which can be executed with JBang, are available in the Pi4J JBang repository.

Important Notes

While working on the presentation about the FFM API and this article, I noticed a few points worth mentioning.

  • Memory Safety Warning: When using FFM, you’re stepping outside the “safe JVM-managed garbage collector”. You’ll see a warning about enabling native access with a few of the examples from this article. This is intentional as the OpenJDK team wants to ensure developers understand they’re working with potentially dangerous operations. Use the --enable-native-access flag to acknowledge this and suppress the warning. This may change in future versions of OpenJDK.
  • JEPs Are Worth Reading: If you’re curious about how Java evolves, I highly recommend reading the JDK Enhancement Proposals. They’re not just technical specifications! They’re well-written documents that explain the thinking behind design decisions, include examples, and provide deep insights from the architects of Java. Similarly, read more about the OpenJDK projects, for instance, by subscribing to their mailing list to follow what is happening within such a project.
  • Keep Up With Java Releases: Java has a six-month release cycle, and every release is a good one! Each brings many bug fixes, improvements, and evolutions (from projects and JEPs). If you can update your systems, especially your development systems, you should! The FFM API was introduced in Java 22, but I learned at Devoxx that Java 24 brought significant performance improvements to the FFM implementation, without any API changes! This shows that the OpenJDK team continues to optimize and improve existing features.

Conclusion

The FFM API has been a significant improvement for developers working with native code in recent years. It removes the complexity of JNI while maintaining, and even improving, performance. It opens new possibilities for Java in embedded systems and hardware interfacing. It will also drive closer integration of Artificial Intelligence (AI), Machine Learning (ML), and Large-Language Model (LLM) development with Java.

Feel free to experiment and contribute. OpenJDK and other open source projects, like Pi4J, thrive on community involvement!

If you’re interested in Java on Raspberry Pi, check out:

Image

Remember: We must thank OpenJDK and all its contributors for amazing features like the Garbage Collector, JIT compilation, Lambdas, Virtual Threads, and now the FFM API. Java keeps getting better, and that’s worth celebrating!

Image

Author: Frank Delporte

Author of ‘Getting Started with Java on the Raspberry Pi’ – Java Champion – Software developer and technical writer – Team member Pi4J – Lead coach CoderDojo – Blogging on http://webtechie.be and https://foojay.io/

Next Post

Previous Post

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.

© 2026 JVM Advent | Powered by steinhauer.software Logosteinhauer.software

Theme by Anders Norén