Introduction to JNI
The Java Native Interface (JNI) is a programming framework that enables Java code running in the JVM to call and be called by native applications (programs written in other languages like C, C++, or Assembly) and libraries.
Why Use JNI?
- Performance: Critical sections can be optimized in native code
- Hardware Access: Direct hardware/OS-level operations
- Legacy Code: Integration with existing native libraries
- Platform-Specific Features: Access OS-specific functionality
1. JNI Architecture & Workflow
High-Level Architecture
Java Application ↓ (JNI calls) JNI Interface Layer ↓ (Native method calls) Native Libraries (.dll, .so, .dylib) ↓ Operating System
Basic Workflow
- Write Java class with
nativemethod declarations - Compile Java class
- Generate C/C++ header using
javac -h - Implement native method in C/C++
- Compile native library
- Load and use native library in Java
2. Basic JNI Example
Step 1: Java Class with Native Methods
public class NativeExample {
// Load native library
static {
System.loadLibrary("NativeLibrary");
}
// Native method declarations
public native String getNativeString();
public native int addNumbers(int a, int b);
public native void printMessage(String message);
public static void main(String[] args) {
NativeExample example = new NativeExample();
// Call native methods
String nativeString = example.getNativeString();
System.out.println("Native string: " + nativeString);
int sum = example.addNumbers(10, 20);
System.out.println("Sum: " + sum);
example.printMessage("Hello from Java!");
}
}
Step 2: Generate Header File
javac -h . NativeExample.java
This generates NativeExample.h:
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class NativeExample */
#ifndef _Included_NativeExample
#define _Included_NativeExample
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: NativeExample
* Method: getNativeString
* Signature: ()Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_NativeExample_getNativeString
(JNIEnv *, jobject);
/*
* Class: NativeExample
* Method: addNumbers
* Signature: (II)I
*/
JNIEXPORT jint JNICALL Java_NativeExample_addNumbers
(JNIEnv *, jobject, jint, jint);
/*
* Class: NativeExample
* Method: printMessage
* Signature: (Ljava/lang/String;)V
*/
JNIEXPORT void JNICALL Java_NativeExample_printMessage
(JNIEnv *, jobject, jstring);
#ifdef __cplusplus
}
#endif
#endif
Step 3: Implement Native Methods
NativeExample.c
#include <stdio.h>
#include <jni.h>
#include "NativeExample.h"
// Implementation of getNativeString
JNIEXPORT jstring JNICALL Java_NativeExample_getNativeString
(JNIEnv *env, jobject thisObject) {
return (*env)->NewStringUTF(env, "Hello from Native Code!");
}
// Implementation of addNumbers
JNIEXPORT jint JNICALL Java_NativeExample_addNumbers
(JNIEnv *env, jobject thisObject, jint a, jint b) {
return a + b;
}
// Implementation of printMessage
JNIEXPORT void JNICALL Java_NativeExample_printMessage
(JNIEnv *env, jobject thisObject, jstring message) {
// Convert Java String to C string
const char *nativeMessage = (*env)->GetStringUTFChars(env, message, NULL);
printf("Native code received: %s\n", nativeMessage);
// Release the string memory
(*env)->ReleaseStringUTFChars(env, message, nativeMessage);
}
Step 4: Compile and Run
On Linux:
# Compile native library
gcc -shared -fpic -I${JAVA_HOME}/include -I${JAVA_HOME}/include/linux \
NativeExample.c -o libNativeLibrary.so
# Run Java program
java -Djava.library.path=. NativeExample
On Windows:
# Compile native library cl -I"%JAVA_HOME%\include" -I"%JAVA_HOME%\include\win32" \ -LD NativeExample.c -FeNativeLibrary.dll # Run Java program java -Djava.library.path=. NativeExample
3. JNI Data Types & Mapping
Primitive Types
| Java Type | Native Type | Description |
|---|---|---|
boolean | jboolean | 8-bit unsigned |
byte | jbyte | 8-bit signed |
char | jchar | 16-bit unsigned |
short | jshort | 16-bit signed |
int | jint | 32-bit signed |
long | jlong | 64-bit signed |
float | jfloat | 32-bit float |
double | jdouble | 64-bit float |
void | void | Void type |
Reference Types
| Java Type | Native Type |
|---|---|
Object | jobject |
String | jstring |
Class | jclass |
Object[] | jobjectArray |
boolean[] | jbooleanArray |
byte[] | jbyteArray |
char[] | jcharArray |
int[] | jintArray |
long[] | jlongArray |
float[] | jfloatArray |
double[] | jdoubleArray |
4. Working with Strings
String Conversion Examples
#include <jni.h>
JNIEXPORT void JNICALL Java_StringExample_handleStrings
(JNIEnv *env, jobject obj, jstring javaString) {
// Method 1: GetStringUTFChars (for modified UTF-8)
const char *nativeString1 = (*env)->GetStringUTFChars(env, javaString, NULL);
if (nativeString1 != NULL) {
printf("UTF-8 string: %s\n", nativeString1);
(*env)->ReleaseStringUTFChars(env, javaString, nativeString1);
}
// Method 2: GetStringChars (for UTF-16)
const jchar *nativeString2 = (*env)->GetStringChars(env, javaString, NULL);
if (nativeString2 != NULL) {
jsize length = (*env)->GetStringLength(env, javaString);
printf("String length: %d\n", length);
(*env)->ReleaseStringChars(env, javaString, nativeString2);
}
// Method 3: GetStringCritical (for performance-critical sections)
const jchar *criticalString = (*env)->GetStringCritical(env, javaString, NULL);
if (criticalString != NULL) {
// Perform time-critical operations
(*env)->ReleaseStringCritical(env, javaString, criticalString);
}
}
// Create new Java string from native code
JNIEXPORT jstring JNICALL Java_StringExample_createString
(JNIEnv *env, jobject obj) {
const char *nativeString = "Created in Native Code";
return (*env)->NewStringUTF(env, nativeString);
}
5. Working with Arrays
Primitive Arrays Example
// Java class
public class ArrayExample {
static { System.loadLibrary("ArrayLibrary"); }
public native int processIntArray(int[] array);
public native double[] createDoubleArray(int size);
public static void main(String[] args) {
ArrayExample example = new ArrayExample();
int[] data = {1, 2, 3, 4, 5};
int sum = example.processIntArray(data);
System.out.println("Sum: " + sum);
double[] doubles = example.createDoubleArray(5);
System.out.println("Created array length: " + doubles.length);
}
}
Native Implementation:
#include <jni.h>
#include <stdio.h>
JNIEXPORT jint JNICALL Java_ArrayExample_processIntArray
(JNIEnv *env, jobject obj, jintArray javaArray) {
jint sum = 0;
jsize length = (*env)->GetArrayLength(env, javaArray);
// Get array elements
jint *nativeArray = (*env)->GetIntArrayElements(env, javaArray, NULL);
if (nativeArray == NULL) {
return 0; // Error handling
}
// Process array
for (int i = 0; i < length; i++) {
sum += nativeArray[i];
nativeArray[i] *= 2; // Modify original array
}
// Release array elements (commit changes back to Java)
(*env)->ReleaseIntArrayElements(env, javaArray, nativeArray, 0);
return sum;
}
JNIEXPORT jdoubleArray JNICALL Java_ArrayExample_createDoubleArray
(JNIEnv *env, jobject obj, jint size) {
// Create new double array
jdoubleArray result = (*env)->NewDoubleArray(env, size);
if (result == NULL) {
return NULL; // Out of memory
}
// Initialize array with values
jdouble *nativeArray = (*env)->GetDoubleArrayElements(env, result, NULL);
if (nativeArray != NULL) {
for (int i = 0; i < size; i++) {
nativeArray[i] = i * 1.5;
}
(*env)->ReleaseDoubleArrayElements(env, result, nativeArray, 0);
}
return result;
}
6. Working with Objects and Fields
Accessing Object Fields and Methods
// Java class
public class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public native void updatePersonNative();
@Override
public String toString() {
return "Person{name='" + name + "', age=" + age + "}";
}
public static void main(String[] args) {
Person person = new Person("John", 25);
System.out.println("Before: " + person);
person.updatePersonNative();
System.out.println("After: " + person);
}
static {
System.loadLibrary("PersonLibrary");
}
}
Native Implementation:
#include <jni.h>
#include <stdio.h>
JNIEXPORT void JNICALL Java_Person_updatePersonNative
(JNIEnv *env, jobject personObj) {
jclass personClass = (*env)->GetObjectClass(env, personObj);
// Get field IDs
jfieldID nameField = (*env)->GetFieldID(env, personClass, "name", "Ljava/lang/String;");
jfieldID ageField = (*env)->GetFieldID(env, personClass, "age", "I");
if (nameField == NULL || ageField == NULL) {
return; // Field not found
}
// Get field values
jstring name = (*env)->GetObjectField(env, personObj, nameField);
jint age = (*env)->GetIntField(env, personObj, ageField);
// Convert Java string to C string
const char *nameStr = (*env)->GetStringUTFChars(env, name, NULL);
printf("Current person: %s, %d years old\n", nameStr, age);
(*env)->ReleaseStringUTFChars(env, name, nameStr);
// Update fields
jstring newName = (*env)->NewStringUTF(env, "Modified in Native");
(*env)->SetObjectField(env, personObj, nameField, newName);
(*env)->SetIntField(env, personObj, ageField, age + 1);
// Call Java methods
jmethodID toStringMethod = (*env)->GetMethodID(env, personClass, "toString", "()Ljava/lang/String;");
if (toStringMethod != NULL) {
jstring result = (*env)->CallObjectMethod(env, personObj, toStringMethod);
const char *resultStr = (*env)->GetStringUTFChars(env, result, NULL);
printf("After modification: %s\n", resultStr);
(*env)->ReleaseStringUTFChars(env, result, resultStr);
}
}
7. Exception Handling in JNI
JNIEXPORT void JNICALL Java_ExceptionExample_riskyOperation
(JNIEnv *env, jobject obj) {
jclass exceptionClass = (*env)->FindClass(env, "java/lang/IllegalArgumentException");
// Check if an exception is already pending
if ((*env)->ExceptionCheck(env)) {
(*env)->ExceptionDescribe(env); // Print exception info
(*env)->ExceptionClear(env); // Clear the exception
}
// Throw new exception from native code
(*env)->ThrowNew(env, exceptionClass, "Error from native code");
// After throwing exception, native function should return immediately
return;
}
// Safe method with exception handling
JNIEXPORT jstring JNICALL Java_ExceptionExample_safeStringOperation
(JNIEnv *env, jobject obj, jstring input) {
if (input == NULL) {
jclass nullPointerException = (*env)->FindClass(env, "java/lang/NullPointerException");
(*env)->ThrowNew(env, nullPointerException, "Input string is null");
return NULL;
}
const char *nativeString = (*env)->GetStringUTFChars(env, input, NULL);
if (nativeString == NULL) {
return NULL; // OutOfMemoryError already thrown
}
// Process the string...
(*env)->ReleaseStringUTFChars(env, input, nativeString);
// Check for exceptions before returning
if ((*env)->ExceptionCheck(env)) {
return NULL;
}
return (*env)->NewStringUTF(env, "Operation completed successfully");
}
8. Advanced JNI Features
Global and Local References
JNIEXPORT void JNICALL Java_ReferenceExample_handleReferences
(JNIEnv *env, jobject obj) {
// Local reference (automatically freed when native method returns)
jstring localString = (*env)->NewStringUTF(env, "Local reference");
// Global reference (must be explicitly managed)
jstring globalString = (*env)->NewGlobalRef(env, localString);
// Weak global reference (doesn't prevent garbage collection)
jstring weakString = (*env)->NewWeakGlobalRef(env, localString);
// Use references...
// Delete global references when no longer needed
(*env)->DeleteGlobalRef(env, globalString);
(*env)->DeleteWeakGlobalRef(env, weakString);
// Local references are automatically deleted, but you can delete explicitly
(*env)->DeleteLocalRef(env, localString);
}
Threading in JNI
// Each thread must attach to JVM before calling JNI functions
JavaVM *jvm; // Should be cached when JVM starts
void* nativeThread(void *arg) {
JNIEnv *env;
// Attach current thread to JVM
(*jvm)->AttachCurrentThread(jvm, (void**)&env, NULL);
// Now you can safely call JNI functions
jstring result = (*env)->NewStringUTF(env, "From native thread");
// Detach thread when done
(*jvm)->DetachCurrentThread(jvm);
return NULL;
}
9. Best Practices and Pitfalls
Do's and Don'ts
✅ DO:
- Always check for exceptions after JNI calls
- Release resources (strings, arrays, references) properly
- Use
GetStringCritical/ReleaseStringCriticalfor performance-critical code - Cache field and method IDs when possible
- Validate parameters before use
❌ DON'T:
- Don't make assumptions about JVM implementation
- Don't call JNI functions in signal handlers
- Don't hold references longer than necessary
- Don't ignore return values and error codes
- Don't perform lengthy operations in JNI without checking for exceptions
Performance Tips
// Cache field and method IDs (expensive operation)
jclass cachedClass = NULL;
jfieldID cachedField = NULL;
JNIEXPORT void JNICALL Java_PerformanceExample_optimizedMethod
(JNIEnv *env, jobject obj) {
if (cachedClass == NULL) {
cachedClass = (*env)->FindClass(env, "java/lang/String");
// Make global reference if used across multiple calls
cachedClass = (*env)->NewGlobalRef(env, cachedClass);
}
if (cachedField == NULL) {
cachedField = (*env)->GetFieldID(env, cachedClass, "value", "[B");
}
// Use cached IDs for better performance
}
10. Common JNI Patterns
Singleton Native Library Initialization
public class NativeLibrary {
private static volatile boolean loaded = false;
public static synchronized void load() {
if (!loaded) {
try {
System.loadLibrary("mylibrary");
loaded = true;
} catch (UnsatisfiedLinkError e) {
System.err.println("Failed to load native library: " + e.getMessage());
throw e;
}
}
}
}
Callback from Native to Java
public class CallbackExample {
private native void nativeMethod();
// Callback method called from native code
public void callbackFromNative(String message) {
System.out.println("Java received: " + message);
}
public static void main(String[] args) {
CallbackExample example = new CallbackExample();
example.nativeMethod();
}
static { System.loadLibrary("CallbackLibrary"); }
}
Native Implementation:
JNIEXPORT void JNICALL Java_CallbackExample_nativeMethod
(JNIEnv *env, jobject obj) {
jclass clazz = (*env)->GetObjectClass(env, obj);
jmethodID callbackMethod = (*env)->GetMethodID(env, clazz,
"callbackFromNative", "(Ljava/lang/String;)V");
if (callbackMethod != NULL) {
jstring message = (*env)->NewStringUTF(env, "Hello from native callback!");
(*env)->CallVoidMethod(env, obj, callbackMethod, message);
(*env)->DeleteLocalRef(env, message);
}
}
Summary
JNI provides powerful capabilities for Java-Native integration but requires careful attention to:
- Memory management and resource cleanup
- Exception handling and error checking
- Thread safety and proper JVM attachment
- Performance optimization through caching and proper API usage
While JNI enables important functionality, it should be used judiciously due to its complexity and potential for introducing stability and security issues. Always prefer pure Java solutions when possible, and reserve JNI for cases where native capabilities are truly necessary.