Calling C From Java Is Easy

Sometimes we need to access operating system functions that the standard Java API doesn’t expose, or use non-Java libraries. Although it’s well known that you can call this “native code” from Java using JNI, there is not so much entry-level material on how it’s actually done. It is often left out of introductory material–including the official Java Tutorial. Here I hope to give a short introduction to get you started.

Programming in JNI starts with defining a class with methods declared as native. Next you generate a C header file that declares functions that implement. Then you define these functions in a separate file and compile it into a shared library. In this tutorial we’ll use Linux and the GCC compiler.

Suppose we need to know the name of the terminal device from which the JVM was launched. (A slightly silly example since you could just use /dev/tty.) From C you would do this by calling the POSIX functions isatty and ttyname. Let’s make a class that gives us access to them:

package ex;
public class TTYUtil {
    static { System.loadLibrary("ttyutil"); }
    public static native boolean isTTY();
    public static native String getTTYName();
}

The call to System.loadLibrary in the class initializer looks for a shared library and links it to the JVM. The file name depends on the operating system: on Windows this code would look for ttyutil.dll, on Linux and Solaris libttyutil.so.

The next step is compiling the Java code and generating the C header file:

$ javac ex/TTYUtil.java
$ javah ex.TTYUtil

The javah tool will create a C header file called ex_TTYUtil.h. This file contains the declarations for the C functions we have to define:

JNIEXPORT jboolean JNICALL Java_ex_TTYUtil_isTTY
  (JNIEnv *, jclass);

JNIEXPORT jstring JNICALL Java_ex_TTYUtil_getTTYName
  (JNIEnv *, jclass);

The idea is that the header file should be generated automatically during the build process so you should not modify it manually. If you need other declarations or #include statements you should use a different file.

Create ex_TTYUtil.c to define the C functions:

#include "ex_TTYUtil.h"
#include <unistd.h>

JNIEXPORT jstring JNICALL Java_ex_TTYUtil_getTTYName
  (JNIEnv *env, jclass cls)
{
    char *name = ttyname(STDOUT_FILENO);
    return (*env)->NewStringUTF(env, name);
}

JNIEXPORT jboolean JNICALL Java_ex_TTYUtil_isTTY
  (JNIEnv *env, jclass cls)
{
    return isatty(STDOUT_FILENO)? JNI_TRUE: JNI_FALSE;
}

These functions receive the JNI environment object as the first argument. The second argument is the class for static native methods and the object for non-static methods. The rest of the arguments, if any, are the method arguments from Java. The JNI environment is used for interacting with the virtual machine, like here the NewStringUTF function is used to create a new Java String object from a C string.

To compile and link the C code you can use:

$ gcc -fPIC -c ex_TTYUtil.c -I $JAVA_HOME/include
$ gcc ex_TTYUtil.o -shared -o libttyutil.so -Wl,-soname,ttyutil

Now you should have libttyutil.so in your working directory. Let’s try using the library.

import ex.TTYUtil;
public class Test {
    public static void main(String[] args) {
        if (TTYUtil.isTTY()) {
            System.out.println("TTY: "+TTYUtil.getTTYName());
        } else {
            System.out.println("Not a TTY");
        }
    }
}

Compile this class like you normally would and then run it:

$ export LD_LIBRARY_PATH=.
$ java Test
TTY: /dev/pts/3

And what if the output is not connected to a terminal?

$ java Test | cat
Not a TTY

The JVM looks for native libraries in the paths specified in the system property java.library.path in addition to what’s normal for the operating system. Here we made the shared library available to the JVM temporarily by adding the current directory to LD_LIBRARY_PATH. To permanently install a shared library on Linux you would copy the .so to /usr/lib (or any other directory mentioned in /etc/ld.so.conf) and then run ldconfig.

5 Comments

  1. Gareth
    Posted 2013/05/20 at 01:04 | Permalink

    Really useful post, thank you :)

  2. Mark
    Posted 2014/01/20 at 23:22 | Permalink

    Great information! This answers MOST of my questions! What question remains is: What is different if the C functions I want to call from Java are in a third-party library, say “libmap.so” (Linux), and not part of the standard C library like isatty() and ttyname()?

  3. Joni
    Posted 2014/01/21 at 16:03 | Permalink

    What’s different when calling a third party library is that you have to add the #include directive for the library’s header to get access to the functions and types declared there, and then link against the .so when linking your native library. In gcc you would link against libmap.so using the -l parameter, so the final command would look like gcc ex_TTYUtil.o -shared -o libttyutil.so -lmap.

    If what you want to do is call functions in an existing external library rather than write your own native code, it’s more convenient to use JNA or SWIG. It saves you the trouble of writing lots of wrapper functions. I’ve been meaning to write about JNA but haven’t got around to it yet.

  4. Marco
    Posted 2014/05/16 at 18:59 | Permalink

    Great example, thanks! Only thing is, I believe your compilation line is missing something. In my case, it had to be this:

    $ gcc -fPIC -c ex_TTYUtil.c -I $JAVA_HOME/include -I $JAVA_HOME/include/linux

    It would seem at least my version of jdk (1.7) moved the jni_md.h file to the linux subdir.

  5. Joni
    Posted 2014/05/16 at 19:11 | Permalink

    Thanks for your comment! The header file jni_md.h only contains the machine dependent definitions for integer types like jint and jlong. There should be no need to include it directly because jni.h will include it when needed.