Reprator

Android ABI and .So file Demystified

We have all seen .so files and ABI folders in an Android project, and for a long time I never looked beyond making them work. Here I will try to unpack the story behind ABI and .so files.

What is a .so file?

A .so file (shared object) is a pre-compiled native library — low-level machine code generated from C, C++, or Rust — that the CPU executes directly.

                    ┌─ compile for ARM64   → arm64-v8a/sdk_name.so
                    │
native_cpp.cpp ─────┼─ compile for ARM32   → armeabi-v7a/sdk_name.so
native_rs.rs        │
                    ├─ compile for x86_64  → x86_64/sdk_name.so
                    │
                    └─ compile for x86     → x86/sdk_name.so

Part 1: Why use C/C++/Rust in an Android codebase

There are a few common reasons:

  • To do the heavy lifting. C, C++, and Rust are compiled, low-level languages, which makes them a good fit for image processing, map rendering, game engines, and video decoding.
  • Better performance, with predictable memory use.
  • Common codebase across platforms (Android, iOS, Web, Desktop).

So whenever we need C, C++, or Rust in the codebase, we compile it and package the output as .so files, one per ABI.

Part 2: Why do we need ABI and .so files?

Part 1 covered why we reach for native code. This part is about how that code reaches a device.

Phones use different processors, such as ARM and x86, and each understands a different instruction set. (Yes, the Microprocessors chapter we skipped in college is back. 😅)

Kotlin code is compiled to DEX, and ART turns that into machine code on the device, so one build runs everywhere. Native code has no such step: it must be compiled ahead of time for each processor type.

Each processor target has its own ABI — arm64-v8a, armeabi-v7a, x86, x86_64 — and compiling for each one produces a separate .so. At runtime, ART loads the matching .so, and Kotlin talks to it through JNI.

Why the ART point matters

Kotlin  →  DEX bytecode  →  ART compiles it on the device  →  CPU
C++     →  .so (already machine code)                      →  CPU directly
                              ↑
                ART only loads it and connects it via JNI

Kotlin gets compiled on the device by ART, so one build works everywhere. C++ has no compiler on the device, so you must compile it ahead of time for every CPU type you want to support.

Are ABI and .so files always present in an Android app?

Not exactly. If you are not using any native code, there may be no .so files of your own. But you still have to check your dependencies: an SDK can bring native libraries in without you writing a line of C++.

Use either of these to see whether your APK contains .so files:

a) From the terminal

zipinfo -1 app-test.apk | grep '\.so$'

b) From Android Studio

Open Build → Analyze APK and look under lib/:

lib/
├── arm64-v8a/sdk_name.so
├── armeabi-v7a/sdk_name.so
├── x86/sdk_name.so
└── x86_64/sdk_name.so

Part 3: Android ABI (Application Binary Interface)

An ABI — application binary interface — is the full agreement that compiled code follows for a specific CPU type: the instruction set, the calling convention, how data is laid out in memory, how symbols are named, and how the binary talks to the system.

On Android, there are four ABIs (official reference):

ABI folderCPU familyWhere you meet it
arm64-v8a64-bit ARMNearly every phone made in the last decade
armeabi-v7a32-bit ARMOlder and budget devices
x86_6464-bit Intel / AMDEmulators, some Chromebooks
x8632-bit IntelOlder emulators

Asking an ARM chip to run x86 code is like reading a French menu to a dog. It is not being difficult. It genuinely cannot.

Part 4: Create your own C++ module

If you create a module from the Android Studio C++ template, you will see something like this in your Gradle file:

app/build.gradle.kts

android {
    externalNativeBuild {
        cmake {
            path = file("src/main/cpp/CMakeLists.txt")
            version = "3.22.1"
        }
    }
}

If you pin version, make sure that CMake version is installed in your SDK. Otherwise, drop the line and let Gradle use the default.

When you build the app, the compiled .so lands here:

Build output

app/build/intermediates/cxx/Debug/<hash>/obj/<abi>/myapplication.so

By default, only the ABI that matches the device or emulator is loaded at runtime.

The command adb shell getprop ro.product.cpu.abi retrieves the primary CPU Application Binary Interface (ABI) — the architecture of a connected Android device.

adb shell getprop ro.product.cpu.abi

If you want to generate .so files for every ABI, run:

./gradlew :app:assembleDebug

You will get one directory per ABI:

Build output

app/build/intermediates/cxx/Debug/<hash>/obj/
├── arm64-v8a/myapplication.so
├── armeabi-v7a/myapplication.so
├── x86/myapplication.so
└── x86_64/myapplication.so

To build for specific architectures only, use abiFilters:

app/build.gradle.kts

android {
    defaultConfig {
        ndk {
            abiFilters += listOf("arm64-v8a", "armeabi-v7a")
        }
    }
}

externalNativeBuild is the required part: it tells Gradle to compile your C++ at all. abiFilters is optional: it narrows the list of ABIs that get built and packaged.

References