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.
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 LessonMy 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) APIThe 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:
The Problem With JNIJNI has been around since Java 1.1, and while it works, it has a few critical drawbacks:
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 EvolvedThe 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 APIAs 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 APIIn the next step, statically typed pure-Java access to native code got integrated, again as an incubator feature.
Foreign Function & Memory APIFinally, 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 ExamplesLet me show you how much simpler things have become with a few examples.
Accessing Memory DirectlyHere’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:
main method “clutter” is no longer needed in a simple example like this.Arena manages memory cleanup automatically in the try block.MemorySegment is a simple wrapper around a native memory address.$ java ArenaDemo.javaSetting 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 FunctionsTo 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.javaLength with native library: 17Length 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 ExampleI 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.
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 pixelsInterval: 56 - Generated 250000 pixels$ java FFMPixelBuffer.javaInterval: 5 - Generated 250000 pixelsInterval: 5 - Generated 250000 pixels
* Java 11 approach using BufferedImage and arrays: ~50-60 milliseconds per frame.
* FFM API approach with direct memory access: ~5 milliseconds per frame.
That’s up to 11x performance improvement just by avoiding the overhead of Java object management!
Why the FFM API Matters for Raspberry Pi ProjectsLet 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 ArchitecturePi4J 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:
These plugins get loaded dynamically at runtime, in the “black bar” in this architecture diagram:
The FFM TransformationPi4J now has an almost-ready FFM-based provider, which will be available in V4. The improvements are dramatic:
A Community Success StoryWhat 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 Haustein, Stephen More, Tom 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 PiThe 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:
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 APII 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-SNAPSHOTimport 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 NotesWhile working on the presentation about the FFM API and this article, I noticed a few points worth mentioning.
--enable-native-access flag to acknowledge this and suppress the warning. This may change in future versions of OpenJDK.ConclusionThe 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:
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!
The post The FFM API: How OpenJDK Changed the Game for Native Interactions (And Made Pi4J Better!) appeared first on JVM Advent.