Tutorial: The Java native interface to C code

This post is the first in a tutorial series about how JNI, the Java native interface, can be used to call C code from a Java program. We start with the basics and finish with a useful Java class that uses an external library for its core operation. Along the way, we look at some useful ways of making JNI manageable that I came up with in working on Klipspringer. The JNI code is going to be plain C only, no C++. Eventually, you’ll want to turn to the official documentation for parts of JNI not covered here, which will be easier after you’ve gone through this tutorial.

This first part gets a rather simple hello world example going, and presents a generic Makefile template to facilitate building and executing Java/JNI programs.

Writing the code

Let’s start our Hello project with agreeing where to put the files.1 First create a hello project directory and cd into it:

mkdir hello
cd hello

All other commands in this post assume that your current working directory is hello. The Java classes are in a package called net.avadeaux.hello.2 Initialize the project file structure:

mkdir -p src/java/net/avadeaux/hello
mkdir -p src/jni

To get an idea how to organize the Java code, we’re not going to put it all in one file but split it up over several classes. First get your favorite text editor up and create src/java/net/avadeaux/hello/Hello.java with the following content:

package net.avadeaux.hello;

public class Hello {
    static { Library.init(); }

    public static native void sayHello(long time);
}

That’s a class with just a static sayHello method which is declared native, meaning that the implementation is not in the Java file, but is to be found in a native-code shared library. The time parameter is supposed to be the current time in seconds since the beginning of 1970, and it’s declared long to make it Y2K38 safe. The method will print the time along with “Hello, world!”

The little static block at the top of the class refers to a Library class, which is my way of doing static initialization of a JNI library. Put this class definition in src/java/net/avadeaux/hello/Library.java:

package net.avadeaux.hello;

public class Library {
    static { System.loadLibrary("hello"); }

    /** Makes sure static resources are properly initialized. */
    public static void init() {
        // Nothing here, since all is done in static initialization block.
    }
}

As you can see, init() doesn’t do anything, but calling it makes sure static code in the class has been executed, and the static code block calls System.loadLibrary to load the actual library file (whose exact name depends on your platform) where our compiled JNI C code is going to be found. Why not simply call loadLibrary directly in the Hello static block? You could do that, and most JNI examples do, but I prefer the convention to take care of static library initialization in a separate class, because sometimes it needs to do more than just load the library. In the Hello example, Library is just a dummy class, but in future posts it’s actually going to do something. If nothing else, this convention isolates the library name, so if you want to change it you don’t have to edit more than one Java file.

Let’s add one more class with a native method while we’re at it. This is src/java/net/avadeaux/hello/OurTime.java, whose native method seconds is supposed to return the current time in seconds.

package net.avadeaux.hello;

public class OurTime {
    static { Library.init(); }

    public static native long seconds();
}

My convention is to have a separate C file for each Java class with any native methods, and to place them all in src/jni. This is src/jni/Hello.c:

#include "net_avadeaux_hello_Hello.h"

#define METHOD(name) JNICALL Java_net_avadeaux_hello_Hello_ ## name

void METHOD(sayHello)(JNIEnv *env, jclass thisClass, jlong jtime) {
    double years = jtime/(60*60*24*365.25);
    printf("Hello, world! It's %.9f years since 1970 began.\n", years);
    fflush(stdout);
}

The header file included at the top is generated from Hello.java by the Java compiler (we’ll get to how), and looks like this:

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class net_avadeaux_hello_Hello */

#ifndef _Included_net_avadeaux_hello_Hello
#define _Included_net_avadeaux_hello_Hello
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     net_avadeaux_hello_Hello
 * Method:    sayHello
 * Signature: (J)V
 */
JNIEXPORT void JNICALL Java_net_avadeaux_hello_Hello_sayHello
  (JNIEnv *, jclass, jlong);

#ifdef __cplusplus
}
#endif
#endif

Declaractions needed in JNI C code (types for instance) are declared in jni.h, included at the beginning. Under the big comment in the middle, you see the declaration of the function Java_net_avadeaux_hello_Hello_sayHello, which implements Hello.sayHello.

Going back to Hello.c, what I did to create it was to first generate the header file and then essentially copy the declared function head from there and append a body, except that to make the function name appear less clunky I used a METHOD macro for the class-identifying prefix.

All JNI method implementation functions have a first JNIEnv *env parameter, which can be used for accessing Java side stuff in the C code. The second parameter of METHOD(sayHello) is a pointer to the class object representing the Hello class, because sayHello is declared static. Otherwise, it would have been a pointer to “this” Hello object instead.

The third parameter of METHOD(sayHello) is the time parameter declared in Hello.java. Its type jlong is the JNI type that corresponds to Java long. Precisely which C type that is depends on the platform – it can be int, long int or long long int depending on which of them is 64 bits like the Java long type – but you can expect it to be the same as int64_t. The time in seconds is converted to years and printed along with “Hello, world!” using printf immediately followed by an fflush to get the output to the terminal. I’ve made a habit of always flushing output immediately in JNI C code, after having been confused by output stuck in a buffer a number of times.

The implementation of OurTime.seconds, which uses the standard C function time to get the time in seconds, goes in src/jni/OurTime.c:

#include <time.h>
#include "net_avadeaux_hello_OurTime.h"

#define METHOD(name) JNICALL Java_net_avadeaux_hello_OurTime_ ## name

jlong METHOD(seconds)(JNIEnv *env, jclass thisClass) {
    return time(NULL);
}

Finally, src/java/net/avadeaux/hello/Main.java contains the main method:

package net.avadeaux.hello;

public class Main {
    public static void main(String[] args) {
        long time = OurTime.seconds();
        Hello.sayHello(time);
    }
}

Compiling and running

Getting the commands right to compile and run is arguably more complicated than writing the source code. In the next section, we look at using a standardized Makefile to take care of all this, but first let’s try individual commands to get a reasonable understanding of them in case you need to do something differently. I’m assuming that you use a Unix-like terminal with a JDK installed so that Java can be compiled with javac and run with java.

Java compile

The Java files with native methods should be compiled to class files like always with Java, but to get the JNI header files for classes with native methods we need to add -h <directory>:

javac -sourcepath src/java -d target/class -h target/jnih src/java/net/avadeaux/hello/*.java

I find it convenient to then collect the class files in a JAR. That way, the whole executable image is kept to just two files (or three when we add a command script), one with bytecode compiled from the Java source and one with machine code compiled from the C source. The following commands create target/hello.jar and then cd back to hello:

cd target/class
jar cf ../hello.jar net/avadeaux/hello/*.class
cd ../..

Finding JAVA_HOME

The C compiler has to be given the locations of general JNI header files under the JAVA_HOME directory in the JDK installation. It would be nice if there was a standard environment variable set to JAVA_HOME, but no such luck. I know of three ways of finding where JAVA_HOME is on your system: one that works in macOS, one that works in Linux, and one that works in both as well as hopefully all others. Let’s start with the last one.

The command “java -XshowSettings:properties -version” prints all JVM property default values to the standard error stream, and among them is java.home which has the value we are looking for. One caveat: if you have more than one JVM installed, and therefore more than one java command on your system, the one used here should be the one of the JDK that you use for compiling. In other words, java and javac should belong to the same installation.

To extract just the desired value, you can do this:

java -XshowSettings:properties -version 2>&1 | grep java\.home | sed 's/[[:blank:]]*java.home[[:blank:]]*\=[[:blank:]]*//'

On my Mac, where I installed the JDK with Homebrew, the result is:

/opt/homebrew/Cellar/openjdk/23.0.2/libexec/openjdk.jdk/Contents/Home

This is the same that I get from the command /usr/libexec/java_home, which is an alternative way of finding JAVA_HOME in macOS.

On my Raspberry Pi I get:

/usr/lib/jvm/java-17-openjdk-armhf

In the Debian-based Linux of the Pi, and some other systems as well, there is the alternative to use the fact that the javac command is symlinked to a file under JAVA_HOME. The command “readlink -f $(command -v javac)” outputs /usr/lib/jvm/java-17-openjdk-armhf/bin/javac, so you can just remove /bin/javac from the end to get the right path.

Below JAVA_HOME, there’s an include directory which contains jni.h alongside a platform-specific subdirectory that also needs to be on the include path. On my Pi, the subdirectory is called linux and on my Mac it’s darwin because the core of macOS is called Darwin.

Building the shared library file

The C files need to be compiled and linked into a shared library file. The C compiler can normally take care of it all in a single command, but needs a number of arguments which are partly platform dependent.

On many systems, including Linux, the hello shared library file should be called libhello.so, and a file in the correct format is produced by GCC with arguments “-shared -fPIC”. In macOS, the file should instead be called libhello.dylib, and is produced by Clang (the C compiler normally used under macOS) with arguments “-dynamiclib -fPIC”.

If none of these are right for you, you may have to do a little bit of digging to find out how to build a file in the right format on your platform. If you find out something that could work for others, I’d appreciate if you’d add a comment about it to this blog post.

This is a command that works on my Raspberry Pi, and should work on other Linux systems except that you have to substitute your location of JAVA_HOME in the second and third line:

gcc -Itarget/jnih \
    -I/usr/lib/jvm/java-17-openjdk-armhf/include \
    -I/usr/lib/jvm/java-17-openjdk-armhf/include/linux \
    -shared -fPIC \
    -O3 -Wall \
    src/jni/Hello.c src/jni/OurTime.c \
    -o target/libhello.so

This works on my Mac:

clang -Itarget/jnih \
      -I/opt/homebrew/Cellar/openjdk/23.0.2/libexec/openjdk.jdk/Contents/Home/include \
      -I/opt/homebrew/Cellar/openjdk/23.0.2/libexec/openjdk.jdk/Contents/Home/include/darwin \
      -dynamiclib -fPIC \
      -O3 -Wall \
      src/jni/Hello.c src/jni/OurTime.c \
      -o target/libhello.dylib

In both variants, the -Itarget/jnih argument tells the C compiler the location of our Java generated header files, the two following -I arguments add the paths for JNI header files under JAVA_HOME, the following line has the arguments to specify the type of output file, and the -O3 -Wall arguments tells the compiler to use aggressive optimization and warn if something looks strange. Then follows the list of C source files and finally an -o argument to give the name of the output file.

Execution

If you’ve followed my build suggestions, you should now have the two files needed to run the program (a JAR and a shared library) in the hello/target directory, and be able to run the main method with the following command:

java -cp target/hello.jar --enable-native-access=ALL-UNNAMED -Djava.library.path=target net.avadeaux.hello.Main

That’s a bit of a mouthful to run a program3, which is why I like to create an executable script with the command, in which I use an absolute path to hello/target so that the script works regardless of your current working directory. We look at a Makefile that creates the script, along with executing all the compile commands, in the next section.

Generic Makefile

Below follows a Makefile that takes care of compiling a Java/JNI project provided that you use GNU Make4 (which is the default on all systems I use), that Bash is installed, and that you follow the file structure conventions from above. On macOS, it also assumes that you have Homebrew installed and may use it for some code libraries.

After a long comment outlining the file structure, follows the only section that normally needs to be edited. It assigns PROG_NAME, MAIN_CLASS, and JNI_CLASSES (explained in file comments) the correct values for the Hello project. LIB_INCLUDE and LDFLAGS have default values that we aren’t going to modify until the fourth installment of this series. DESTDIR specifies where files should go if you make install to get a system-wide installation of the target files from the project.

If this content is placed in hello/Makefile, the make command performs the necessary commands to build a JAR, a shared library, and an executable script hello, which are all placed in hello/target. So the following two commands (given with hello as the working directory) build and run the program:

make
./target/hello

The targets clean, install, and uninstall can be used for removing local compiled files, install target files in DESTDIR, and remove installed files.

# Makefile for Java program with some JNI.
#
# Local directory structure:
# prog/                         <- root of project
# |- src/                       <- source files
# |  |- java/                   <- Java source package hierarchy below here
# |  |- jni/                    <- C files for dynamic library below here
# |- target/                    <- all compilation output goes here
#    |- class/                  <- compiled Java classes
#    |- jnih/                   <- headers generated by javac -h
#    |- prog.jar                <- compiled Java classes packaged in JAR
#    |- libprog.so or .dylib    <- compiled from src/jni/ source files
#    |- prog                    <- local executable script
#
# Results of sudo make install:
# DESTDIR/                      <- e.g. /usr/local
# |- lib/
# |  |- prog/
# |     |- prog.jar             <- package of compiled Java classes
# |     |- libprog.so or .dylib <- compiled from C sources
# |     |- bin/
# |        |- prog              <- global executable script
# |- bin/
#    |- prog                    <- symlink to DESTDIR/lib/prog/bin/prog

# ------------------------------------------------------------------------------
# Project specific values. Sections after this are meant to be generic.

# Name for executable script, JAR, and dynamic library
PROG_NAME = hello

# The class that contains a main method to be run by the executable script
MAIN_CLASS = net.avadeaux.hello.Main

# Classes that contain native methods, separated by space
JNI_CLASSES = net.avadeaux.hello.Hello net.avadeaux.hello.OurTime

# Extra include paths for libraries in this project
LIB_INCLUDE = -I$(LIB_BASE)/include

# Libraries to link with and where to find them
LDFLAGS = -L$(LIB_BASE)/lib -lc

# Where to install system-wide (if at all)
DESTDIR = /usr/local

# ------------------------------------------------------------------------------
# File name values, derived from the above

SHELL = /bin/bash

JNI_JAVA = $(shell for c in $(JNI_CLASSES); do echo -n src/java/$$c | sed 's|\.|/|g'; echo .java; done)
JNI_H = $(shell for c in $(JNI_CLASSES); do echo -n target/jnih/$$c | sed 's/\./_/g'; echo .h; done)
SRC_JAVA = $(shell find src/java -name '*.java')
SRC_C = $(shell find src/jni -name '*.c')
SRC_H = $(shell find src/jni -name '*.h')
ABS_LOCAL = $(shell pwd)

# ------------------------------------------------------------------------------
# Platform dependent values

OS = $(shell uname -s | tr '[:upper:]' '[:lower:]')
ifeq ($(OS),darwin)             # macOS
JNI_CFLAGS = -dynamiclib -fPIC
SOEXT = dylib
LIB_BASE = $(shell brew --prefix)
else                            # Linux and others
JNI_CFLAGS = -shared -fPIC
SOEXT = so
LIB_BASE = /usr
endif

JAVA_HOME = $(shell java -XshowSettings:properties -version 2>&1 | grep java\.home | sed 's/[[:blank:]]*java.home[[:blank:]]*\=[[:blank:]]*//')
JNI_INCLUDE = -I$(JAVA_HOME)/include -I$(JAVA_HOME)/include/$(OS)

CFLAGS = -O3 -Wall

# ------------------------------------------------------------------------------
# Targets

# Default target: build local
all: target/$(PROG_NAME).jar target/lib$(PROG_NAME).$(SOEXT) target/$(PROG_NAME)

# JAR file
target/$(PROG_NAME).jar: $(SRC_JAVA)
        mkdir -p target/class
        javac -sourcepath src/java -d target/class $(SRC_JAVA)
        cd target/class; find . -name '*.class' -exec jar cf ../$(PROG_NAME).jar {} +

# Generated header files
$(JNI_H): $(JNI_JAVA) Makefile
        mkdir -p target/class target/jnih
        javac -sourcepath src/java -d target/class -h target/jnih $(JNI_JAVA)

# Dynamic library
target/lib$(PROG_NAME).$(SOEXT): $(SRC_C) $(SRC_H) $(JNI_H) Makefile
        $(CC) -Itarget/jnih $(JNI_INCLUDE) $(LIB_INCLUDE) $(JNI_CFLAGS) $(CFLAGS) $(SRC_C) -o target/lib$(PROG_NAME).$(SOEXT) $(LDFLAGS)

# Local executable cript for running main class
target/$(PROG_NAME): Makefile
        echo '#!/bin/sh' > target/$(PROG_NAME)
        echo 'java -cp $(ABS_LOCAL)/target/$(PROG_NAME).jar --enable-native-access=ALL-UNNAMED -Djava.library.path=$(ABS_LOCAL)/target $(MAIN_CLASS) "$$@"' >> target/$(PROG_NAME)
        chmod 755 target/$(PROG_NAME)

# Remove local generated files
clean:
        rm -rf target

# System wide installation
install: all
        mkdir -p $(DESTDIR)/lib/$(PROG_NAME)/bin $(DESTDIR)/bin
        cp target/$(PROG_NAME).jar target/lib$(PROG_NAME).$(SOEXT) $(DESTDIR)/lib/$(PROG_NAME)/
        echo '#!/bin/sh' > $(DESTDIR)/lib/$(PROG_NAME)/bin/$(PROG_NAME)
        echo 'java -cp $(DESTDIR)/lib/$(PROG_NAME)/$(PROG_NAME).jar -Djava.library.path=$(DESTDIR)/lib/$(PROG_NAME) $(MAIN_CLASS) "$$@"' >> $(DESTDIR)/lib/$(PROG_NAME)/bin/$(PROG_NAME)
        chmod 755 $(DESTDIR)/lib/$(PROG_NAME)/bin/$(PROG_NAME)
        ln -s $(DESTDIR)/lib/$(PROG_NAME)/bin/$(PROG_NAME) $(DESTDIR)/bin/

# Remove system-wide installed files
uninstall:
        rm -f $(DESTDIR)/bin/$(PROG_NAME)
        rm -rf $(DESTDIR)/lib/$(PROG_NAME)

Coming up

The following posts in this series will go into details on error handling, transferring external data using ByteBuffer, and maintaining the external state of an object that uses external resources for its operation.

Notes

  1. Lazy readers can download a zip file containing the file structure with all the example files already there.
  2. Because avadeaux.net is the domain I own, and I prefix all my package names with it in accordance with the Java package naming convention.
  3. Even more so than before since recent Java versions require that you add an option to enable native access to avoid an aggressive warning, or blocking in future versions. You can do this in other ways, and be more selective using modularization (yet another topic for which the Java team would have you spend reading documentation to get it right, without any actual improvement), but just adding an --enable-native-access option makes it work like before.
  4. GNU Make has more shell script capabilities than generally required of a POSIX compliant make which is convenient to script the whole build process in the Makefile. An alternative is to use a configure script to create a platform-specific Makefile, like I did in an earlier blog post. Another alternative is to use elaborate macro expansions, and create a more portable Makefile, but I find that it gets less readable than with the GNU Make extensions.

2 comments

    1. Thanks for the appreciation, Oleg! You inspired me to update this post, and the Makefiles in the zip, to add the –enable-native-access option that newer versions of Java want.

Leave a comment

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.