jni
the old way to call native code, and why it's being fenced in.
- chapter
- chapter 15.08
- reading time
- 8 min read
the Java Native Interface (JNI) is the original way to call C from java: declare a method native, generate a C header with javac -h, implement the function in C against the JNIEnv API, compile a shared library per platform, and load it with System.loadLibrary. it's been in java since 1.1 and runs a huge amount of existing code.
since java 24 (JEP 472) loading a native library is a restricted operation: it prints a warning today, and is planned to need --enable-native-access in the future. for new code, the FFM API does the same job without the C glue.
the picture
with FFM, java learned to speak C. with JNI, you hire a translator who lives in C: a small layer of C functions with very particular names, written by you, compiled by you, for every operating system and CPU you ship to. the translator can talk back to the JVM through a phone book of about 230 functions (JNIEnv): "give me this string's characters", "make a new array", "call this java method", "throw this exception".
the whole round trip
here's a real JNI library built from scratch on a mac: a native int add(int, int) and a native String greet(String).
- →
- →
- →
- →
- →
public class Greeter { static { System.loadLibrary("greeter"); } static native int add(int a, int b); static native String greet(String name); public static void main(String[] args) { System.out.println(add(40, 2)); System.out.println(greet("ada")); }}
the java side declares methods with no body, marked native, and loads a library that will provide them.
a few things to notice in there:
- the names are the contract.
Java_Greeter_addis found by name when the method is first called (or registered explicitly withRegisterNatives). rename the class or move it to a package and the C function name must change too, which is why you regenerate the header. - primitives cross for free, objects don't.
jintis just anint. ajstringis an opaque handle; C has to ask the JVM for a copy of the characters and give it back. - the library is per platform.
libgreeter.dylibon macOS,libgreeter.soon linux,greeter.dllon windows, each built separately, often for two CPU architectures each. libraries like sqlite-jdbc or netty's native transports ship a dozen binaries inside the jar and pick one at runtime.
crossing the boundary
what actually happens when java calls add(40, 2):
hover a stage.
that's the overhead: not the C code (an add is one instruction), but the bookkeeping around it. measured with JMH, calling the same add from the same library:
why it's being fenced in
JNI code runs in the same process as the JVM with no checks at all. a native library can read or write any memory, corrupt the heap, or crash the whole JVM (you get an hs_err_pid<n>.log and the process is gone). and until java 24, any library on the class path could do that silently: a dependency of a dependency could load native code without anyone in the application deciding it should.
JEP 472 (java 24) makes native access an explicit decision, the same way strong encapsulation made deep reflection one (reflection):
| jdk 23 and earlier | jdk 24 and 25 | planned (a future release) | |
|---|---|---|---|
System.loadLibrary, declaring native methods | allowed, silent | allowed, warning once per module | throws IllegalCallerException unless enabled |
| FFM downcalls and upcalls | warning (since 22) | warning once per module | same |
| opt in | n/a | --enable-native-access=ALL-UNNAMED (or module names), or Enable-Native-Access in the jar manifest | same |
and since java 24 there's a tool to find out who needs it before you flip the switch, jnativescan:
$ jnativescan --class-path greeter.jar
greeter.jar (ALL-UNNAMED):
Greeter:
Greeter::add(int,int)int is a native method declaration
Greeter::greet(String)String is a native method declaration
Greeter::<clinit>()void references restricted methods:
java.lang.System::loadLibrary(String)voidrun it over your application's class path and you get the list of jars that do native things, which is the list of modules to name in --enable-native-access.
where JNI still makes sense+
for new code, FFM covers almost everything JNI does: calling C functions, passing structs and arrays, callbacks, and with jextract you don't write descriptors by hand. JNI still shows up where you need to work with java objects from native code in ways FFM doesn't aim for (calling back into arbitrary java methods with java objects as arguments, from inside a native library), in embedding scenarios (a C program that creates a JVM with the invocation API, JNI_CreateJavaVM), and in the long tail of existing libraries that work and that nobody is going to rewrite. the JDK itself still has plenty of JNI inside. and the restriction is about consent, not removal: JNI isn't deprecated.
- JNI:
nativemethods,javac -h, C againstJNIEnv, a shared library per platform,System.loadLibrary. - primitives cross cheaply; objects need JNI calls, and every acquire needs a release.
- a native bug crashes the whole JVM.
-Xcheck:jniwhile developing. - since java 24, native access warns. opt in with
--enable-native-access;jnativescanfinds who needs it. - for new code, use the FFM API: no C glue, and faster calls.