Skip to main content
The Java Native Interface (JNI) is the bridge that allows Java and Kotlin code to call and be called by native C/C++ code. It’s the fundamental mechanism that enables NDK development on Android.

What is JNI?

JNI is a standardized API that provides:
  • Two-way communication: Java/Kotlin can call C/C++ functions, and C/C++ can call back into Java/Kotlin
  • Type system mapping: Conversions between Java types and C types
  • Object access: Native code can create, inspect, and modify Java objects
  • Exception handling: Propagate and handle exceptions across the language boundary
  • Memory management: Coordinate between garbage-collected Java heap and manually-managed native memory
JNI is a standard Java feature, not Android-specific. However, Android makes heavy use of JNI for its native components.

How JNI works

Basic architecture

The JNI layer handles:
  1. Function dispatch: Routing Java native method calls to C/C++ implementations
  2. Type marshaling: Converting data between Java and C representations
  3. Reference management: Tracking object references across the boundary
  4. Thread synchronization: Ensuring thread-safe access to JVM state

Native method declaration

In Java/Kotlin, declare methods with the native keyword:

Native implementation

C/C++ implementations follow a naming convention:
C++ can use a cleaner syntax with the -> operator instead of (*env)->. C++ also allows automatic JNI method registration.

JNI types and signatures

Type mappings

Java types map to corresponding C types:

Method signatures

Method signatures encode parameter and return types:
Incorrect signatures will cause runtime errors. Use javap -s to see the exact signature of Java methods.

JNIEnv interface

The JNIEnv* pointer provides access to all JNI functions:

Common operations

Reference management

JNI has three types of object references:

Local references

Automatically managed within native method scope:
Local references are freed when the native method returns. Do not store them for later use. The JVM has a limit on local references (typically 512).

Global references

Persist across native method calls:

Weak global references

Like global references but don’t prevent garbage collection:

Thread considerations

JNIEnv is thread-local

Each thread has its own JNIEnv*:
Never cache JNIEnv* across threads. Each thread must use its own JNIEnv*.

Attaching native threads

Native threads must attach to access JNI:

Performance considerations

JNI call overhead

Crossing the JNI boundary has cost:

Direct buffers

For large data transfer, use direct ByteBuffers:

Critical sections

For short, performance-critical array access:
Between GetPrimitiveArrayCritical and ReleasePrimitiveArrayCritical, do not:
  • Call any JNI functions
  • Block or wait
  • Allocate memory
This may disable the garbage collector.

Best practices

Check for errors

Always check for NULL and exceptions:

Use helper macros

Simplify error checking:

Cache class and method IDs

Lookups are expensive, cache them:

Next steps

Now that you understand JNI:
  • Learn about ABIs for multi-architecture builds
  • Understand bionic C library specifics
  • Explore build systems for compiling native code
For complete JNI reference, see Oracle’s JNI Specification.