NDK Development Guide: Getting Started with Native Code

What is the Android NDK?
The Android Native Development Kit (NDK) is a toolset that allows you to implement parts of your Android app using languages such as C and C++. It provides native libraries, cross-compilers, build tools, and debugging capabilities. Unlike the Android SDK, which relies on the Java Virtual Machine (JVM) and Kotlin, the NDK enables direct execution of compiled machine code on the device’s CPU. This is essential for computationally intensive tasks—gaming engines, real-time audio processing, computer vision, cryptography, and scientific computing—where Dalvik or ART overhead becomes a bottleneck.
The NDK does not replace the SDK. It complements it. Most applications remain primarily Java or Kotlin, with native modules invoked through the Java Native Interface (JNI). Understanding when to use native code is critical; misusing it can increase complexity, reduce portability, and introduce security vulnerabilities.
Prerequisites and Installation
Before writing native code, ensure your environment is properly set up. You need:
- Android Studio (Arctic Fox or later recommended)
- NDK and CMake (install via SDK Manager > SDK Tools > NDK & CMake)
- LLDB for native debugging
- A physical device or emulator with ARM (or x86/x86_64) support
To verify installation, open a terminal and run:
ndk-build --versionIf you see version output, the NDK is available. Additionally, confirm CMake is installed:
cmake --versionProject Structure Overview
A standard NDK project contains:
app/
src/
main/
java/ # Java/Kotlin source
cpp/ # Native C/C++ source
jniLibs/ # Prebuilt .so libraries (optional)
build.gradle.kts # CMake or ndk-build configuration
CMakeLists.txt # CMake build script (if using CMake)The cpp/ directory holds your .c and .cpp files. The build system compiles them into shared libraries (.so files) for each target ABI (Application Binary Interface)—ARM, ARM64, x86, x86_64. These .so files are then packaged into your APK.
Choosing a Build System: CMake vs. ndk-build
Google recommends CMake for new NDK projects. It offers better readability, cross-platform support, and integration with Android Studio’s IDE features (code completion, refactoring, linting). The legacy ndk-build (based on Android.mk and Application.mk) is still supported but should be avoided for new development.
CMakeLists.txt Example
cmake_minimum_required(VERSION 3.22.1)
project("mynativeapp")
add_library(
mynative
SHARED
native-lib.cpp
)
find_library(
log-lib
log
)
target_link_libraries(
mynative
${log-lib}
)This script creates a shared library called libmynative.so from native-lib.cpp, linking it to the Android logging library.
Writing Your First Native Function
Create a file native-lib.cpp inside app/src/main/cpp/. Here is a minimal example:
#include
#include
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_mynativeapp_MainActivity_stringFromJNI(
JNIEnv* env,
jobject /* this */) {
std::string hello = "Hello from C++ via NDK!";
return env->NewStringUTF(hello.c_str());
}Key points:
- The function name follows the convention
Java_{package_name}_{class_name}_{method_name}. JNIEXPORTandJNICALLare macros required by JNI.JNIEnv*provides access to JNI functions (creating strings, arrays, calling Java methods).jobjectis a reference to the calling Java object (or class, for static methods).
In your MainActivity.kt (or Java), declare the native method and load the library:
class MainActivity : AppCompatActivity() {
companion object {
init {
System.loadLibrary("mynative")
}
}
external fun stringFromJNI(): String
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
findViewById(R.id.textView).text = stringFromJNI()
}
}Gradle Configuration for NDK
In your module-level build.gradle.kts (or build.gradle), add:
android {
compileSdk = 34
defaultConfig {
minSdk = 21
ndk {
abiFilters += listOf("arm64-v8a", "x86_64")
}
}
externalNativeBuild {
cmake {
path = file("CMakeLists.txt")
version = "3.22.1"
}
}
}The abiFilters limits which architectures are built, reducing APK size. For development, include all common ABIs; for release, target only the device’s architecture.
Understanding JNI: The Bridge Between Java and C++
JNI is the protocol for communication. Every native function receives at least two arguments: JNIEnv* (interface pointer) and jobject (or jclass for static methods). Through JNIEnv, you can:
- Access Java fields and methods:
GetFieldID,GetStaticMethodID - Convert between Java and native types:
NewStringUTF,GetStringUTFChars - Handle exceptions:
ExceptionCheck,ExceptionClear - Manage global references to prevent garbage collection
Memory Warning: JNI does not automatically clean up local references. In loops or functions that create many local references, use PushLocalFrame/PopLocalFrame or manually delete references with DeleteLocalRef.
Data Type Mapping
| Java Type | Native Type | JNI Signature |
|---|---|---|
| boolean | jboolean | Z |
| byte | jbyte | B |
| char | jchar | C |
| short | jshort | S |
| int | jint | I |
| long | jlong | J |
| float | jfloat | F |
| double | jdouble | D |
| void | void | V |
| String | jstring | Ljava/lang/String; |
| Object[] | jobjectArray | [Ljava/lang/Object; |
For arrays, use GetArrayElements and ReleaseArrayElements to access primitive arrays efficiently.
Building and Running
Connect a device or start an emulator. In Android Studio, click Run. The build system will:
- Run CMake to generate Makefiles for each ABI.
- Compile all native sources.
- Generate
.sofiles inbuild/intermediates/ndkBuild/. - Package them into the APK.
If you encounter a Library not loaded error at runtime, verify the System.loadLibrary call matches the library name (without the lib prefix and .so extension). Also check abiFilters match your device’s architecture.
Debugging Native Code
Debugging native crashes or logic errors requires LLDB. Android Studio integrates LLDB natively:
- Set breakpoints in
.cppfiles. - Use the Debug tool window to inspect variables (C++ types, not Java equivalents).
- View the native call stack for JNI transitions.
For memory issues (buffer overflows, use-after-free), enable AddressSanitizer (ASan) by adding to your CMakeLists.txt:
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address -fno-omit-frame-pointer")
set(CMAKE_LINKER_FLAGS "${CMAKE_LINKER_FLAGS} -fsanitize=address")Then, in your AndroidManifest.xml, add:
Common Performance Pitfalls
Excessive JNI crossings: Every call from Java to C++ (or vice versa) incurs overhead. Batch work in native functions rather than calling them in loops.
Global reference leaks: Every time you cache a
jclassorjmethodID, pin it as a global reference. Failing to do so can cause garbage collection to reclaim the class, leading to crashes.Incorrect type casting: Using
jdoublewherejintis expected corrupts the stack. Use the exact JNI signatures.Neglecting exception handling: A Java exception thrown in native code will not be automatically cleared. Always check
ExceptionCheck()after calling JNI functions.
ABIs and Performance
Android supports five primary ABIs: armeabi-v7a (32-bit ARM), arm64-v8a (64-bit ARM), x86 (32-bit Intel), x86_64 (64-bit Intel), and riscv64 (emerging). For modern apps, target arm64-v8a and x86_64. 32-bit ABIs are legacy; omitting them can reduce APK size significantly.
Each ABI corresponds to a different CPU instruction set. The NDK cross-compiler generates optimized machine code for each. Always test on the target architecture; x86 emulators are faster but ARM devices dominate the market.
Advanced Considerations
Prebuilt libraries: If you have third-party
.sofiles, place them inapp/src/main/jniLibs//. No JNI wrapper is needed if you call them from C++ only.C++ exceptions and RTTI: Enable with
-fexceptionsand-frttiinCMakeLists.txt. Disabled by default to reduce code size.Thread safety: JNI is not thread-safe per se. Each thread gets its own
JNIEnv*. UseAttachCurrentThreadandDetachCurrentThreadwhen spawning native threads.Shared libraries vs. static libraries: Static libraries compile into your
.so, increasing its size. Shared libraries stay separate, enabling code reuse across modules but requiring careful linking.
Security and Best Practices
- Never obfuscate native code as a substitute for encryption; native code can be reverse-engineered. Use NDK only for performance or legacy C/C++ libraries.
- Validate all input from Java in native code to prevent buffer overflows.
- Use
-D_FORTIFY_SOURCE=2and-O2for release builds. - Strip debug symbols from release
.sofiles using thestriptool in the NDK toolchain.
Testing Native Code
Write unit tests with Google Test (included in NDK). Place tests in app/src/test/cpp/ and configure a test executable in CMakeLists.txt using add_executable and enable_testing. For integration tests, use Android’s Instrumentation framework to call native methods from Java.
Resources and Toolchain
The NDK includes a standalone toolchain for cross-compilation outside Android Studio. Invoke it via:
$NDK/build/tools/make_standalone_toolchain.py --arch arm64 --api 34 --install-dir /tmp/my-toolchainThis is useful for CI pipelines or integrating existing C/C++ projects.
The latest NDK version r27 (as of 2026) supports LLVM 17, C++20, and improved LTO (Link-Time Optimization). Always check the release notes for deprecated functions and new features.





