Skip to main content
This page covers common problems NDK developers encounter and provides practical solutions. Many of these issues have straightforward fixes once you understand the underlying cause.

Build issues

This error occurs when the dynamic linker cannot find a required shared library.Common causes:
  • Library not packaged in the APK
  • ABI mismatch (e.g., loading arm64-v8a library on armeabi-v7a device)
  • Incorrect library name
  • Missing dependencies
Solutions:
  1. Verify the library exists in your APK:
unzip -l app.apk | grep \.so
  1. Check that you’re building for the correct ABIs:
// app/build.gradle
android {
    defaultConfig {
        ndk {
            abiFilters 'armeabi-v7a', 'arm64-v8a'
        }
    }
}
  1. Ensure library name matches exactly:
// If your library is libexample.so
System.loadLibrary("example");  // Correct
System.loadLibrary("libexample");  // WRONG
  1. Check for missing dependencies:
readelf -d libexample.so | grep NEEDED
The JNI method signature in your C/C++ code doesn’t match the Java declaration.Common causes:
  • Incorrect function name
  • Wrong package or class name in JNI function
  • Missing extern "C" in C++
  • Method not registered
Solutions:
  1. Generate the correct signature:
javah -classpath . com.example.MyClass
  1. Verify your JNI function name:
// Java
package com.example;
class MyClass {
    native int calculate(int x);
}
// C/C++ - function name must match exactly
JNIEXPORT jint JNICALL
Java_com_example_MyClass_calculate(JNIEnv* env, jobject obj, jint x) {
    return x * 2;
}
  1. Use extern "C" for C++ code:
extern "C" {
    JNIEXPORT jint JNICALL
    Java_com_example_MyClass_calculate(JNIEnv* env, jobject obj, jint x);
}
  1. Alternative: Use dynamic registration:
static JNINativeMethod methods[] = {
    {"calculate", "(I)I", (void*)calculate_impl}
};

JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) {
    JNIEnv* env;
    vm->GetEnv((void**)&env, JNI_VERSION_1_6);
    
    jclass clazz = env->FindClass("com/example/MyClass");
    env->RegisterNatives(clazz, methods, 1);
    
    return JNI_VERSION_1_6;
}
Text relocations are not allowed on Android 6.0+ (API level 23) for security reasons.Error message:
TEXTREL error: requires text relocations
Cause: Code was not compiled as position-independent code (PIC).Solution:Ensure you’re using -fPIC flag:
# CMakeLists.txt
add_library(mylib SHARED src/mylib.cpp)
target_compile_options(mylib PRIVATE -fPIC)
Or check your existing libraries:
readelf -d libexample.so | grep TEXTREL
If third-party libraries have text relocations, contact the vendor for an updated version.
The linker cannot find the implementation of a function or variable.Common causes:
  • Missing library in link command
  • Wrong link order
  • Symbol not exported
  • C++ name mangling issues
Solutions:
  1. Add the required library:
target_link_libraries(mylib
    android
    log
    # Add missing library
)
  1. Check symbol availability:
nm -D libexample.so | grep symbol_name
  1. Fix C++ name mangling:
// If calling C library from C++
extern "C" {
    #include "c_library.h"
}
  1. Check symbol visibility:
// Ensure symbol is exported
__attribute__((visibility("default")))
int my_function() {
    return 42;
}
CMake can’t locate your NDK installation.Solutions:
  1. Set ANDROID_NDK environment variable:
export ANDROID_NDK=/path/to/ndk
  1. Specify in CMake command:
cmake -DANDROID_NDK=/path/to/ndk ...
  1. Use Android Gradle Plugin (recommended):
// app/build.gradle
android {
    ndkVersion "26.1.10909125"
}

Runtime issues

The most common native crash. Your code accessed invalid memory.Common causes:
  • Null pointer dereference
  • Use after free
  • Buffer overflow
  • Stack overflow
  • Invalid JNI reference
Debugging:
  1. Get the tombstone (see Understanding crashes)
  2. Use Address Sanitizer:
target_compile_options(mylib PRIVATE -fsanitize=address)
target_link_options(mylib PRIVATE -fsanitize=address)
  1. Check JNI usage:
adb shell setprop debug.checkjni 1
  1. Use Android Studio’s native debugger
Your code or a library called abort(), often due to assertion failures or critical errors.Common causes:
  • Failed assertion (assert() or CHECK())
  • Memory allocation failure
  • Fatal error in C++ standard library
  • Stack smashing detected
Debugging:Check the tombstone for the abort message:
Abort message: 'assertion failed: x > 0'
Look at the stack trace to identify the failing assertion or error.
Native memory is not garbage collected. You must manually free allocated memory.Detection:Use Address Sanitizer with leak detection:
target_compile_options(mylib PRIVATE 
    -fsanitize=address 
    -fsanitize=leak
)
target_link_options(mylib PRIVATE 
    -fsanitize=address 
    -fsanitize=leak
)
Common leak patterns:
  1. Missing free() or delete:
// WRONG
void leak() {
    char* buffer = new char[1024];
    // ... use buffer ...
    // Missing: delete[] buffer;
}

// CORRECT
void no_leak() {
    char* buffer = new char[1024];
    // ... use buffer ...
    delete[] buffer;
}
  1. JNI reference leaks:
// WRONG
void leak(JNIEnv* env) {
    jstring str = env->NewStringUTF("hello");
    // Missing: env->DeleteLocalRef(str);
}

// CORRECT
void no_leak(JNIEnv* env) {
    jstring str = env->NewStringUTF("hello");
    // ... use str ...
    env->DeleteLocalRef(str);
}
You’ve created too many JNI local references without deleting them.Error:
JNI ERROR: local reference table overflow (max=512)
Solution:
  1. Delete local references when done:
void process_array(JNIEnv* env, jobjectArray arr) {
    int len = env->GetArrayLength(arr);
    for (int i = 0; i < len; i++) {
        jobject obj = env->GetObjectArrayElement(arr, i);
        // ... process obj ...
        env->DeleteLocalRef(obj);  // IMPORTANT!
    }
}
  1. Use PushLocalFrame/PopLocalFrame for bulk cleanup:
void process_many(JNIEnv* env) {
    env->PushLocalFrame(100);  // Reserve space for 100 refs
    
    // Create many local references
    for (int i = 0; i < 100; i++) {
        jstring str = env->NewStringUTF("test");
        // ... use str ...
    }
    
    env->PopLocalFrame(NULL);  // Delete all at once
}
Multiple threads accessing shared data without synchronization.Detection:Use Thread Sanitizer:
target_compile_options(mylib PRIVATE -fsanitize=thread)
target_link_options(mylib PRIVATE -fsanitize=thread)
Solutions:
  1. Use mutexes:
#include <mutex>

std::mutex g_mutex;
int g_counter = 0;

void increment() {
    std::lock_guard<std::mutex> lock(g_mutex);
    g_counter++;
}
  1. Use atomic operations:
#include <atomic>

std::atomic<int> g_counter(0);

void increment() {
    g_counter++;  // Thread-safe
}
  1. Remember: Each thread needs its own JNIEnv*:
// WRONG: Sharing JNIEnv* across threads
JNIEnv* g_env;  // DON'T DO THIS

// CORRECT: Get JNIEnv* for each thread
void thread_function(JavaVM* vm) {
    JNIEnv* env;
    vm->AttachCurrentThread(&env, NULL);
    // ... use env ...
    vm->DetachCurrentThread();
}

Platform-specific issues

The emulator and physical devices can have different characteristics.Common causes:
  • ABI mismatch (emulator is x86, device is ARM)
  • Uninitialized memory (different initial values)
  • Timing issues (emulator is slower)
  • Hardware features (NEON, SSE)
Solutions:
  1. Test on actual hardware early
  2. Use sanitizers to catch undefined behavior
  3. Check CPU features before using SIMD:
#include <cpu-features.h>

if (android_getCpuFeatures() & ANDROID_CPU_ARM_FEATURE_NEON) {
    // Use NEON code
} else {
    // Use fallback
}
Android’s C library (bionic) and system behavior evolve over time.Solutions:
  1. Check API level at runtime:
#include <android/api-level.h>

int api_level = android_get_device_api_level();
if (api_level >= 29) {
    // Use Android 10+ features
}
  1. Review Android changes for NDK developers
  2. Test on multiple Android versions
Type size assumptions or ABI issues.Common causes:
  • Assuming int and pointers are same size
  • Assuming long is 32 bits
  • Structure packing differences
  • Inline assembly for wrong architecture
Solutions:See 32-bit ABI issues for detailed migration guide.Quick fixes:
// Use fixed-width types
int32_t x;  // Always 32 bits
int64_t y;  // Always 64 bits
intptr_t p; // Pointer-sized integer

Performance issues

Frequent JNI calls have overhead.Solutions:
  1. Batch operations:
// SLOW: Many JNI calls
for (int i = 0; i < 1000; i++) {
    nativeProcess(i);
}

// FAST: One JNI call
nativeProcessBatch(array, 1000);
  1. Cache JNI method IDs and field IDs:
// Cache in JNI_OnLoad
jmethodID g_method_id;

JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) {
    JNIEnv* env;
    vm->GetEnv((void**)&env, JNI_VERSION_1_6);
    
    jclass clazz = env->FindClass("com/example/MyClass");
    g_method_id = env->GetMethodID(clazz, "callback", "(I)V");
    
    return JNI_VERSION_1_6;
}
  1. Use direct buffer access:
void* data = env->GetDirectBufferAddress(buffer);
jlong size = env->GetDirectBufferCapacity(buffer);
// Process directly without copying
Native code uses too much memory.Solutions:
  1. Use tools to analyze:
# Memory profiler in Android Studio
# or command line:
adb shell dumpsys meminfo <package>
  1. Reuse buffers:
// WASTEFUL: Allocate every call
void process() {
    std::vector<char> buffer(LARGE_SIZE);
    // ...
}

// BETTER: Reuse buffer
std::vector<char> g_buffer;
void process() {
    if (g_buffer.size() < LARGE_SIZE) {
        g_buffer.resize(LARGE_SIZE);
    }
    // ...
}
  1. Free large allocations promptly
  2. Consider memory mapping for large files

Getting help

If you’re still stuck:
  1. Check tombstone files (see Understanding crashes)
  2. Enable detailed logging
  3. Search android-ndk GitHub issues
  4. Ask on android-ndk Google Group
  5. Review bionic documentation
Always include your NDK version, target API level, device/emulator info, and complete error messages when asking for help.

Build docs developers (and LLMs) love