Java Native Interface (JNI) Basics in Java

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

  1. Write Java class with native method declarations
  2. Compile Java class
  3. Generate C/C++ header using javac -h
  4. Implement native method in C/C++
  5. Compile native library
  6. 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 TypeNative TypeDescription
booleanjboolean8-bit unsigned
bytejbyte8-bit signed
charjchar16-bit unsigned
shortjshort16-bit signed
intjint32-bit signed
longjlong64-bit signed
floatjfloat32-bit float
doublejdouble64-bit float
voidvoidVoid type

Reference Types

Java TypeNative Type
Objectjobject
Stringjstring
Classjclass
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/ReleaseStringCritical for 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.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper