Java 10 Removed javah: Generate JNI Headers with javac -h

Introduction Java code can call functions written in C or C++ through the Java Native Interface (JNI). A Java class declares such functions as native methods, and their C implementations must use exact function names and signatures, which are listed in a C header file generated from the class. For years this header was generated by a separate tool, javah. Java 10Click to read tutorial: Overview of New Features in Java 10 removed javah (JEP 313), and the header is now generated by javac itself, with its -h option. This tutorial explains how to generate JNI headers with javac -h. We will first see why javah was removed. We will then generate a header for a class with native methods, and understand the generated function names and signatures. Lastly, we will update Maven and Gradle builds which used javah. Why javah was removed javah read compiled .class files, so generating a header was always a separate step after compilation, which build tools had to run with the right class path. Java 8 added the -h <directory> option to javac, which writes the headers while compiling the source code, in the same step. With that, javah was no longer needed. It was deprecated in Java 9 and removed in Java 10. Running javah on Java 10 or later fails, since the command no longer exists.
A class with native methods The class below declares three native methods - an instance method, a static method, and two overloads of add(). Native methods have no body, just like abstract methods. The static block loads the native library containing their implementation when the class is first used.
A class with native methods
//NativeGreeter.java
package com.javabrahman.jni;

public class NativeGreeter {
  public static final int MAX_NAME_LENGTH = 64;

  static {
    System.loadLibrary("greeter");   // loads greeter.dll, libgreeter.so or libgreeter.dylib
  }

  public native String greet(String name);

  public static native long nanoTime();

  public native int add(int a, int b);

  public native long add(long a, long b);
}
Generating the header with javac -h -h takes the directory where the headers are written. The class files go, as usual, to the directory given with -d -
javac -h native -d classes NativeGreeter.java
This creates one header per class with native methods, named after the fully qualified class name with the dots replaced by underscores. Here it is native/com_javabrahman_jni_NativeGreeter.h -  GENERATED com_javabrahman_jni_NativeGreeter.h
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_javabrahman_jni_NativeGreeter */

#ifndef _Included_com_javabrahman_jni_NativeGreeter
#define _Included_com_javabrahman_jni_NativeGreeter
#ifdef __cplusplus
extern "C" {
#endif
#undef com_javabrahman_jni_NativeGreeter_MAX_NAME_LENGTH
#define com_javabrahman_jni_NativeGreeter_MAX_NAME_LENGTH 64L
/*
 * Class:     com_javabrahman_jni_NativeGreeter
 * Method:    greet
 * Signature: (Ljava/lang/String;)Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_com_javabrahman_jni_NativeGreeter_greet
  (JNIEnv *, jobject, jstring);

/*
 * Class:     com_javabrahman_jni_NativeGreeter
 * Method:    nanoTime
 * Signature: ()J
 */
JNIEXPORT jlong JNICALL Java_com_javabrahman_jni_NativeGreeter_nanoTime
  (JNIEnv *, jclass);

/*
 * Class:     com_javabrahman_jni_NativeGreeter
 * Method:    add
 * Signature: (II)I
 */
JNIEXPORT jint JNICALL Java_com_javabrahman_jni_NativeGreeter_add__II
  (JNIEnv *, jobject, jint, jint);

/*
 * Class:     com_javabrahman_jni_NativeGreeter
 * Method:    add
 * Signature: (JJ)J
 */
JNIEXPORT jlong JNICALL Java_com_javabrahman_jni_NativeGreeter_add__JJ
  (JNIEnv *, jobject, jlong, jlong);

#ifdef __cplusplus
}
#endif
#endif
Understanding the generated header
  • Function names are made of Java_, the fully qualified class name with underscores, and the method name - so greet() becomes Java_com_javabrahman_jni_NativeGreeter_greet. The JVM uses these names to find the C function for each native method.
  • Overloaded methods need different C names, so the argument types are encoded after a double underscore. add(int, int) becomes ..._add__II and add(long, long) becomes ..._add__JJ, where I and J are the JVM's type descriptors for int and long. Methods which are not overloaded keep the short name.
  • The first two parameters are added by JNI. JNIEnv * gives the C code access to JNI functions, such as converting strings. The second is a jobject - the this object - for an instance method, and a jclass - the class - for a static method like nanoTime().
  • Java types map to JNI types - String to jstring, int to jint, long to jlong, and so on.
  • The Signature comment shows each method's JVM descriptor - (Ljava/lang/String;)Ljava/lang/String; means "takes a String, returns a String".
  • The static final constant MAX_NAME_LENGTH was written as a C #define, prefixed with the class name, so the C code can use the same value as the Java code.
  • The extern "C" block lets the same header be used from C++ as well as C.
Implementing the native methods The C file includes the generated header and implements its functions. Below is a sketch of greet() and one add() -
greeter.c (implementation of two native methods)
#include <stdio.h>
#include "com_javabrahman_jni_NativeGreeter.h"

JNIEXPORT jstring JNICALL Java_com_javabrahman_jni_NativeGreeter_greet
  (JNIEnv *env, jobject self, jstring name) {
  const char *cname = (*env)->GetStringUTFChars(env, name, NULL);
  char buffer[com_javabrahman_jni_NativeGreeter_MAX_NAME_LENGTH + 16];
  snprintf(buffer, sizeof buffer, "Hello, %s!", cname);
  (*env)->ReleaseStringUTFChars(env, name, cname);
  return (*env)->NewStringUTF(env, buffer);
}

JNIEXPORT jint JNICALL Java_com_javabrahman_jni_NativeGreeter_add__II
  (JNIEnv *env, jobject self, jint a, jint b) {
  return a + b;
}
The C file is compiled into a shared library with the JDK's include directories on the compiler's include path, and the library is placed where System.loadLibrary() can find it - in a directory on java.library.path. For example, on Linux with GCC -
gcc -shared -fPIC -I"$JAVA_HOME/include" -I"$JAVA_HOME/include/linux" -Inative greeter.c -o libgreeter.so
java -Djava.library.path=. -cp classes com.javabrahman.jni.SomeMainClass
Note - For new code, also consider the Foreign Function & Memory API, a standard feature since Java 22. It calls native libraries from pure Java code, without writing any C glue code or generating headers. Updating builds which used javah Builds which ran javah as a separate step should instead pass -h to the compiler. In Maven, add it to the compiler plugin's arguments, and remove any plugin which ran javah -
Maven - generating JNI headers during compilation
<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-compiler-plugin</artifactId>
  <configuration>
    <compilerArgs>
      <arg>-h</arg>
      <arg>${project.build.directory}/native/include</arg>
    </compilerArgs>
  </configuration>
</plugin>
In Gradle, the JavaCompile task has a property for this -
Gradle - generating JNI headers during compilation
tasks.named('compileJava') {
  options.headerOutputDirectory = layout.buildDirectory.dir('native/include')
}
Summary In this tutorial we saw that Java 10 removed the javah tool, since javac -h has generated JNI headers while compiling since Java 8. We generated a header for a class with native methods, and went through how JNI builds the C function names, including for overloaded methods, and the extra JNIEnv and jobject or jclass parameters. We then implemented two of the functions in C, and lastly updated Maven and Gradle builds to use -h. In the next tutorial, we will look at the Unicode locale extensionsClick to read tutorial: Java 10 Unicode Locale Extensions: Currency, Week, Region added in Java 10.