Arguably one of the biggest drawbacks of Java is its inability to call and interact with native C/C++ code easily. Project Panama is a WIP initiative to improve this major drawback by making native C/C++ bindings easy to extract and by creating a “foreign” memory API to easy interact with native C/C++ code.

This blog post is a personal account of me trying it out and providing some feedback. A bit of a disclaimer: I’ve never actually written any C/C++ code before, so don’t necessarily assume what i’m doing is 100% correct. I’m also a hobbyist programmer, not a professional one. This is also my first time working with native bindings. Take everything with a grain of salt. That said, the API(s) are pretty straightforward and easy to use once you figure out initial first-time problems.

Why Do I Need Project Panama?

First, i’d like to talk about my personal use case for Project Panama since it explains why I’m using it for what I am. I’ve made my own overclocking utility for Nvidia GPUs under Linux called Goliath Envious FX and the corresponding abstraction layer API Goliath Envious. Goliath Envious(the focus here) provides a nice, clean way of getting important GPU data. Basic usage of Goliath Envious looks a little like this:

// pre-init NvSMI attributes.

NvSMI.init();



// GPU object representing the primary GPU(GPU-0)

NvGPU primaryGPU = NvGPU.getPrimaryNvGPU();



System.out.println("GPU ID is " + primaryGPU.getIDNvReadable().getValue());

System.out.println("GPU Name is " + primaryGPU.getNameNvReadable().getValue());

System.out.println("GPU Family is " + primaryGPU.getFamilyNvReadable().getValue());



// NvSMI = nvidia-smi.

NvSMI smi = NvSMI.getPrimaryNvGPUInstance();



System.out.println("GPU Utilization is " + smi.getCoreUtilization().getValue());

System.out.println("GPU Temp is " + smi.getCoreTemp().getValue());

System.out.println("GPU Default PowerLimit is " + smi.getDefaultPowerLimit().getValue());

It’s very simple and easy to use. Problem is(besides my lack of documentation, intertwining code with JavaFX, API potholes, among other issues) is that it uses the command line just like you would by opening a terminal and using nvidia-smi(and nvidia-settings) yourself, which is very bad.

(Technically nvidia-smi isn’t completely terrible but nvidia-settings absolutely is. Nvidia-settings causes major CPU usage problems and is extremely latent. Considering this is a gaming related application and will be open while gaming, that’s a huge problem.).

The solution? Use Project Panama in order to utilize the native C/C++ programming language NVML and NvXCtrl APIs, (hopefully) bypassing command line performance penalties and restrictions.

Note: I’m only doing NVML here since I can’t currently get jextract to spit out bindings for NvXCtrl due to int64_t errors.

Using JExtract To Get The Bindings

First thing to do is to actually extract the bindings. This is done via jextract, which you can find other examples on how to use it here. The first thing you’l notice with it is how easy all the arguments kind of just blend in and become command line switch soup. Worse yet, the descriptions for these switches are poorly documented nor do they provide examples:

jextract

Non-option arguments:

[String] -- header files Option Description

------ -----------

-?, -h, --help print help

-C <String> pass through argument for clang

-I <String> specify include files path

-L, --library-path <String> specify library path

-d <String> specify where to place generated class files

--dry-run parse header files but do not generate output jar

--exclude-headers <String> exclude the headers matching the given pattern

--exclude-symbols <String> exclude the symbols matching the given pattern

--include-headers <String> include the headers matching the given pattern.

If both --include-headers and --exclude-

headers are specified, --include-headers are

considered first.

--include-symbols <String> include the symbols matching the given pattern.

If both --include-symbols and --exclude-

symbols are specified, --include-symbols are

considered first.

-l <String> specify a library

--log <String> specify log level in java.util.logging.Level name

--missing-symbols <String> action on missing native symbols. --

missing_symbols=error|exclude|ignore|warn

--no-locations do not generate native location information in

the .class files

-o <String> specify output jar file or jmod file

--package-map <String> specify package mapping as dir=pkg

--record-library-path tells whether to record library search path in

the .class files

--src-dump-dir <String> specify output source dump directory

--static-forwarder <Boolean> generate static forwarder class (default is true)

-t, --target-package <String> target package for specified header files

This is just an EA build so of course this can and probably will improve in the future and they do provide example usage(above) but IMO, if you want people to use Project Panama and provide feedback on it, you need to provide some better documentation. Right now you practically need to go down the list of example usage(again, above) and take guesses as to what they actually do based on those example.

Pro Tip: If you get some warning that some library names couldn’t be resolved, you got it wrong.

In many examples given they use:

/usr/lib/x86_64-linux-gnu

Which doesn’t exist in Arch Linux(what I use). Presumably this is a symbolic link to a default GCC version which only exists in Ubuntu. In Arch Linux this:

/usr/lib/gcc/x86_64-pc-linux-gnu/9.1.0

…is the current GCC path.

Likewise the -L option is either misleading or Java’s failed library loading exception is bugged because directories specified here aren’t added to java.library.path according to the exception message.

Also dynamically specifying and loading libraries isn’t supported? Why?

And another thing, you don’t use the actual file name of a given library… at least not with NVML. I don’t know if this is Java’s fault or what here, but it was horribly confusing. I spent way longer than i’d like to admit trying to figure out why Java couldn’t find the library only to skim down and find the OpenGL example:

jextract -L /usr/lib/x86_64-linux-gnu -l glut -l GLU -l GL --record-library-path -t opengl -o opengl.jar /usr/include/GL/glut.h

…wait, GL? There is no GL library under /usr/lib, only libGL.

What?

Yeah, you have to remove the “lib” part in front. The thing is, some libraries under /usr/lib don’t have “lib” attached to the front like flatpak. What happens when you use them? Don’t know. Maybe the world explodes or something.

Honestly, a configuration file that can be used to consistently extract bindings easily is really recommended here. Maybe use Java 11’s single Java source code compilation feature to automate binding extraction.

(insert some Javascript joke here)

With that all out of the way, how do you extract NVML? Thankfully it’s easy:

jextract -t org.nvidia -L /usr/lib -lnvidia-ml --record-library-path /opt/cuda/targets/x86_64-linux/include/nvml.h -o org.nvidia.nvml.jar

Note: you need CUDA installed to make the bindings but can be removed afterwards since the library(nvidia-ml) should come with the driver. You only need it for the header file.

The Foreign Memory API

The hard part is over. Now for the good stuff, using the bindings. The good news is that NVML is incredibly well documented to the point that, even as someone who doesn’t know C/C++, it was easy to figure out how to use the API. Panama’s foreign API helps a lot here too as it is also mostly pretty easy to use and figure out as well. The main classes to be concerned with are:

Scope — used to allocate memory from within Java. The purpose or usefulness of the fork/merge methods aren’t entirely clear.

Pointer — acts as an object wrapper go between Java and C code. Pointers are either filled with data by the function you pass it to or have data set in the from within Java and passed to API functions. Its usage is somewhat like Optional.

LayoutType — acts as primitive data models for C/C++ memory, such as ints, booleans, structs, etc.

NativeTypes — provides a wide variety of primitive C/C++ data types, such as int, short, char, etc.

API binding data is split into two parts, nvml_h(enums and structs) and nvml_api(functions and constants). I’m not copying/pasting API docs here, you’l have to read them for yourself. I don’t feel like getting sued for copyright infringement.

First, we need to load and initialize the NVML library:

package goliath.nvtest; import java.foreign.Libraries;

import java.foreign.Scope;

import java.foreign.memory.LayoutType;

import java.foreign.memory.Pointer;

import java.lang.invoke.MethodHandles;

import org.nvidia.nvml_h;

import java.foreign.NativeTypes;

import org.nvidia.nvml_lib; public class GoliathNvTest

{

public static void main(String[] args)

{

// Load the NVML library. Note: actualy name is libnvidia-ml.so.

Libraries.loadLibrary(MethodHandles.lookup(), "nvidia-ml");



try(Scope s = nvml_lib.scope().fork())

{

// initialize Nvidia Management Library

nvml_lib.nvmlInit_v2();

}

}

}

From this point forward all code snippets are taking place from within the try statement.

Getting GPU Count

Before we should even ask NVML anything related to our connected Nvidia GPU, we should ask how many GPUs are connected first. To do that, we just need to do:

// Pointer reference to the returned GPU count

Pointer<Integer> count = s.allocate(NativeTypes.INT);



// call and send the count pointer to nvmlDeviceGetCount function. count is "filled" by the function.

nvml_lib.nvmlDeviceGetCount_v2(count);



// print GPU count

System.out.println("Number of GPUs connected: " + count.get());

Now that we have a count of how many GPUs are connected, we can iterate over these Nvidia GPUs in a for loop later if we wanted too.

Getting GPU References

Not hard at all. The next bit, actually getting GPU references, is a little bit more complicated:

// Pointer reference to the GPU struct

Pointer<Pointer<nvml_h.nvmlDevice_st>> gpuPtr = s.allocate(LayoutType.ofStruct(nvml_h.nvmlDevice_st.class).pointer());



// call and send the gpuPtr to nvmlDeviceGetHandleByIndex function.

nvml_lib.nvmlDeviceGetHandleByIndex_v2(count.get()-1, gpuPtr);

Basically we have a struct type inside a struct type, so two pointers are needed with one wrapping the other.

Getting GPU’s name — What’s in a name?

Obviously the next thing to do is to print the GPU’s name. Unfortunately, I couldn’t get this work:



// Name reference to GPU-0's name

Pointer<Byte> namePtr = s.allocateCString("");



// fill namePtr

nvml_lib.nvmlDeviceGetName(gpuPtr.get(), namePtr, nvml_lib.NVML_DEVICE_NAME_BUFFER_SIZE);

Problem is that the Pointer is a Byte, not a char array and I have no clue at this time how to convert it. There is probably a lower level way to convert it, but i’m not seeing it. It would be nice if a convenience methods were provided to handle this since this isn’t likely to be an infrequency occurrence.

Getting GPU’s Default Power Limit

Moving on, lets try getting our GPU’s default Power Limit:

// pointer reference to default power limit

Pointer<Integer> powerDefaultPtr = s.allocate(NativeTypes.INT); // fill powerDefaultPtr nvml_lib.nvmlDeviceGetPowerManagementDefaultLimit(gpuPtr.get(), powerDefaultPtr);



// print default power limit

System.out.println("Default power Limit is " + powerDefaultPtr.get()/1000 + "W");

Note: NVML reports milliwatts, so converting it to Watts by dividing by 1000 is used here.

On my GTX 1080 the default Power Limit is 215W and as expected this prints 215W. Everything is working great.

Getting GPU’s Min/Max Power Limits

Now lets try getting the min/max power limits:

// pointer references to min/max power limits

Pointer<Integer> powerMinPtr = s.allocate(NativeTypes.INT);

Pointer<Integer> powerMaxPtr = s.allocate(NativeTypes.INT);



// fill min/max power limit pointers nvml_lib.nvmlDeviceGetPowerManagementLimitConstraints(gpuPtr.get(), powerMinPtr, powerMaxPtr);



// print power limits

System.out.println("Min Power Limit is " + powerMinPtr.get()/1000 + "W");

System.out.println("Max Power Limit is " + powerMaxPtr.get()/1000 + "W");

Again, prints 108W and 280W as expected for my GPU.

Getting GPU’s Utilization

And finally, lets get GPU and Memory utilization. These are stored in a struct and therefor are a little different than the above:

// utilization struct

nvml_h.nvmlUtilization_st utilStructPtr = s.allocate(LayoutType.ofStruct(nvml_h.nvmlUtilization_st.class)).get();



// fill GPU and Memory utilization struct

nvml_lib.nvmlDeviceGetUtilizationRates(gpuPtr.get(), utilStructPtr.ptr());



// print utilization of gpu/memory

System.out.println("GPU Utilization: " + utilStructPtr.gpu$get() + "%");

System.out.println("Memory Bandwidth Utilization: " + utilStructPtr.memory$get() + "%");

Which correctly prints GPU and memory utilization as expected.

Final Thoughts And Wish List

Project Panama is insanely easy to use overall. Even if it doesn’t perform as good as native C to C usage would, it’s still well worth using in my eyes(I haven’t tested performance and I don’t really care). In my case specifically, literally anything is better than the command line and Panama is extremely easy to use once you get jextract right.

I’ll wrap things up with a bullet point list of thoughts while messing with NVML and other software:

jextract’s command line interface should be made less soupy somehow. Maybe environment variables could help here?

jextract’s logging switch seems broken. Attempting to set to “warn” just results in an exception.

jextract’s command line interface needs way better documentation than it is currently, regardless of whether or not it’s a early access build. If you fail at the first hurdle you can’t provide much feedback.

There should be more API convenience methods to handle common char sequences/arrays C/C++ return types. Again, I have no clue what to do with this Byte value.

Again, I can’t get jextract to work with NvXCtrl(NvXCtrl_libs.h actually). It spits out an error about an unknown bool type at first, then if you specify clang headers, it complains about int64_t being unknown despite being defined in stdint.h.

Add support for dynamically loading system libraries. PLEASE.

Provide examples on how to jextract much more larger/complicated projects, like GTK. I tried doing it and it just started complaining about being unable to find GDK.

Stop using var in code documentation and examples. No one should have to drop to an IDE to figure out what the variable type is. Ambiguity has no place in code documentation. This is especially infuriating because, despite big heads at Oracle telling others to use better variable names, you yourselves suck at variable naming. Want an example from Panama examples page?

// call "readline" API

var p = readline(pstr);

Or how about from Project Loom’s documentation:

try (var scope = FiberScope.open()) {

var fiber1 = scope.schedule(() -> "one");

var fiber2 = scope.schedule(() -> "two");

});

“scope” could just as easily be of type Scope as it could be FiberScope. There are plenty of JDK examples as well, i’m sure. This is really annoying. Stop it.