I08 — NDK hello
Call C++ from Kotlin through JNI: a native library, a CMake build, an ABI split, and the JNI naming rules that make the call work. Then watch what happens when the ABI is wrong.
JNI is the seam where managed code meets native code, and it is where most Android crashes that are not Java exceptions actually happen. A minimal project makes the seam visible and the failure modes concrete.
What you are building#
An app that calls a C++ function to compute a string and an integer, with a CMake build and a declared ABI. A second button calls a function that deliberately crashes, so you can see a native crash and its tombstone.
Steps#
- Add the NDK and CMake
Install the NDK and CMake through the SDK manager, and point the module's build file at the CMake list. The build system generates the native build for each ABI.1
You should seeThe SDK manager installs both. - Write the C++ function
Write a function with
extern "C"andJNIEXPORT/JNICALL, returning ajstringbuilt withenv->NewStringUTF. The naming convention encodes the Java package and class.You should seeThe function compiles. - Declare the Kotlin method
Declare an
external funin the Kotlin class with the same name as the native symbol. Load the library in a companion object'sinitblock.You should seeThe method resolves at runtime. - Match the signature
Ensure the native name is
Java_<package>_<class>_<method>with underscores escaped. A mismatch is the most common JNI failure: the method throwsUnsatisfiedLinkErrorat first call, not at load.You should seeA working call. - Build for two ABIs
Configure
ndk.abiFiltersfor two architectures and inspect the APK. Each ABI has its ownlib/<abi>/directory, which is why APKs grow with native code.2You should seeTwo sets of native libraries in the APK. - Inspect the APK
Unzip the APK and list
lib/. The class loader picks the directory matching the device's ABI; a missing one is aUnsatisfiedLinkErroron that device only.You should seeA `lib/` directory with one subdirectory per ABI. - Cause a native crash
Dereference a null pointer in a native function. The app dies with a SIGSEGV, and the crash buffer has a tombstone with a backtrace.
You should seeA tombstone and a process death. - Read the tombstone
Pull the tombstone or read the crash buffer. The backtrace names your function and the library, which is more information than a Java stack trace gives you.
You should seeThe signal, the backtrace and the fault address.
The native side and the Kotlin declaration must agree on the name:
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_ndkhello_MainActivity_greet(JNIEnv* env, jobject /* this */) {
return env->NewStringUTF("hello from C++");
}class MainActivity : ComponentActivity() {
external fun greet(): String
companion object {
init { System.loadLibrary("hello") }
}
}unzip -l app/build/outputs/apk/debug/app-debug.apk | grep "lib/"
adb logcat -b crash -d
adb shell ls /data/tombstonesExample output
Illustrative output — paths and symbols differ.Verification#
The working call should return the string and the integer, and the crash should produce a tombstone.
The most instructive check is the ABI one: build for one ABI, install on a device with another, and
observe the UnsatisfiedLinkError that only that device sees.
What you learned#
- JNI is a naming and calling convention, not a compiler feature. The symbol name is the link.
- ABI is part of the APK's identity. A missing ABI is a device-specific crash, not a build error.
- Native crashes are process deaths. The evidence is a tombstone, not an exception.
- The NDK is for code that must be native, not for making code faster by default.
Read JNI, the NDK and ABIs for the ABI table, and Memory safety for why new platform code is moving to Rust.
Extend it#
- Add a native thread and call back into Kotlin with
AttachCurrentThread, then handle the detach correctly. - Add a 16 KB page-size check to the build and see whether your native library is compatible.
- Add AddressSanitizer to a debug build and reproduce a buffer overflow with a useful report.
Sources#
Sources
Every version number, API level, class name and date on this page is traced to one of these. Pages are re-verified against them; last_verified above says when.
- docsJNI tipsdeveloper.android.comaccessed 2026-10-27↩
- docsConfigure your builddeveloper.android.comaccessed 2026-10-27↩
- docsAndroid Runtime (ART) and Dalviksource.android.comaccessed 2026-10-27↩
- docs16 KB page size: developer optionsource.android.comaccessed 2026-10-27↩
- docsMemory safetysource.android.comaccessed 2026-10-27↩