Java: Calling Go library over JNA
When working with Java, there are times when you need to make use of native libraries written in other languages, like C++, Rust or even Go. Traditionally, Java Native Interface (JNI) has been the go-to solution for these tasks. However, JNI can be complex and cumbersome, requiring significant boilerplate code and deep knowledge of both Java and the target language’s memory management.
Java Native Access (JNA) offers a more streamlined and user-friendly alternative. With JNA, you can directly call native functions from dynamic libraries without the need for extensive coding or manual handling of pointers and memory. This ease of use makes JNA particularly appealing for developers who need to integrate native code into their Java applications but want to avoid the steep learning curve and pitfalls associated with JNI.
In this article, we’ll explore the practical use of JNA by demonstrating how to call methods from a Go library within a Java application.
Part 1. Building the Go Library
In this section, we’ll walk through the process of creating a simple Go library that we’ll later use in our Java application. The Go library will contain a Contains
function that checks whether a specific string exists within a slice of strings:
package main
import "C"
//export Contains
func Contains(values []string, value string) bool {
for _, v := range values {
if v == value {
return true
}
}
return false
}
func main() {
}
The code might look simple and mostly self-explanatory, but there are a few key aspects to highlight.
- The
import “C”
statement is crucial when building Go libraries meant to be used with other languages. This import enables Go to interface with C, allowing us to create functions that can be called from C-compatible languages, like Java via JNA. Essentially, it bridges the gap between Go and the external world. //export Contains
is more than just documentation — it instructs the Go compiler to export theContains
function, making it accessible to other languages, like Java in our case. The//export
directive must be placed immediately before the function definition you intend to export.
This Go code is now ready to be compiled into a shared library that we can call from Java using JNA. In order to do that execute the following build command:
go build -buildmode=c-shared -o bin/library.so main.go
Under the hood it will use the cgo build command. You’ll end up with two important files: a shared object file (.so) and a C header file (.h). These files are essential for integrating the Go library with other programming languages like Java. For the sake of this article I’ve committed both of those files, but generally you shouldn’t do that.
The .so file is a shared object file that contains the compiled code of your Go library. It’s this file that Java, through JNA, will load and interact with. The shared object file is a dynamic library that can be loaded at runtime, making it possible for Java to call the Contains function defined in our Go code.
The .h file is a C header file generated by Go when you use cgo to build the library. This file serves as an interface between your Go code and other languages like C and, by extension, Java. You can check out the complete file over here, in this article however we will only focus on the key parts of it:
extern GoUint8 Contains(GoSlice values, GoString value);
This line declares the Contains
function in a way that can be recognized by other languages. The function expects a GoSlice
and a GoString
as arguments, making it possible to pass Go-like slices and strings from Java to this function via JNA. That’s the function that we will be calling from the Java side.
typedef struct { const char *p; ptrdiff_t n; } _GoString_;
This line defines a type _GoString_
, which is used to represent strings in Go when interfacing with other languages. Go strings are not simple null-terminated C strings; they include both a pointer to the string data p
and the string’s length n
. This ensures that strings are handled correctly even if they contain null bytes.
typedef struct { void *data; GoInt len; GoInt cap; } GoSlice;
This line defines a type GoSlice
, which is used to represent slices in Go. A Go slice includes a pointer to the underlying array data
, its length len
, and its capacity cap
.
These structures are crucial because they ensure that data types like strings and slices, which are native to Go, can be correctly interpreted and used in other languages. Now let’s jump over to the Java side of this project.
Part 2. Building the Java Application
First we’ll create a Java interface called LibraryBridge
that will serve as a bridge between our Java application and the Go library we previously created. This interface will define the structures and methods needed to interact with the Go code using JNA.
import java.util.List;
import com.sun.jna.Library;
import com.sun.jna.Pointer;
import com.sun.jna.Structure;
public interface LibraryBridge extends Library {
class GoSlice extends Structure {
public static class ByValue extends GoSlice implements Structure.ByValue {
}
public Pointer data;
public long len;
public long cap;
@Override
protected List<String> getFieldOrder() {
return List.of("data", "len", "cap");
}
}
class GoString extends Structure {
public static class ByValue extends GoString implements Structure.ByValue {
}
public String p;
public long n;
@Override
protected List<String> getFieldOrder() {
return List.of("p", "n");
}
}
boolean Contains(GoSlice.ByValue values,
GoString.ByValue value);
}
The LibraryBridge
interface extends the Library
interface provided by JNA, which enables us to load and interact with the native shared library generated from our Go code. Let’s break down the code:
- The
GoString
class represents the Go string structure in Java. It matches the_GoString_
struct in the Go library, which includes a pointer to the string data and the length of the string. - The
GoSlice
class represents the Go slice structure in Java. This structure is defined to match theGoSlice
type from the Go library, which includes a pointer to the data, the length of the slice, and its capacity. - The
ByValue
nested class inside ensures that instances will be passed by value to the native function, mimicking the behavior of Go slices and strings. - Both classes have the
getFieldOrder
method that returns a list of the field names in the order they are defined in the Go structure. JNA uses this method to correctly map the Java fields to their corresponding native fields. - Lastly,
Contains
method corresponds to theContains
function in our Go library. The method takes two parameters: aGoSlice.ByValue
representing the slice of strings, and aGoString.ByValue
representing the string to search for within the slice. The method returns a boolean indicating whether the string was found in the slice.
Now that we’ve established the LibraryBridge
interface to interact with the Go library, the next step is to create a class that will load this library into the Java application. This class will handle the process of extracting and loading the shared library (library.so
) at runtime:
import java.io.IOException;
import com.sun.jna.Native;
public class LibraryLoader {
public static LibraryBridge INSTANCE;
static {
loadLib();
}
private static void loadLib() {
try {
final var file = Native.extractFromResourcePath("library.so", LibraryLoader.class.getClassLoader());
INSTANCE = Native.load(file.getAbsolutePath(), LibraryBridge.class);
} catch (final IOException e) {
throw new RuntimeException(e);
}
}
}
In this LibraryLoader
class, the Go library is loaded statically using a static initializer block. By doing so, we ensure that the Go shared library is loaded as soon as the LibraryLoader
class is accessed, typically at the start of the application. This guarantees that the native methods provided by the library are available and ready to be invoked throughout the application’s lifecycle.
In the loadLib()
method, we use the Native.extractFromResourcePath()
function to load the library.so
file. This method works seamlessly with the Maven resources folder structure, which is a common setup in Java projects. With Maven one typically stores resource files, such as native libraries, in the src/main/resources
directory. When you build your project, Maven includes these resources in the classpath, making them accessible at runtime. The Native.extractFromResourcePath(“library.so”, LibraryLoader.class.getClassLoader())
function extracts the library.so
file from the application’s resources and places it in a temporary directory where it can be accessed by the operating system. This is particularly useful because it allows the native library to be packaged within the JAR file, and JNA handles the extraction process automatically. This approach ensures that your native library is always accessible, regardless of the environment in which your application is deployed. Whether running locally during development or as a packaged application in production, Native.extractFromResourcePath
simplifies the process of managing native libraries, making them easy to distribute and use.
Finally, we’ll implement the actual method that interacts with our Go library through the LibraryBridge
interface. This method will leverage JNA to convert Java data types into Go-compatible types, call the Go Contains
function, and return the result. Below is the implementation, followed by a detailed explanation.
import com.sun.jna.Memory;
public class Main {
public boolean containsGo(final List<String> valuesToCheck, final String valueToCheck) {
final var values = convertValues(valuesToCheck);
final var size = values[0].size();
try (final var memory = new Memory((long) values.length * size)) {
var offset = 0L;
for (final var value : values) {
value.write();
memory.write(offset, value.getPointer().getByteArray(0, size), 0, size);
offset += size;
}
final var valuesAsSlice = new LibraryBridge.GoSlice.ByValue();
valuesAsSlice.data = memory;
valuesAsSlice.len = values.length;
valuesAsSlice.cap = values.length;
return LibraryLoader.INSTANCE.Contains(valuesAsSlice, getGoString(valueToCheck));
}
}
private static LibraryBridge.GoString[] convertValues(final List<String> valuesToCheck) {
final var values = (LibraryBridge.GoString[]) new LibraryBridge.GoString().toArray(valuesToCheck.size());
var i = 0;
for (final var pair : valuesToCheck) {
values[i].p = pair;
values[i++].n = pair.length();
}
return values;
}
private static LibraryBridge.GoString.ByValue getGoString(final String value) {
final var valueAsGoString = new LibraryBridge.GoString.ByValue();
valueAsGoString.p = value;
valueAsGoString.n = valueAsGoString.p.length();
return valueAsGoString;
}
}
The first step is to convert the list of Java strings (valuesToCheck
) into an array of GoString
structures, which can be passed to the Go function. This is achieved through the convertValues
method. This method creates an array of LibraryBridge.GoString
structures, where each GoString
contains a pointer to a Java string p
and its length n
. This array mirrors the Go slice structure, making it compatible with our Go library.
After this we need to build a “slice” with the values to send to Go. The Memory
object is used to allocate a block of memory that can store all the strings in the Go slice. This memory block is needed because Go expects a continuous block of memory for its slices, and JNA facilitates this process.
Each string in the array is written into the allocated memory block. The offset
is updated after each string to ensure that each string is stored sequentially in memory. After writing the strings to memory, we create an instance of LibraryBridge.GoSlice.ByValue
. We set the data
field to point to the memory block, and set both len
and cap
fields to the number of elements in the slice. This structure mirrors the Go slice in a way that the Go library can understand.
With all of that we are now ready to call the Contains
function using the static reference to the LibraryBridge
instance.
Part 3. Testing
There are various ways to test this code, in this article we’ve decided to utilise the power of JUnit tests:
import java.util.List;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
public class MainTest {
@Test
void testContains() {
final var valuesToCheck = List.of("value1", "value2", "value3");
final var valueToCheck = "value2";
final var main = new Main();
final var contains = main.containsGo(valuesToCheck, valueToCheck);
Assertions.assertTrue(contains);
}
@Test
void testDoesNotContain() {
final var valuesToCheck = List.of("value1", "value2", "value3");
final var valueToCheck = "missing";
final var main = new Main();
final var contains = main.containsGo(valuesToCheck, valueToCheck);
Assertions.assertFalse(contains);
}
}
With this simple test suite we can assure that the code works correctly and that the Go library is returning expected results.
Afterwards
Integrating Java with native libraries, especially those written in Go, can seem daunting at first. However, as we’ve demonstrated, JNA significantly simplifies this process. With JNA, you can bypass much of the complexity typically associated with JNI, allowing you to focus more on functionality and less on boilerplate code. The relative ease of use that JNA offers makes it an appealing choice for developers looking to extend Java applications with the performance and capabilities of native code.
That said, while JNA is powerful, it’s not without its challenges. Real-world interactions between Java and native libraries often involve dealing with complex data structures, memory management, and the intricacies of different language ecosystems. JNA can abstract away much of this complexity, but it’s important to remember that it doesn’t eliminate it entirely. Issues like performance overhead, debugging difficulties, and the potential for subtle bugs when dealing with pointers and memory are still very much present. Therefore, JNA should be used wisely and where it is appropriate
All of the code can be found at this GitHub repository.