Table of Contents

  1. NDK Toolchain File
  2. ABI Targeting
  3. Android API Level
  4. Gradle CMake Integration
  5. Native Activity Setup
  6. JNI Bridging
  7. STL Selection
  8. APK/AAB Packaging
  9. Testing on Emulator
Back to CMake Mastery Series

Android NDK

June 4, 2026 Wasil Zafar 12 min read

Build native C++ libraries for Android using CMake with the NDK toolchain — ABI targeting, Gradle integration, JNI bridging, STL selection, and packaging into APK/AAB bundles.

NDK Toolchain File

The Android NDK ships with a CMake toolchain file that configures the entire cross-compilation environment — compiler, sysroot, target architecture, and Android-specific settings. This toolchain file is the foundation for all Android native builds with CMake.

# Basic NDK cross-compilation
# $NDK is your NDK installation path (e.g., ~/Android/Sdk/ndk/26.1.10909125)
cmake -G Ninja \
    -DCMAKE_TOOLCHAIN_FILE=$NDK/build/cmake/android.toolchain.cmake \
    -DANDROID_ABI=arm64-v8a \
    -DANDROID_PLATFORM=android-24 \
    -DCMAKE_BUILD_TYPE=Release \
    -S . -B build-android

cmake --build build-android
# CMakeLists.txt — works for both host and Android builds
cmake_minimum_required(VERSION 3.21)
project(AndroidNativeLib LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# Detect Android build
if(ANDROID)
    message(STATUS "Building for Android")
    message(STATUS "  ABI: ${ANDROID_ABI}")
    message(STATUS "  API Level: ${ANDROID_PLATFORM}")
    message(STATUS "  NDK: ${ANDROID_NDK}")
    message(STATUS "  STL: ${ANDROID_STL}")
endif()

# Create shared library (loaded via System.loadLibrary in Java/Kotlin)
add_library(native-lib SHARED
    src/native-lib.cpp
    src/audio-engine.cpp
    src/renderer.cpp
)

# Android-specific libraries
if(ANDROID)
    find_library(LOG_LIB log)        # Android logging
    find_library(ANDROID_LIB android) # Native Android APIs
    find_library(EGL_LIB EGL)        # OpenGL ES context
    find_library(GLES_LIB GLESv3)    # OpenGL ES 3.x

    target_link_libraries(native-lib PRIVATE
        ${LOG_LIB}
        ${ANDROID_LIB}
        ${EGL_LIB}
        ${GLES_LIB}
    )
endif()
NDK Path Discovery: Android Studio's SDK Manager installs the NDK under ~/Android/Sdk/ndk/VERSION/. Set the ANDROID_NDK environment variable or use the ndk.dir property in local.properties. The toolchain file is always at $NDK/build/cmake/android.toolchain.cmake.

ABI Targeting

Android devices use different CPU architectures. The ANDROID_ABI variable selects which architecture to build for. Modern Android requires at minimum arm64-v8a (64-bit ARM) and optionally x86_64 for emulator testing.

# Build for all common ABIs
for ABI in arm64-v8a armeabi-v7a x86_64; do
    cmake -G Ninja \
        -DCMAKE_TOOLCHAIN_FILE=$NDK/build/cmake/android.toolchain.cmake \
        -DANDROID_ABI=$ABI \
        -DANDROID_PLATFORM=android-24 \
        -DCMAKE_BUILD_TYPE=Release \
        -S . -B build-$ABI
    cmake --build build-$ABI
done
# ABI-specific optimizations
if(ANDROID)
    if(ANDROID_ABI STREQUAL "arm64-v8a")
        # NEON is always available on arm64
        target_compile_definitions(native-lib PRIVATE HAS_NEON)
    elseif(ANDROID_ABI STREQUAL "armeabi-v7a")
        # NEON optional on 32-bit ARM (but common since Android 5.0)
        target_compile_options(native-lib PRIVATE -mfpu=neon)
        target_compile_definitions(native-lib PRIVATE HAS_NEON)
    elseif(ANDROID_ABI STREQUAL "x86_64")
        # SSE4.2 available on x86_64 emulators
        target_compile_options(native-lib PRIVATE -msse4.2)
    endif()
endif()
Android Gotcha: Google Play requires 64-bit native code since August 2019. Always include arm64-v8a. The armeabi-v7a ABI is still useful for legacy 32-bit devices but is being phased out. The x86 ABI (32-bit Intel) is effectively dead — only x86_64 matters for emulator testing.

Android API Level

The API level determines which Android system APIs are available to your native code. It's the native equivalent of minSdkVersion in your Gradle configuration. Higher API levels expose more NDK functions but limit device compatibility.

cmake_minimum_required(VERSION 3.21)
project(ApiLevelDemo LANGUAGES CXX)

# Set minimum API level (android-24 = Android 7.0 Nougat)
# The toolchain file uses ANDROID_PLATFORM or ANDROID_NATIVE_API_LEVEL
# Both are equivalent: android-24 or 24

if(ANDROID)
    # Check API level for conditional compilation
    if(ANDROID_NATIVE_API_LEVEL GREATER_EQUAL 26)
        target_compile_definitions(native-lib PRIVATE
            HAS_AAUDIO          # AAudio available in API 26+
            HAS_NEURAL_NETWORKS  # NNAPI available in API 27+
        )
    endif()

    if(ANDROID_NATIVE_API_LEVEL GREATER_EQUAL 30)
        target_compile_definitions(native-lib PRIVATE
            HAS_OBOE_AAUDIO_MMAP  # Low-latency audio in API 30+
        )
    endif()
endif()

Gradle CMake Integration

Android Studio projects use Gradle to orchestrate the build. The externalNativeBuild block in build.gradle tells Gradle to invoke CMake with your native code. This is the standard way to integrate C++ code into Android apps.

// app/build.gradle.kts (Kotlin DSL)
android {
    namespace = "com.example.nativeapp"
    compileSdk = 34

    defaultConfig {
        minSdk = 24
        targetSdk = 34

        ndk {
            // ABIs to include in the APK
            abiFilters += listOf("arm64-v8a", "armeabi-v7a", "x86_64")
        }

        externalNativeBuild {
            cmake {
                // CMake arguments passed at configure time
                arguments += listOf(
                    "-DANDROID_STL=c++_shared",
                    "-DENABLE_LOGGING=ON"
                )
                // C++ flags applied to all targets
                cppFlags += listOf("-std=c++20", "-fexceptions", "-frtti")
            }
        }
    }

    externalNativeBuild {
        cmake {
            path = file("src/main/cpp/CMakeLists.txt")
            version = "3.22.1"  // CMake version bundled with NDK
        }
    }
}
# src/main/cpp/CMakeLists.txt
cmake_minimum_required(VERSION 3.21)
project(NativeApp LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 20)

add_library(native-lib SHARED
    native-lib.cpp
    engine/audio.cpp
    engine/graphics.cpp
)

find_library(log-lib log)
target_link_libraries(native-lib PRIVATE ${log-lib})

Native Activity Setup

For applications written entirely in C/C++ (games, rendering demos), Android provides NativeActivity. Combined with the native_app_glue library from the NDK, you can create apps without any Java/Kotlin code.

cmake_minimum_required(VERSION 3.21)
project(NativeActivityDemo LANGUAGES CXX)

# native_app_glue is provided as source in the NDK
add_library(native_app_glue STATIC
    ${ANDROID_NDK}/sources/android/native_app_glue/android_native_app_glue.c
)
target_include_directories(native_app_glue PUBLIC
    ${ANDROID_NDK}/sources/android/native_app_glue
)

# Main application library
add_library(game SHARED src/main.cpp src/renderer.cpp)
target_link_libraries(game PRIVATE
    native_app_glue
    android     # Native Activity APIs
    EGL         # EGL context management
    GLESv3      # OpenGL ES 3.x
    log         # __android_log_print
)
// src/main.cpp — NativeActivity entry point
#include <android_native_app_glue.h>
#include <android/log.h>
#include <EGL/egl.h>
#include <GLES3/gl3.h>

#define LOG_TAG "NativeGame"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)

void android_main(struct android_app* app) {
    LOGI("Native activity starting");

    // Event loop
    while (true) {
        int events;
        struct android_poll_source* source;

        while (ALooper_pollAll(0, nullptr, &events,
                              reinterpret_cast<void**>(&source)) >= 0) {
            if (source) source->process(app, source);
            if (app->destroyRequested) return;
        }

        // Render frame
        // ... EGL swap, GL draw calls ...
    }
}

JNI Bridging

The Java Native Interface (JNI) connects Java/Kotlin code to native C++ libraries. CMake builds the shared library that System.loadLibrary() loads at runtime. JNI function naming follows a strict convention that maps to the Java package and class structure.

// native-lib.cpp — JNI functions callable from Kotlin/Java
#include <jni.h>
#include <string>
#include <android/log.h>

#define TAG "NativeLib"

extern "C" {

// Function name format: Java___
// Package: com.example.app → com_example_app
// Class: MainActivity
// Method: getGreeting

JNIEXPORT jstring JNICALL
Java_com_example_app_MainActivity_getGreeting(
    JNIEnv* env,
    jobject /* this */,
    jstring name
) {
    const char* nameChars = env->GetStringUTFChars(name, nullptr);
    std::string greeting = "Hello from C++, " + std::string(nameChars) + "!";
    env->ReleaseStringUTFChars(name, nameChars);
    return env->NewStringUTF(greeting.c_str());
}

JNIEXPORT jint JNICALL
Java_com_example_app_NativeEngine_processFrame(
    JNIEnv* env,
    jobject /* this */,
    jbyteArray frameData,
    jint width,
    jint height
) {
    jbyte* data = env->GetByteArrayElements(frameData, nullptr);
    jsize length = env->GetArrayLength(frameData);

    // Process the frame data...
    int result = 0; // computation result

    env->ReleaseByteArrayElements(frameData, data, JNI_ABORT);
    return result;
}

} // extern "C"

STL Selection

The NDK supports two C++ Standard Library implementations. The choice between c++_shared and c++_static affects library size, compatibility, and how multiple native libraries in the same app interact.

# Shared STL — single libc++_shared.so in APK
cmake -G Ninja \
    -DCMAKE_TOOLCHAIN_FILE=$NDK/build/cmake/android.toolchain.cmake \
    -DANDROID_ABI=arm64-v8a \
    -DANDROID_STL=c++_shared \
    -S . -B build

# Static STL — linked into each .so (larger, but self-contained)
cmake -G Ninja \
    -DCMAKE_TOOLCHAIN_FILE=$NDK/build/cmake/android.toolchain.cmake \
    -DANDROID_ABI=arm64-v8a \
    -DANDROID_STL=c++_static \
    -S . -B build
Android Decision Guide
c++_shared vs c++_static

Use c++_shared when your app loads multiple native libraries (e.g., your lib + a third-party .so). All libraries share one STL instance, avoiding ODR violations and reducing total APK size. Use c++_static when you have exactly one native library or when building a standalone .so distributed as an SDK — it avoids STL version conflicts with the consuming app.

Critical Rule: Never mix c++_static across multiple shared libraries in the same APK. Each library gets its own copy of the STL with separate global state — passing C++ objects (std::string, std::vector) between libraries will crash. Always use c++_shared when multiple .so files coexist.

APK/AAB Packaging

After CMake builds your native libraries, Gradle packages them into the correct location within the APK (lib/<abi>/) or AAB. You can also build standalone and manually place libraries for non-Gradle workflows.

# Standard Gradle workflow (handles everything)
./gradlew assembleRelease    # Produces APK with native libs
./gradlew bundleRelease      # Produces AAB for Play Store

# Manual: inspect native library placement in APK
unzip -l app-release.apk | grep "\.so"
# lib/arm64-v8a/libnative-lib.so
# lib/arm64-v8a/libc++_shared.so
# lib/armeabi-v7a/libnative-lib.so
# lib/armeabi-v7a/libc++_shared.so

# Standalone build without Gradle — copy into jniLibs
mkdir -p app/src/main/jniLibs/arm64-v8a
cp build-arm64/libnative-lib.so app/src/main/jniLibs/arm64-v8a/
# Strip symbols for release (reduce .so size by 60-80%)
# The NDK toolchain does this automatically for Release builds
# but you can configure it explicitly:
if(ANDROID AND CMAKE_BUILD_TYPE STREQUAL "Release")
    add_custom_command(TARGET native-lib POST_BUILD
        COMMAND ${CMAKE_STRIP} --strip-unneeded $<TARGET_FILE:native-lib>
        COMMENT "Stripping native-lib for release"
    )
endif()

Testing on Emulator

The Android emulator runs x86_64 images for performance on Intel/AMD hosts. You can push native executables directly to the emulator for quick testing without a full APK build cycle.

# Build a test executable for the emulator (x86_64)
cmake -G Ninja \
    -DCMAKE_TOOLCHAIN_FILE=$NDK/build/cmake/android.toolchain.cmake \
    -DANDROID_ABI=x86_64 \
    -DANDROID_PLATFORM=android-24 \
    -DCMAKE_BUILD_TYPE=Debug \
    -S . -B build-test

cmake --build build-test --target unit_tests

# Push and run on emulator
adb push build-test/unit_tests /data/local/tmp/
adb shell chmod +x /data/local/tmp/unit_tests
adb shell /data/local/tmp/unit_tests

# For shared STL, push libc++_shared.so too
adb push $NDK/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/x86_64-linux-android/libc++_shared.so /data/local/tmp/
adb shell "cd /data/local/tmp && LD_LIBRARY_PATH=. ./unit_tests"
# CMakeLists.txt with test executable option
option(BUILD_TESTS "Build native test executables" OFF)

if(BUILD_TESTS AND ANDROID)
    add_executable(unit_tests
        tests/main.cpp
        tests/test_audio.cpp
        tests/test_math.cpp
    )
    target_link_libraries(unit_tests PRIVATE native-lib log)
endif()
Fast Iteration Tip: For rapid native development, build test executables for x86_64 and push them directly to the emulator with adb. This avoids the full Gradle→APK→Install cycle and gives you sub-second feedback loops during development.