Contents

Project Panama from Zero to hero Part 1

Written by: David Vlijmincx

Introduction

In this post I will show you the basics of project Panama and working with foreign functions and Memory. This will be the basis for the next posts. Each one building on the previous one. The goal of the series is to make you proficient with the API, but to also learn you some performance tips and tricks

What is Project Panama?

Project Panama is a new API that allows you to call native code from Java. Meaning you can use it to call C functions from Java. We already have JNI, but the new API is much friendlier and easier to use, as you will see. The performance of Panama is on par or better than JNI. The foreign function and Memory API were added as part of Project Panama and delivered with JEP-454 as was part of JDK 22.

Why use Panama

Java has a great ecosystem of libraries and frameworks. Maven Central is full of libraries that can solve many challenges. But C has a great ecosystem of libraries as well! The advantage of C is that it has many libraries in areas where Java is a bit underrepresented. Take AI, Game Development, and your operating system. One example would be IO_Uring an async IO library for Linux. We could use this library from Java. Or we could use Tensorflow if we wanted to do Machine Learning. All this is possible with the foreign function API using pure Java (if you don't count the C library itself).

Hello, Panama!

So let's get started with the Hello World version of Panama. The idea is to print a string to the console using the C printf function. The idea is to allocate some memory that holds a String and then call the C function. To do that, we need a couple of things, so let’s start with that.

The first thing we need is a C function that takes a String as an argument and prints it to the console. For this I chose the printf function. The signature of the function is int printf ( const char * format, ... );. So let's dissect this signature as we need it later. The signature is basically these three things:

  • The function name
  • The return type
  • The arguments

Now that we know this, we can prepare what we need to call the C function. We need to allocate some memory that holds the string we want to print. For this we will use the Arena. The Arena is a class that can be used to allocate memory. We need to find the symbol of the printf function in the C library. We can do that using the native Linker. The last thing we need is the FunctionDescriptor.

So like I said, the first thing we need is an Arena to manage the native memory. In the next example the Arena is created inside a try-with-resources statement. There are four types of Arena's but let's keep things simple for now. I used the ofConfined arena because it is meant for single threaded use with a clear end.

1
2
3
4
5
6
try (Arena arena = Arena.ofConfined()){
            

} catch (Throwable e) {
    throw new RuntimeException(e);
}

You don't need to use the Try and call close yourself if you want. The Arena will manage the memory for you inside the try and when it closes it will free the memory. This will help you avoid memory leaks. Inside the try we want to allocate native memory that can hold the String we want to print. The Arena has some helper methods that make this easy to do like the allocateFrom(). It takes a String as a Parameter and returns a MemorySegment backed by native memory.

1
MemorySegment helloPanama = arena.allocateFrom("Hello, Panama!");

A MemorySegment is a reference to a block of memory. A MemorySegment can be backed by native memory or Java heap memory. In this case we allocated some native memory. The MemorySegment holds some metadata about the memory like the size and the address.

Now we need to make the call to the native method (a downcall). First let’s find the symbol of the printf function using the Linker.

1
MemorySegment printfSymbol = linker.defaultLookup().findOrThrow("printf");

The Linker is aware of the most common/popular C libraries available on the system. So we can use the defaultLookup method to find the symbol. The findOrThrow method will throw an exception if the symbol is not found. The MemorySegment that is returned holds the address of the symbol.

Next up is the FunctionDescriptor. The FunctionDescriptor is used to describe the signature of the function we want to call. The FunctionDescriptor takes as parameter the return type and the arguments. In this case we want to call the printf function which returns an int and takes a char* as an argument. So we can create the FunctionDescriptor like this:

1
FunctionDescriptor printfSignature = FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS);

The ValueLayout is a class that describes the type of the argument. In this case we want to pass a char* so we use the ADDRESS layout. Because pointers in C are just an address.

Combining all of this, we can create a MethodHandle that can be used to call the printf function. This looks as follows:

1
MethodHandle printf = linker.downcallHandle(printfSymbol, printfSignature);

Now all we need to do is call the printf method with the memorySegment that holds the string we want to print.

1
int value =  (int) printf.invokeExact(helloPanama);

And that's it! This will invoke the C method and print the string to the console. The value we get back from the function is the number of characters printed.

Complete Example

The complete example is shown below:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import java.lang.foreign.*;
import java.lang.invoke.MethodHandle;

public class Main {

    static void main() {
        try (Arena arena = Arena.ofConfined()){
            MemorySegment helloPanama = arena.allocateFrom("Hello, Panama!");

            Linker linker = Linker.nativeLinker();

            MemorySegment printfSymbol = linker.defaultLookup().findOrThrow("printf");
            FunctionDescriptor printfSignature = FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS);

            MethodHandle printf = linker.downcallHandle(printfSymbol, printfSignature);

            int value =  (int) printf.invokeExact(helloPanama);
            System.out.println("value = " + value);

        } catch (Throwable e) {
            throw new RuntimeException(e);
        }
    }
}

This is the complete example of this post. If you followed along it should look like this.

Conclusion

In this first part of the Panama series, we've covered the fundamentals of calling native C functions from Java. We've seen how to allocate native memory using Arena, look up C function symbols with Linker, describe function signatures with FunctionDescriptor, and finally make the actual downcall using a MethodHandle.

While our “Hello, Panama!” example is simple, it demonstrates the core concepts you'll use throughout the series. The beauty of the Foreign Function & Memory API is that it gives you direct, type-safe access to native code without the complexity and boilerplate of JNI.

In the next post, we'll build on this foundation by exploring how to work with C structs and primitive types from Java. We'll see how to map complex C data structures to Java and pass them back and forth between the two worlds. This will give you the tools to work with more realistic C libraries that use structured data.