whoami

Lucas Jenß

cat /etc/motd

The Coding Journal ツ — Notes taken on an epic coding journey. Technical solutions, debugging notes, and practical guides from the trenches of software development.

Close-up of white dominoes with black dots standing on green felt surface, shallow depth of field, focused mood

ls -la ~/languages/

total 8
drwxr-xr-x
▶ PHP
▶ Ruby
▶ Scala
▶ C#
▶ JavaScript
▶ Objective-C
▶ Shell Scripting

ls -la ~/toolchain/

total 7
drwxr-xr-x
▶ Typo3
▶ Akka
▶ Capistrano
▶ Git
▶ MAMP
▶ Adobe Illustrator
▶ NSTrackingArea (Cocoa)

uname -a

platforms
drwxr-xr-x
▶ Mac OS X
▶ Unix

Cross-Compile Android NDK Libraries for ARM64 and x86_64

Android applications increasingly need native code for performance-sensitive work such as image processing, audio, encryption, machine learning, and hardware integration. When that code is written in C or C++, the Android NDK can produce libraries for several CPU architectures, but each ABI needs its own compiler target and build output.

For current Android releases, the two important 64-bit targets are arm64-v8a for ARM64 devices and x86_64 for Intel and AMD-based devices or emulators. A reliable build keeps these outputs separate, uses the Android NDK toolchain consistently, and packages each library under the directory expected by Gradle.

Android ABI CMake architecture value Common use Package directory
arm64-v8a aarch64 Modern Android phones and tablets jniLibs/arm64-v8a
x86_64 x86_64 Desktop emulators, some ChromeOS devices jniLibs/x86_64
armeabi-v7a arm Older 32-bit ARM devices jniLibs/armeabi-v7a
x86 x86 Older Intel emulators and devices jniLibs/x86

Choose The ABI And NDK Toolchain

The Android NDK uses Clang rather than a traditional GCC-based cross-compiler. CMake selects the correct compiler, linker, sysroot, and Android platform libraries through android.toolchain.cmake. The ABI is supplied with ANDROID_ABI, while ANDROID_PLATFORM controls the minimum Android API level.

A practical baseline for a modern project is an API level such as 21 or higher for arm64-v8a, with the precise value determined by application requirements. x86_64 also supports current API levels, although emulator images may behave differently from physical hardware. Check the NDK version used by the Gradle project and keep local development machines, CI runners, and release builders aligned.

For example, a Linux or macOS build can configure an ARM64 directory with:

cmake -S . -B build/arm64-v8a \
  -DCMAKE_TOOLCHAIN_FILE="$ANDROID_NDK_HOME/build/cmake/android.toolchain.cmake" \
  -DANDROID_ABI=arm64-v8a \
  -DANDROID_PLATFORM=android-21 \
  -DCMAKE_BUILD_TYPE=Release

The corresponding x86_64 configuration is:

cmake -S . -B build/x86_64 \
  -DCMAKE_TOOLCHAIN_FILE="$ANDROID_NDK_HOME/build/cmake/android.toolchain.cmake" \
  -DANDROID_ABI=x86_64 \
  -DANDROID_PLATFORM=android-21 \
  -DCMAKE_BUILD_TYPE=Release

Do not reuse the same CMake build directory for both architectures. CMake caches compiler paths and target properties, so switching ANDROID_ABI in place can leave a misleading mixture of object files and configuration values.

Configure CMake For Reproducible Outputs

Your CMakeLists.txt should describe the native target without hard-coding a host compiler. A shared library might be declared as follows:

cmake_minimum_required(VERSION 3.22)
project(native_processor LANGUAGES C CXX)

add_library(native_processor SHARED
    src/native_processor.cpp
    src/codec.cpp
)

target_compile_features(native_processor PRIVATE cxx_std_17)

target_link_libraries(native_processor
    android
    log
)

CMake will produce libnative_processor.so for each selected ABI. If Java or Kotlin calls the library through JNI, export the expected JNI symbols or register native methods explicitly. Avoid assuming that an ARM64 pointer has the same size or alignment as values used during x86_64 testing. Use fixed-width types such as std::uint32_t, and inspect serialisation code carefully.

For a practical walkthrough of the Android-specific CMake setup, CMake shared libraries provides useful background on generating NDK shared objects without relying on ndk-build. That approach fits well with Gradle projects and makes architecture-specific build directories straightforward to automate.

Build each target independently:

cmake --build build/arm64-v8a --parallel
cmake --build build/x86_64 --parallel

If the project has third-party dependencies, confirm that they are also available for both ABIs. A native library compiled only for ARM64 cannot be linked into an x86_64 application package, even if its C++ API is identical.

Package Native Libraries In The APK

The Android Gradle Plugin recognises native libraries when they are placed in ABI-specific jniLibs directories. A common arrangement is:

app/src/main/jniLibs/
├── arm64-v8a/
│   └── libnative_processor.so
└── x86_64/
    └── libnative_processor.so

The files can be copied after CMake finishes, or CMake can write directly into an intermediate packaging directory. For larger projects, prefer Gradle external native builds so configuration and packaging are tied to one build process. A manually copied .so file is easy to forget when someone changes the native source late on a Friday afternoon in Melbourne.

Gradle may create APK splits or an Android App Bundle containing only the relevant ABI for each device. Test the generated artefact rather than assuming that a successful native compilation means packaging is correct. Inspect an APK with unzip -l and confirm that the expected path contains lib/arm64-v8a/ or lib/x86_64/.

On a physical Pixel or Samsung handset commonly used in Sydney or Brisbane testing, ARM64 is the normal path. The Android Studio emulator running on a developer workstation may use x86_64, depending on the selected system image and host machine. Keeping both paths in the package prevents a “works on my phone” result from hiding an emulator failure.

Test ABI-Specific Behaviour And Dependencies

A cross-compiled library can fail at several layers: compilation, linking, dynamic loading, JNI registration, or runtime behaviour. Start by checking the file type:

file app/src/main/jniLibs/arm64-v8a/libnative_processor.so
file app/src/main/jniLibs/x86_64/libnative_processor.so

Use Android tools to inspect dependencies and symbols. readelf -h shows the ELF machine type, while adb logcat often reports missing shared objects, unresolved symbols, or an incorrect JNI name. If a library loads on ARM64 but fails on x86_64, investigate assumptions about assembly instructions, compiler intrinsics, alignment, endianness, and prebuilt vendor binaries.

Do not link an ARM-only prebuilt dependency into the x86_64 target. Maintain an ABI matrix for every native dependency, including OpenSSL, FFmpeg, SQLite extensions, computer vision packages, and proprietary SDKs. If a vendor supplies only ARM64 binaries, the x86_64 build needs a supported alternative or the emulator must run an ARM-compatible image where appropriate.

Australian testing also benefits from realistic release conditions. A user in Perth may download over a slower connection than a Sydney office, and an Android App Bundle can reduce unnecessary native payloads by delivering the matching ABI. Test installation and upgrades on both Wi-Fi and mobile data, then verify that Play Console device filtering has not excluded an intended handset model.

Automate Clean Multi-ABI Builds

A small build script avoids accidental compiler reuse and makes local and CI results comparable:

#!/usr/bin/env bash
set -euo pipefail

for abi in arm64-v8a x86_64; do
  build_dir="build/${abi}"

  cmake -S . -B "$build_dir" \
    -DCMAKE_TOOLCHAIN_FILE="$ANDROID_NDK_HOME/build/cmake/android.toolchain.cmake" \
    -DANDROID_ABI="$abi" \
    -DANDROID_PLATFORM=android-21 \
    -DCMAKE_BUILD_TYPE=Release

  cmake --build "$build_dir" --parallel
done

Pin the NDK and CMake versions in the project rather than relying on whichever SDK installation happens to be active. Save compiler warnings, archive the generated libraries, and run smoke tests against both emulator and physical-device targets. CI workers should start from clean directories so an old ARM64 object cannot contaminate an x86_64 package.

Native build pipelines often sit beside broader infrastructure automation. If your release environment also manages network controls or virtualised lab systems, NSX-T policy automation is a useful example of treating operational configuration as repeatable code rather than manual console work.

Useful release checks include:

  • Build arm64-v8a and x86_64 from separate directories.
  • Inspect every shared object with file or readelf.
  • Install the APK on a physical ARM64 device and an x86_64 emulator.
  • Verify all transitive native dependencies exist for both ABIs.
  • Test the final App Bundle or APK, not just the CMake build folders.

Make Cross-Compilation Part Of Release Discipline

Once both architectures build cleanly, keep the process boring: fixed tool versions, isolated output directories, explicit ABI packaging, and repeatable tests. That discipline matters when a project moves between a developer laptop in Adelaide, a CI runner in Melbourne, and a release window scheduled around Australian business hours.

Add the ABI matrix to code review and document the minimum Android API level, NDK revision, CMake version, and expected output paths. With those details recorded, future changes to native dependencies become easier to diagnose, and an x86_64 emulator failure is less likely to be dismissed as an environment quirk.

Run the two-ABI script as part of every release candidate, inspect the packaged libraries, and retain the build logs with the artefact. That simple workflow turns ARM64 and x86_64 support from a last-minute packaging task into a dependable part of Android development.


cat ~/interests.json

KeyValue
editorTerminal-first workflow
osMac OS X / Unix
vcsGit, distributed version control
deployCapistrano, cron automation
graphicsSVG, Adobe Illustrator troubleshooting
networkingIP validation, SSH, VPN

git log --oneline --reverse

2013-10-30

Solving SVG import issues in Adobe Illustrator CS6 and CC

When importing an SVG into Illustrator, the operation fails with an unknown error [CANT]. A workaround for this Adobe-side bug.

2013

Solving NDK build issues on OS X

Troubleshooting native development kit compilation problems on Mac OS X.

2013

Programmatically adding PHP generated TypoScript to the backend configuration

Integrating dynamically generated TypoScript into Typo3 backend setups using PHP.

2013

ArgumentError: Could not parse PKey: no start line

Debugging an SSH key parsing error encountered during deployment.

2011-08-04

Validating IP-Addresses in PHP

Using PHP filter functions with flags like FILTER_FLAG_IPV4 and FILTER_FLAG_IPV6, and understanding how filter_var handles reserved IP addresses.

2011-07-09

Cocoa: Using NSTrackingArea

A short tutorial on using Cocoa's NSTrackingArea to capture mouseEntered and mouseExited events.


cat ~/contact.txt

github: github.com/x3ro
stackoverflow: x3ro
coderwall: coderwall.com/x3ro
twitter: @x3rames