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()
~/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()
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
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.
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()
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.