NDK (Native Development Kit) is a toolkit for writing parts of your application in C and C++ for Android. Unlike the regular Android SDK, NDK compiles code into native libraries (.so) that work directly with the processor without a virtual machine layer. According to Google NDK Guides, 2026, NDK is used for high-performance computing, games, audio processing, and reusing existing C/C++ projects. JNI (Java Native Interface) connects native code with Kotlin and Java.
Key Takeaways
NDK (Native Development Kit) is a set of tools for cross-compiling C and C++ code into executable libraries for Android. NDK includes the Clang compiler, the libc++ standard library, Android API header files, and build utilities. The developer writes native code in C or C++, compiles it into .so files (shared objects), and connects them to the application via JNI.
NDK does not replace Android SDK, but complements it. All UI, lifecycle and system services remain in Kotlin or Java. Native code solves specific tasks: mathematical computations, cryptography, codecs, physics in games. Google recommends using NDK only when the SDK does not provide the required performance or access to hardware capabilities.
The history of NDK begins in 2009 (NDK r1). Over this time, the toolkit has evolved from an experimental set of scripts to a mature system with support for CMake, LLDB debugging and profiling. As of 2026, the current version is NDK r27 with Clang 19, full C++20 support and libc++ as the only standard library.
NDK gives developers access to Android's low-level capabilities through native APIs. The main use cases include: working with OpenGL ES and Vulkan for 3D graphics, audio processing via Oboe, cryptographic operations through BoringSSL and NEON optimizations for ARM. All these APIs are available from C/C++ and require NDK for compilation.
In addition, NDK allows reusing existing C/C++ libraries without rewriting them in Kotlin. This is especially relevant for projects with a long history in C++ — game engines (Unity, Unreal Engine), computer vision libraries (OpenCV) or cryptographic packages (OpenSSL). In such cases, NDK saves years of development.
| Component | Purpose |
|---|---|
| Clang | C/C++ compiler with C++20 support |
| libc++ | Standard C++ library (only one in NDK r27) |
| CMake | Build system for native libraries |
| LLDB | Native code debugger in Android Studio |
| ndk-build | Legacy build system (replaced by CMake) |
The choice between NDK and pure SDK depends on the task. Android SDK provides ready-made Java/Kotlin APIs for 90% of scenarios — networking, files, notifications, camera. NDK is used when these APIs do not provide the required performance or functionality. For example, real-time audio processing via SDK is possible but with 50–200 ms latency, while a native library via Oboe reduces latency to 5–15 ms.
APK size is another factor. Adding NDK increases the application size because each ABI architecture requires a separate .so library. However, using Android App Bundle (AAB) mitigates the problem: Google Play delivers only the library for the user's architecture. A typical native project adds 1–10 MB to the install size per device.
| Criterion | SDK (Kotlin/Java) | NDK (C/C++) |
|---|---|---|
| Performance | Medium (JIT/AOT compilation) | High (machine code) |
| Audio latency | 50–200 ms | 5–15 ms via Oboe |
| 3D graphics | Via Canvas/OpenGL Kotlin API | Vulkan/OpenGL directly |
| Development complexity | Low | High (memory management, JNI) |
| Code portability | Android only | Linux, Windows, macOS, iOS |
| APK size | Minimal | +1–10 MB per library |
NDK is installed separately from Android SDK via the SDK Manager. Inside the NDK directory are the toolchain (compilers), platform libraries, header files and utilities. NDK Toolchain includes Clang for cross-compilation to all target ABIs: arm64-v8a, armeabi-v7a, x86_64 and x86. The compiler automatically selects the required architecture based on CMake or ndk-build flags.
Android APIs for native code are provided by header files in the sysroot/usr/include directory. They contain declarations for all APIs available in native code: native_activity.h for Activity lifecycle, input.h for input events, sensor.h for sensors. Including these headers gives access to hardware capabilities without a JNI layer.
ndk/
toolchains/
llvm/prebuilt/windows-x86_64/
bin/ // Clang, ld, llvm-profdata
sysroot/ // Android API header files
platforms/
android-26/ // libc, libm, libdl for API 26
android-34/
sources/
cxx-stl/ // libc++ headers
build/
cmake/ // CMake toolchain file
The following groups of native APIs are available through NDK: Native App Glue (managing Activity lifecycle from C), OpenGL ES 3.2 and Vulkan 1.3 (graphics), Oboe (low-latency audio), Neural Networks API (on-device machine learning). Each API has a header file and a static/dynamic library as part of NDK.
Working with native APIs does not require JNI — functions are called directly from C/C++ code. However, interaction with the Kotlin UI still requires JNI. This hybrid architecture is common in game engines: graphics and physics in C++ (via Vulkan), menus and UI in Kotlin (via Jetpack Compose).
JNI (Java Native Interface) is a standard mechanism for calling native code from the Java/Kotlin virtual machine. The developer declares an external function in Kotlin with the external keyword and loads the .so library via System.loadLibrary. On the C++ side, the function is declared using the JNI naming convention, which encodes the package and class name.
class NativeBridge {
companion object {
init {
System.loadLibrary("native-lib")
}
}
external fun stringFromJNI(): String
external fun fibonacci(n: Int): Long
external fun processBuffer(data: ByteArray): ByteArray
}
// Usage in code
val bridge = NativeBridge()
println(bridge.stringFromJNI()) // "Hello from C++"
#include <jni.h>
#include <string>
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_app_NativeBridge_stringFromJNI(
JNIEnv* env, jobject /* this */) {
std::string hello = "Hello from C++ NDK";
return env->NewStringUTF(hello.c_str());
}
extern "C" JNIEXPORT jlong JNICALL
Java_com_example_app_NativeBridge_fibonacci(
JNIEnv* env, jobject /* this */, jint n) {
if (n <= 1) return n;
jlong a = 0, b = 1;
for (int i = 2; i <= n; i++) {
jlong temp = a + b;
a = b;
b = temp;
}
return b;
}
CMake is the primary build system for NDK, having replaced the legacy ndk-build. The CMakeLists.txt file describes source files, libraries and compilation flags. Android Studio automatically invokes CMake when building the project if externalNativeBuild is configured in build.gradle. CMake generates makefiles for each target ABI and compiles native code in parallel.
cmake_minimum_required(VERSION 3.22.1)
project("nativelib")
# Include header files
include_directories(src/main/cpp/include)
# Create native library
add_library(
native-lib
SHARED
src/main/cpp/native-lib.cpp
src/main/cpp/math_utils.cpp
src/main/cpp/audio_processor.cpp
)
# Link Android system libraries
target_link_libraries(
native-lib
android
log
OpenSLES
# libc++ linked automatically
)
# Optimization flags
target_compile_options(native-lib PRIVATE -O3 -Wall -Wextra)
android {
defaultConfig {
ndk {
// Target ABIs for build
abiFilters "arm64-v8a", "armeabi-v7a", "x86_64"
}
}
externalNativeBuild {
cmake {
path "CMakeLists.txt"
version "3.22.1"
}
}
buildTypes {
release {
externalNativeBuild {
cmake {
arguments "-DCMAKE_BUILD_TYPE=Release"
}
}
}
}
}
ABI (Application Binary Interface) is the machine code format that determines compatibility with the device's processor. Each ARM or x86 architecture has its own ABI. NDK compiles native code separately for each specified ABI. The most common ABIs as of 2026: arm64-v8a (99% of modern devices), armeabi-v7a (older 32-bit devices), x86_64 (emulator and Chromebook).
Specifying abiFilters in build.gradle limits the build to only the required architectures, reducing compilation time. For Google Play, it is recommended to include all ABIs that the library supports — this ensures it works on all devices. Google Play Console allows configuring ABI-specific APK delivery through App Bundle.
#include <jni.h>
#include <android/api-level.h>
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_app_NativeBridge_getABIInfo(
JNIEnv* env, jobject /* this */) {
#if defined(__arm__)
#if defined(__ARM_ARCH_7A__)
return env->NewStringUTF("armeabi-v7a");
#endif
#elif defined(__aarch64__)
return env->NewStringUTF("arm64-v8a");
#elif defined(__x86_64__)
return env->NewStringUTF("x86_64");
#elif defined(__i386__)
return env->NewStringUTF("x86");
#endif
return env->NewStringUTF("unknown");
}
| ABI | Architecture | Bitness | Devices |
|---|---|---|---|
| arm64-v8a | ARMv8-A | 64-bit | Almost all modern phones |
| armeabi-v7a | ARMv7-A | 32-bit | Older devices (pre-2020) |
| x86_64 | x86-64 | 64-bit | Emulator, Chromebook |
| x86 | x86 IA-32 | 32-bit | Legacy emulators |
Let us consider a practical example: a math library for working with floating-point numbers. Native C++ code performs calculations more efficiently than Kotlin, thanks to direct access to ARM NEON instructions and the absence of array bounds checks at runtime. This example demonstrates a typical NDK usage pattern — offloading heavy computations to the native layer.
#include <jni.h>
#include <cmath>
#include <vector>
extern "C" JNIEXPORT jfloatArray JNICALL
Java_com_example_app_NativeBridge_normalizeArray(
JNIEnv* env, jobject, jfloatArray input) {
jsize len = env->GetArrayLength(input);
jfloat* elements = env->GetFloatArrayElements(input, nullptr);
// Calculate mean and standard deviation
float sum = 0.0f, sumSq = 0.0f;
for (jsize i = 0; i < len; i++) {
sum += elements[i];
sumSq += elements[i] * elements[i];
}
float mean = sum / len;
float stddev = std::sqrt(sumSq / len - mean * mean);
// Normalization: (x - mean) / stddev
jfloat* result = new jfloat[len];
for (jsize i = 0; i < len; i++) {
result[i] = (elements[i] - mean) / stddev;
}
env->ReleaseFloatArrayElements(input, elements, JNI_ABORT);
jfloatArray output = env->NewFloatArray(len);
env->SetFloatArrayRegion(output, 0, len, result);
delete[] result;
return output;
}
For debugging native code, use the __android_log_print macro from the android/log.h library. Messages appear in Logcat alongside Java/Kotlin logs. The logging level (ANDROID_LOG_DEBUG, ANDROID_LOG_ERROR) helps filter messages.
#include <android/log.h>
#define LOG_TAG "NativeLib"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
extern "C" JNIEXPORT void JNICALL
Java_com_example_app_NativeBridge_processData(
JNIEnv* env, jobject, jint count) {
LOGD("Processing %d items", count);
for (int i = 0; i < count; i++) {
// Heavy processing
LOGD("Item %d processed", i);
}
LOGD("Processing complete");
}
Frequently Asked Questions
No. For most applications, the Kotlin SDK is sufficient. NDK is needed for high-performance tasks: real-time audio/video processing, 3D graphics, cryptography, or reusing existing C/C++ projects.
NDK uses Clang from the LLVM toolchain. Since NDK r23, GCC has been completely removed. Clang cross-compiles code for all Android ABIs: arm64-v8a, armeabi-v7a, x86_64, x86.
ABI (Application Binary Interface) is the machine code format that determines compatibility with the processor. NDK builds .so libraries for each ABI separately. arm64-v8a is the primary ABI for modern Android devices.
Yes. Android Studio supports LLDB — a native code debugger. You can set breakpoints in C++ files, view variables and the call stack. NDK and the LLDB plugin are required.
Each native library adds 100 KB to several MB to the APK. Each ABI requires a separate .so library. Android App Bundle delivers only the matching architecture to the user, reducing install size.
Summary
We will develop a mobile application turnkey
IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.
Read also