1. Introduction

As we know, Java and Go are two prominent programming languages, each excelling in different domains. Java is renowned for its portability and extensive ecosystem, while Go is celebrated for its simplicity, performance, and efficient concurrency handling. In certain scenarios, combining the strengths of both languages can lead to more robust and efficient applications.

In this tutorial we’ll explore how to invoke Go functions from Java without writing any C code, utilizing the Java Native Access (JNA) library to bridge the gap between the two languages.

2. Bridging Java and Go with JNA

Traditionally, invoking native code from Java required writing Java Native Interface (JNI) code in C, which adds complexity and overhead. However, with the advent of the Java Native Access (JNA) library, it is possible to call native shared libraries directly from Java without delving into C code. This approach simplifies the integration process and allows developers to harness Go’s capabilities seamlessly within Java applications.

To understand how this integration works, first, we’ll explore the role of the Java Native Access (JNA) library in bridging Java and Go. Specifically, JNA provides a straightforward way to call functions in native shared libraries from Java code. By compiling Go code into a shared library and exporting the necessary functions, Java can interact with Go functions as if they were part of its own ecosystem.

In essence, this process involves writing the Go functions, compiling them into a shared library, and then creating corresponding Java interfaces that map to these functions using JNA.

2. Setting Up the Project

Before diving into the implementation, it is essential to set up the project environment properly. This involves configuring the build tools and dependencies required for the integration.
In our case, we need the following components:

  • Java Development Kit (JDK): For compiling and running Java code.
  • Go Programming Language: For writing and compiling Go code.
  • Java Native Access (JNA) Library: Included as a dependency in the Maven project.
  • Build Tools: Maven for Java and the Go compiler for Go code.

Let’s add the JNA library as a maven dependency:

<dependency>
    <groupId>net.java.dev.jna</groupId>
    <artifactId>jna-platform</artifactId>
    <version>5.15.0</version>
</dependency>

3. Invoking Go Function from Java

To demonstrate the integration of Go functions into a Java application, we’ll create a simple program where Java calls a Go function that prints a message to the console. The implementation involves several key steps: writing the Go function, compiling it into a shared library, and then writing the Java code that uses the Java Native Access (JNA) library to invoke the Go function.

3.1. Building Go Shared Library

First, we need to define the Go code properly. To make a Go function accessible from the shared library, we must export it with C linkage. This is achieved by including the import “C” statement and placing the //export directive before the function definition. Additionally, an empty main function is required when building a shared library in Go.

Let’s create a file called hello.go:

package main

/*
#include <stdlib.h>
*/
import "C"
import "fmt"

//export SayHello
func SayHello() {
    fmt.Println("Hello Baeldung from Go!")
}

func main() {}

Let’s highlight the most important parts of the code above:

  • The import “C” statement enables CGO, allowing the use of C features and exporting functions.
  • The //export SayHello directive tells the Go compiler to export the SayHello function with C linkage.
  • The empty main function is necessary when compiling the Go code into a shared library.

After writing the Go function, the next step is to compile the Go code into a shared library. This is done using the Go build command with the -buildmode=c-shared option, which produces a shared library file (e.g., libhello.so on Linux or libhello.dll on Windows).

Depending on the operating system we use we need to execute different commands to compile a shared library.

For Linux-based operating systems, we can use the following command:

go build -o libhello.so -buildmode=c-shared hello.go

For Windows, in order to build a dll library, we apply the next command:

go build -o libhello.dll -buildmode=c-shared hello.go

For macOS, to get a dylib library, we execute the corresponding command:

go build -o libhello.dylib -buildmode=c-shared hello.go

As a result, we should find the shared library in our current directory.

For convenience, all these scripts are included in the source code along with the README.md file.

3.2. Creating the JNA Interface

The next step is to proceed to the Java side of the integration. In the Java application, we utilize the Java Native Access (JNA) library to load the shared library and call the Go function. First, we define a Java interface that extends com.sun.jna.Library, declaring the methods corresponding to the exported Go function:

import com.sun.jna.Library;

public interface GoLibrary extends Library {
    void SayHello();
}

In this interface, GoLibrary extends Library, a marker interface from JNA, and the SayHello method signature matches the exported Go function from the shared library.

Next, we write the Java application that uses this interface to load the shared library and invoke the Go function.

import com.sun.jna.Native;

public class HelloGo {
    public static void main(String[] args) {
        GoLibrary goLibrary = Native.load("hello", GoLibrary.class);
        goLibrary.SayHello();
    }
}

In this code, Native.load loads the shared library libhello.so (omitting the lib prefix and .so extension), creates an instance of GoLibrary to access the exported functions, and calls the SayHello method, which invokes the Go function and prints the message to the console.

When running the Java application, it is important to ensure that the shared library is accessible in the library path. This can be achieved by placing the shared library in the same directory as the Java application or by setting the appropriate environment variables:

For Linux-based systems, we define environment variables by calling the export command:

export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:.

For Windows, we need to add the directory containing the libhello.dll file to the PATH environment variable. This can be done using the following command in Command Prompt (or permanently through the system environment settings):

set PATH=%PATH%;C:\path\to\directory

For macOS, we use a command similar to Linux:

export DYLD_LIBRARY_PATH=$DYLD_LIBRARY_PATH:.

Finally, if everything is setup correctly, we should be able to run our app and get the following output:

Hello Baeldung from Go!

4. Passing Parameters and Returning Values

Building upon the simple example, we can enhance the integration by passing parameters to the Go function and returning values to the Java application. This demonstrates how data can be exchanged between Java and Go.

First, we modify the Go code to include a function that adds two integers and returns the result. In the Go code, we define the AddNumbers function to accept two integers of type C.int and return a C.int.

//export AddNumbers
func AddNumbers(a, b C.int) C.int {
    return a + b
}

After updating the Go code, we need to recompile the shared library to include the new function.

Next, we update the Java interface to include the AddNumbers function. We define an interface corresponding to the Go function, specifying the method signature with appropriate parameters and return types.

public interface GoLibrary extends Library {
    void SayHello();
    int AddNumbers(int a, int b);
}

As a result, we can call the AddNumbers function and pass int parameters:

public static void main(String[] args) {
    GoLibrary goLibrary = Native.load("hello", GoLibrary.class);
    int result = goLibrary.AddNumbers(2, 3);
    System.out.printf("Result is %d%n", result);
}

After running the application we should see the result of the calculation in the output:

Result is 5

5. Handling Complex Data Types

In addition to simple data types, it is often necessary to handle more complex data types, such as strings. To pass a string from Java to Go and receive a string back, we need to handle pointers to C strings in the Go code and map them appropriately in Java.

First, we’ll implement a Go function that accepts a string and returns a greeting message. In the Go code, we define the Greet function, which accepts a *C.char and returns a *C.char. We use C.GoString to convert the C string to a Go string and C.CString to convert the Go string back to a C string.

//export Greet
func Greet(name *C.char) *C.char {
    greeting := fmt.Sprintf("Hello, %s!", C.GoString(name))
    return C.CString(greeting)
}

After adding the new function, we need to recompile the shared library.

Next, we need to update the Java interface to include the Greet function. Since the Go function returns a C string, we’ll use the Pointer class provided by JNA to handle the returned value.

public static void main(String[] args) {
    GoLibrary goLibrary = Native.load("hello", GoLibrary.class);
    Pointer ptr = goLibrary.Greet("Alice");
    String greeting = ptr.getString(0);
    System.out.println(greeting);
    Native.free(Pointer.nativeValue(ptr));
}

In this code, the Greet method is called with the argument “Alice,” the returned Pointer retrieves the string, and Native.free releases the memory allocated by the Go function.

If we run the app, we should probably get the following result:

Hello, Alice!

6. Conclusion

By following the guidelines in this tutorial, we can easily integrate Go functions into a Java application using the Java Native Access (JNA) library without writing any C code. This method combines Go’s performance and concurrency with Java, streamlining integration and speeding up development.

Key factors include ensuring compatibility between Java and Go data types, properly managing memory to avoid leaks, and setting up robust error handling. By combining Java’s ecosystem with Go’s performance optimizations, developers can create efficient and powerful applications.

JNA offers several advantages over JNI, such as eliminating the need for C code, supporting cross-platform development, and simplifying the native integration process for faster implementation.

In conclusion, integrating Go functions into Java using JNA is an effective and straightforward approach that enhances performance while simplifying development.

As always, code snippets are available over on GitHub.