12 March 2025
JNI allows managed code inside the JVM or ART to call into native code.
Java methods can be declared as native
, and then a corresponding C function1 can be written and automatically wired together when the native library is loaded.
Native code lacks mechanisms like packages and overloads, so a special format is used to encode the Java method signature. A Java method defined as:
package com.example;
class Things {
static native long createThing(String name, int count);
}
Requires a matching C declaration which looks like:
jlong Java_com_example_Things_createThing(
JNIEnv *env, jclass type, jstring name, jint count) {
// …
}
If you add parameter overloading into the mix, the C declaration must include the parameter signature as well:
jlong Java_com_example_Things_createThing_Ljava_lang_String_2I(
JNIEnv *env, jclass type, jstring name, jint count) {
// …
}
Woof! And if you get any part of the encoding wrong, the method call will fail at runtime:
Exception in thread "main" java.lang.UnsatisfiedLinkError:
'long Things.createThing(java.lang.String, int)'
at Things.createThing(Native Method)
at Main.main(example.java:6)
In my experience, these signatures do not change frequently. Once they’re correct you can mostly just leave them untouched. However, it’s a class of problem that would be nice to eliminate completely. Especially if within your projects they do change frequently.
When compiling native code, a header represents a series of functions implemented somewhere else. It allows consumers of a library to compile against its API without requiring the full implementation. When compiling the library itself, the compiler requires all header functions have corresponding implementations.
Defining a manually-written header for our C functions would be redundant and subject to all the same problems above.
Instead, we want to automatically derive the header from the corresponding Java code.
As of Java 8, javac
can do this for us with its -h
flag.
Let’s learn how to use it from javac
’s help output
❯ javac -h
error: -h requires an argument
Usage: javac <options> <source files>
use --help for a list of possible options
Wait… shit. Please don’t use -h
for real flags.
Anyway, it just takes a directory.
❯ javac -h h -d out Example.java
❯ tree
.
├── Example.java
├── things.c
├── h
│ └── com_example_Things.h
└── out
└── com
└── example
└── Things.class
Here is the full content of com_example_Things.h
:
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_example_Things */
#ifndef _Included_com_example_Things
#define _Included_com_example_Things
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_example_Things
* Method: createThing
* Signature: (Ljava/lang/String;I)J
*/
JNIEXPORT jlong JNICALL Java_com_example_Things_createThing
(JNIEnv *, jclass, jstring, jint);
#ifdef __cplusplus
}
#endif
#endif
Among the requisite boilerplate for single inclusion and C++ support is a function signature matching the one we wrote above!
Including this file from our .c
will cause the native compiler to validate all header functions have corresponding C implementations.
Let’s change the Java native method signature and see what happens.
class Things {
- static native long createThing(String name, int count);
+ static native long createThing(String name, int count, byte[] buffer);
}
❯ javac -h h -d out Example.java
❯ clang -I "$JAVA_HOME/include" \
-I "$JAVA_HOME/include/darwin" \
-I h \
things.c
things.c:4:7: error: conflicting types for 'Java_com_example_Things_createThing'
jlong Java_com_example_Things_createThing(
^
h/com_example_Things.h:15:25: note: previous declaration is here
JNIEXPORT jlong JNICALL Java_com_example_Things_createThing
^
1 error generated.
It’s not the most amazing message that I have seen. But it did fail compilation! Fixing the problem is now a matter of comparing the two signatures and updating the C file as needed.
Do you use Gradle?
Good news!
It automatically configures your javac
with the -h
flag so you don’t really need to do much.
❯ tree
.
├── build
│ ├── classes
│ │ └── java
│ │ └── main
│ │ └── com
│ │ └── example
│ │ └── Things.class
│ ├── generated
│ │ └── sources
│ ⋮ └── headers
│ └── java
│ └── main
│ └── com_example_Things.h
⋮
├── build.gradle
├── src
│ └── main
│ └── java
│ └── com
│ └── example
│ └── Example.java
└── things.c
This is the result after moving Example.java
into src/main/java/
, writing apply plugin: 'java-library'
in build.gradle
, and invoking ./gradlew assemble
If your native build occurs outside Gradle, the compileJava
task should be run first, then the external native build, and finally (with the native binaries put somewhere like src/main/resources/
) the full assemble
or build
task can be run.
For native builds which run as a Gradle task, you can consume the associated JavaCompile
task’s options.headerOutputDirectory
property which becomes an additional include directory.
Alternative languages which target Java bytecode usually have equivalent markers to bind to native functions, such as Kotlin’s external
modifier.
Unsurprisingly, when writing Kotlin we cannot use java -h
because we don’t have any Java!
There remains a long-standing feature request for kotlinc
to generate these headers like javac
.
Until then, there are three approaches to solving this problem: just write Java, use javah
, or write our own tool.
Since JNI methods are only stubs, continuing to write them in Java is not too painful. The Kotlin compiler supports bidirectional references from Java-to-Kotlin and Kotlin-to-Java. This allows your Kotlin to reference the Java stubs, while any Kotlin types needed in those Java stubs still work.
final class Jni {
static { loadLibrary("my-library"); }
private Jni() {}
static native long createThing(String name, int count);
}
Centralizing the stubs in a Java class creates a single location for the native library to be loaded. This does end up limiting access to a single package, which may or may not be desired.
If you are building with Gradle, both Kotlin/JVM and Kotlin/Multiplatform still get the automatic inclusion of the -h
flag on the resulting javac
execution.
All you need to do is create the Java file, and you’re good to go!
Finally, writing JNI stubs in Java helps avoid the need to understand how Kotlin (or any other language) maps its features to the underlying bytecode.
You no longer need to worry about how object
s, internal fun
s, or value class
parameters get converted.
javah
toolPrior to the javac
’s -h
flag producing headers, the JDK contained a standalone javah
tool which parsed Java .class
files.
This means that any other language which targeted Java bytecode and used its ACC_NATIVE
flag could generate headers.
While this sounds like the perfect solution for alternate languages, the tool was deprecated in Java 9 and removed in Java 10. However, if your code still targets Java 9 or older your class files can be read by this tool.
If you use Gradle, fetching old-ass JDKs is the perfect use-case for Gradle toolchains (which despite their docs are otherwise rarely a good idea).
def launcher = javaToolchains.launcherFor {
languageVersion = JavaLanguageVersion.of(8)
}
tasks.register('generateHeaders', Exec) {
def javah = launcher.map { it.metadata.installationPath.file("bin/javah") }
executable(javah.get().asFile) // TODO Lazy, once Gradle supports.
args("-h") // TODO Pass class dirs, etc.
}
❯ ./gradlew -q generateHeaders
Usage:
javah [options] <classes>
…
With the ASM library or Java’s new Class-File API, parsing these files to find native methods is possible. Once found, the mapping to their native signature can be done with a well-documented formula. Someone just has to do the work.
There was a repo which attempted this, but it is incomplete and now seemingly abandoned.
When the JDK removed the javah
tool, the Scala community forked that library to create their sbt-jni plugin.
But to my knowledge there is no other general-purpose tool for other languages which fulfills this need today.
JNI is generally avoided unless extenuating circumstances demand its use. It remains a challenging system even with compile-time validation due to performance concerns, multiple memory spaces, and almost no safety or security. Elaborate tools such as JNA and SWIG were invented to try and simplify native library use over JNI.
Starting last year with Java 22, the new Foreign Function & Memory API became available to use. FFM inverts ownership of the stubs, generating the Java sources from native headers using the jextract tool.
If I manually write a things.h
file with a regular C API that can be fed into jextract
.
#ifndef things_h
#define things_h
long createThing(char* name, int count, void* buffer);
#endif // things_h
❯ jextract --output ffm things.h
❯ tree ffm
ffm
└── things_h.java
The resulting things_h.java
file is a chonker, but among a slew of FFM implementation detail is a public Java API which corresponds to our native function.
public class things_h {
// …
/**
* {@snippet lang=c :
* long createThing(char *name, int count, void *buffer)
* }
*/
public static long createThing(MemorySegment name, int count, MemorySegment buffer) {
var mh$ = createThing.HANDLE;
try {
if (TRACE_DOWNCALLS) {
traceDowncall("createThing", name, count, buffer);
}
return (long)mh$.invokeExact(name, count, buffer);
} catch (Throwable ex$) {
throw new AssertionError("should not reach here", ex$);
}
}
}
Since both the name
and buffer
parameters on the C function were pointers to memory, they come across typed as MemorySegment
s.
If we have a Java String
and byte[]
, conversion and/or pinning of their managed memory such that it can be used by native code is required.
The FFM API provides utilities for this conversion in a similar way to which jni.h
provided conversion utilities for jstring
and jbytearray
types.
The boilerplate which makes up the rest of the file helps the JVM understand the shape of the native code so that it can be optimized alongside regular Java as well as layered with safety checks to avoid things like unrestricted memory access.
If your minimum-supported JDK is not yet Java 22 or newer, you can still use FFM through multi-release jars.
This embeds your FFM-flavored class files inside the META-INF/versions/22/
directory which is only loaded when the consumer is running on Java 22 or newer.
With the ownership reversed, there is no chance of changes to Java breaking the native code. Instead, changes to the native code will now break the Java compilation.
#ifndef things_h
#define things_h
-long createThing(char* name, int count, void* buffer);
+long createThing(char* name, int count);
#endif // things_h
❯ jextract --output ffm things.h
❯ javac -d out Example.java ffm/things_h.java
Example.java:23: error: method createThing in class things_h cannot be applied to given types;
return things_h.createThing(nameSegment, count, bufferSegment);
^
required: MemorySegment,int
found: MemorySegment,int,MemorySegment
reason: actual and formal argument lists differ in length
1 error
With jextract
able to parse native headers, the need to write custom C code to support the use of native libraries is diminished.
Ideally you would only run jextract
on the headers of the desired native libraries and then write 100% of your interaction with it from Java or your favorite JVM language.
Or any language which can produce functions compatible with the C ABI. ↩
— Jake Wharton