DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Newsletter
Log In / Join
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Related

  • Foreign Function and Memory API: Modernizing Native Interfacing in Java 17
  • Taming the Virtual Threads: Embracing Concurrency With Pitfall Avoidance
  • Java 21 SequenceCollection: Unleash the Power of Ordered Collections
  • Cordova: Communicating Between JavaScript and Java

Trending

  • Event Sourcing in Distributed Systems: Architecture, Consistency, and Fault-Tolerant Projections
  • Data Integration Landscape 2026: Event Streaming, API, and Batch in the Era of Agentic AI
  • How to Build Your Own AI SRE From Scratch
  • Delegated Transport, Forked Authorization: Where Chat and AI-Agent Integrations Actually Break

Draft This article hasn't been published, so readers can't see it.

In moderation This article is waiting for review, so readers can't see it yet.

Deleted This article has been deleted, so readers can't see it.

  1. DZone
  2. Coding
  3. Java
  4. Diving into JNI: My Messy Adventures With C++ in Android

Diving into JNI: My Messy Adventures With C++ in Android

JNI is powerful but tricky. Automate boilerplate with generators, carefully manage references, test with CheckJNI, and embrace the chaos; it gets satisfying.

By 
Ruslan Vidzert
·
Oct. 10, 25 · Analysis
Post on X (opens in a new tab) Share on LinkedIn (opens in a new tab)
4.4K Views

Join the DZone community Save articles, join the discussion, and download Refcards and Trend Reports with a free account.

Join for free Already a member? Log in

So, I've been deep in the trenches with JNI lately (yeah, that Java Native Interface stuff) while working on a project where we had to plug a C++ AI assistant into our Android app. At first, it felt like stepping into a weird twilight zone — half Java, half C++, and all these random edge cases you never think about until you hit them. I remember staring at the stack trace for what felt like hours, realizing that one tiny missed DeleteLocalRef was enough to crash the whole app. Thought I'd share what actually tripped me up, what worked, and some ways to make life a little less miserable if you ever have to do this.

What the Hell Is JNI Anyway?

JNI is basically the bridge that lets Java (or Kotlin) talk to C/C++ code and vice versa. On Android, it’s the only real way to get heavy lifting done efficiently or access low-level APIs that Java/Kotlin just can't reach. Honestly, the first time I tried to wrap my head around it, I felt like I was learning a new language on top of Java and C++ at the same time.

Some key points I painfully learned:

  • You can call C++ from Java, and Java from C++. Sounds simple, but the devil is in the details — especially when managing references and lifetimes.
  • Despite newer stuff like Project Panama or JNA, Android devs still live and die by JNI. Trying to convince anyone on my team to switch to newer abstractions was a losing battle.

The classic workflow:

  • Declare a native (Java) or external (Kotlin) method
  • Generate a .h header file with javac -h
  • Implement your C++ function using JNIEnv
  • Build with CMake / ndk-build and load the .so in Gradle

Example in Kotlin:

Kotlin
 
object Greeting {
   init {
       System.loadLibrary("libgreeting")
   }
   private external fun sayHello(name: String)
}


And Java:

Java
 
public class Greeting {
   static { System.loadLibrary("greeting"); }
   private static native void sayHello(@NonNull String name);
}


Building JNI projects

Options I've used:

  • CMake: standard, integrates nicely with Gradle
  • ndk-build: old-school, works fine for legacy stuff
  • External build: if your C++ is huge/messy, let a separate build pipeline spit out .so files

Early on, I tried mixing Gradle and an external build manually, and the first APK I got wouldn’t even load. Spent a day chasing UnsatisfiedLinkError before realizing the paths were wrong. Wrapping the external build in a Gradle plugin finally made everything consistent. That felt like the first small victory in this jungle. My initial structure:

Project structure

Boilerplate Hell and Generators

JNI is boring as hell if you’re writing everything by hand: method stubs, reference handling, glue code... it stacks up. I remember writing a single interface manually and spending a full morning just fixing subtle bugs in jstring conversions. My hairline was receding faster than the app was building.

Some popular generators:

  • Djinni (Dropbox, cross-platform, kinda dead since 2020)
  • SWIG (classic wrapper generator for tons of languages)
  • JNI Zero (Chromium, annotation-based, modern, but tied to Chromium build)

In my case, we adapted a JNI Zero-style generator into Gradle. Watching the generator spit out working JNI stubs felt like magic after days of repetitive coding. Saved a ton of hours and prevented countless silly bugs.

My Example: An AI Assistant in an Android App

Imagine integrating a C++ AI assistant into an app. Instead of rewriting logic in Java/Kotlin, you just reuse the same C++ code everywhere. JNI is unavoidable.

Scale:

  • ~5k lines of C++
  • ~50k lines of Java/Kotlin
  • Generator produces JNI wrappers automatically

The flow:

  • In Kotlin, create listener with @JNINamespace + native methods
  • Annotate methods called from C++ with @CalledByNative
  • Generator spits out simplified JNI stubs
  • Implement listener in C++ using the generated headers
  • Initialize listener in C++ so Java UI gets updates

Seeing the generator work felt like watching the pieces of a puzzle finally click into place. Suddenly, instead of worrying about glue code, we could focus on features. It was surprisingly satisfying to see the C++ assistant pop up in the UI without crashes.

CI/CD and Gradle Tricks

Integrating C++ into a proper Android build pipeline was a lesson in patience:

  • Wrap external builds in Gradle tasks so the pipeline is aware of native dependencies
  • Output .so files to jniLibs folders for automatic packaging
  • Use variant-aware tasks for debug/release flavors to avoid mismatched builds
  • Cache compiled .so artifacts in CI to cut build times drastically
  • Run small instrumentation tests after every build to catch subtle JNI errors early

I remember one morning when a CI build failed due to mismatched .so files. It took me three cups of coffee and a couple of “why is this happening” mutterings to fix it. After wrapping everything neatly, the pipeline became almost boringly reliable.

Gotchas

  • References: Always clean up local references. Local Reference Table overflow is silent until your app crashes randomly.
  • Compiler/linker quirks: If the compiler thinks a JNI function isn't used, it strips it. Workaround: call a dummy noinline function in JNI_OnLoad.
  • CheckJNI mode: Run in emulator or rooted device. Catches thread misuse, invalid references, UTF issues, null pointers... basically all the weird stuff that makes you pull your hair out.
  • Unit tests: Instrumentation tests covering the JNI layer are lifesavers. I remember one subtle bug that caused a crash only on Pixel devices – it took tests like that to catch it before users did.

TL;DR

JNI is tricky but powerful. If you:

  • Automate repetitive code with generators
  • Carefully manage references
  • Test with CheckJNI and instrumented tests

... you'll avoid most pain.

Even an imaginary AI assistant shows how you can scale JNI: fast updates, unified C++ code, good performance, and way less boilerplate headaches.

Honestly, after fighting through the initial chaos, JNI starts to feel... kinda satisfying — like taming a beast. Some days I even miss the weird thrill of debugging a random crash caused by a missing reference. It’s like a mix of fear, frustration, and eventual victory that only JNI can deliver.

Java Native Interface Android (robot) Java (programming language)

Opinions expressed by DZone contributors are their own.

Related

  • Foreign Function and Memory API: Modernizing Native Interfacing in Java 17
  • Taming the Virtual Threads: Embracing Concurrency With Pitfall Avoidance
  • Java 21 SequenceCollection: Unleash the Power of Ordered Collections
  • Cordova: Communicating Between JavaScript and Java

Partner Resources

×

Comments

Loading comments

The comments didn't load

Please try again in a moment.

Likes

Loading

The next likes didn't load. Please try again.

The likes didn't load

Please try again in a moment.

See who liked this

Log in or join DZone for free to see who liked this article.

Log in Join for free
  • RSS
  • X
  • Facebook

ABOUT US

  • About DZone
  • Support and feedback
  • Community research

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 215
  • Nashville, TN 37211
  • [email protected]

Let's be friends:

  • RSS
  • X
  • Facebook