NDK Android: What It Is, Native Development Kit and JNI for C++

Author: IT Sectr Published: 2026-02-09 Reading time: 10 min

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 — a toolkit for compiling C/C++ code into native libraries for Android.
  • JNI — an interface for interaction between the Java/Kotlin virtual machine and native code.
  • CMake — the primary build system for NDK projects, configured via CMakeLists.txt.
  • ABI — the target processor architecture (arm64-v8a, armeabi-v7a, x86_64) for which the library is built.
  • NDK is not needed for most Android applications and is only used in tasks with high performance requirements.

What is NDK

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.

Key NDK Capabilities

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.

ComponentPurpose
ClangC/C++ compiler with C++20 support
libc++Standard C++ library (only one in NDK r27)
CMakeBuild system for native libraries
LLDBNative code debugger in Android Studio
ndk-buildLegacy build system (replaced by CMake)

NDK vs SDK: When Native Code is Needed

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.

SDK vs NDK Comparison by Criteria

CriterionSDK (Kotlin/Java)NDK (C/C++)
PerformanceMedium (JIT/AOT compilation)High (machine code)
Audio latency50–200 ms5–15 ms via Oboe
3D graphicsVia Canvas/OpenGL Kotlin APIVulkan/OpenGL directly
Development complexityLowHigh (memory management, JNI)
Code portabilityAndroid onlyLinux, Windows, macOS, iOS
APK sizeMinimal+1–10 MB per library

NDK Architecture: Tools and Libraries

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 Directory Structure

text
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

Native Android APIs

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: How Kotlin Calls C++

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.

Declaring a Native Function in Kotlin

kotlin
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++"

JNI Function Implementation in C++

cpp
#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;
}

Setting up CMakeLists.txt for NDK

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.

Example CMakeLists.txt for a Native Library

cmake
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)

Configuring build.gradle for NDK

groovy
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 and Multi-platform Build

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.

Determining Device ABI in Native Code

cpp
#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");
}
ABIArchitectureBitnessDevices
arm64-v8aARMv8-A64-bitAlmost all modern phones
armeabi-v7aARMv7-A32-bitOlder devices (pre-2020)
x86_64x86-6464-bitEmulator, Chromebook
x86x86 IA-3232-bitLegacy emulators

Native C++ Code Examples

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.

Mathematical Operations in Native Code

cpp
#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;
}

Logging from Native Code

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.

cpp
#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

Is NDK mandatory for Android development?

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.

Which compiler does NDK use?

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.

What is ABI in the context of NDK?

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.

Can C++ code be debugged through NDK?

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.

How does NDK affect APK size?

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

  • NDK — a set of tools for cross-compiling C/C++ code into native libraries for Android using the Clang compiler.
  • JNI — an interface connecting Kotlin/Java with native code through the external keyword and function naming conventions.
  • CMake — the primary NDK build system, configured via CMakeLists.txt and externalNativeBuild in Gradle.
  • ABI defines the processor architecture — arm64-v8a, armeabi-v7a, x86_64. Each ABI requires a separate library build.
  • Use NDK only for tasks requiring high performance: games, audio, graphics, cryptography, on-device machine learning.
  • Google Play supports ABI splitting through App Bundle: the user receives only the library for their device architecture.
  • Native libraries significantly increase APK size but provide maximum performance and low latency for critical operations.

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.

Discuss the project

Read also