Chapter 11: NDK -- Native Development Kit¶
The Android NDK (Native Development Kit) is the gateway through which applications written in C and C++ access the Android platform. Unlike the Java/Kotlin framework APIs that evolve freely across releases, NDK APIs carry a strict stability guarantee: a symbol exported in API level 21 must remain available and ABI-compatible on every subsequent release. This constraint fundamentally shapes how the NDK is built, how its headers and stub libraries are generated inside AOSP, and how three nested library categories -- NDK, LL-NDK, and VNDK -- divide the native world into stable tiers.
This chapter follows the NDK from the perspective of the platform builder. We
start with the architecture that separates app-facing APIs from internal
framework code, then inspect the Soong module types (ndk_library,
ndk_headers, llndk_libraries_txt, vndk_prebuilt_shared) that generate the
sysroot shipped to app developers. We then trace how the LL-NDK and VNDK layers
extend the same stability principles to vendor code, examine the framework
bindings for Camera, Media, and Binder that expose native services through NDK
headers, explore the ndk_translation_package module type that packages
NativeBridge dependencies, and conclude with a hands-on exercise that ties it
all together.
Throughout this chapter, we reference real files in the AOSP source tree. Every path, struct definition, and build rule cited here can be found in that tree.
11.1 NDK Architecture Overview¶
11.1.1 What the NDK Is -- and What It Is Not¶
The NDK is a set of stable C/C++ APIs that application developers can call
from native code loaded via System.loadLibrary() or from a purely native
NativeActivity. "Stable" means two things:
- ABI stability -- the symbol name, calling convention, and data-structure layout of every function exported in a given API level never change.
- Header stability -- every header installed into the NDK sysroot is verified to be self-contained and valid C at build time.
The NDK is explicitly not the totality of native code in the platform. Most
of the C/C++ code under frameworks/, system/, and hardware/ is
framework-internal and is never exposed to applications. The boundary
between "NDK" and "non-NDK" is enforced at two levels:
- Build time: the
ndk_libraryandndk_headersSoong module types control exactly which symbols and headers are placed into the sysroot. - Runtime: the dynamic linker's namespace isolation prevents apps from
dlopen()-ing libraries that are not on the NDK or LL-NDK lists.
11.1.2 The NDK Call Stack¶
The following diagram traces a typical call from Java application code through JNI into NDK APIs and down to system libraries:
graph TD
A["Java/Kotlin Application Code"] --> B["JNI Layer<br/>(System.loadLibrary)"]
B --> C["Application Native Code<br/>(libmyapp.so)"]
C --> D["NDK APIs<br/>(libc, liblog, libmediandk,<br/>libcamera2ndk, libaaudio, ...)"]
D --> E["Platform System Libraries<br/>(libbinder, libgui, libcutils,<br/>libstagefright, ...)"]
E --> F["Kernel Interfaces<br/>(ioctl, Binder driver,<br/>ashmem, ion)"]
style A fill:#4a90d9,color:white
style B fill:#7b68ee,color:white
style C fill:#50c878,color:white
style D fill:#ff8c00,color:white
style E fill:#dc143c,color:white
style F fill:#333,color:white
Each layer in the diagram represents a different stability domain:
| Layer | Stability guarantee | Who consumes it |
|---|---|---|
| NDK APIs | ABI-stable across releases | App developers |
| Platform system libs | No stability guarantee | Framework developers |
| Kernel interfaces | Stable via kernel ABI | All native code |
11.1.3 NDK vs Framework Native Code¶
It is essential to distinguish between "native code that uses the NDK" and "native code that is part of the platform". Consider two concrete examples:
App using the NDK -- a game engine links against libc.so, liblog.so,
libEGL.so, libGLESv3.so, and libaaudio.so. These libraries are all on the
NDK list. The game ships an APK containing lib/arm64-v8a/libgame.so, and the
platform guarantees that the APIs it calls will work identically on any device
running the same or higher API level.
Framework native code -- the SurfaceFlinger compositor links against
libgui.so, libui.so, libsync.so, libhwbinder.so, and dozens of other
internal libraries. None of these carry an NDK stability guarantee. A device
manufacturer can (and must) rebuild SurfaceFlinger against the exact platform
tree.
The build system enforces this distinction. When a module sets
sdk_version: "current", Soong resolves its shared library dependencies against
the NDK stub libraries rather than the real platform implementations. If the
module tries to use a non-NDK symbol, linking fails at build time.
11.1.4 Sysroot Generation Flow¶
The NDK sysroot is not a hand-curated directory of headers and libraries. It is
an output of the AOSP build. The build system assembles it from three components
registered as Soong module types in
build/soong/cc/ndk_sysroot.go:
graph LR
subgraph "Soong Module Types"
NH["ndk_headers<br/>(headers)"]
NL["ndk_library<br/>(stub .so)"]
BS["Bionic static libs<br/>(libc.a, libm.a)"]
end
subgraph "NDK Sysroot"
INC["sysroot/usr/include/**"]
LIB["sysroot/usr/lib/<triple>/<api>/"]
STA["sysroot/usr/lib/<triple>/"]
end
NH --> INC
NL --> LIB
BS --> STA
TS["ndk.timestamp"] --> INC
TS --> LIB
TS --> STA
style NH fill:#4a90d9,color:white
style NL fill:#ff8c00,color:white
style BS fill:#50c878,color:white
The comment at the top of build/soong/cc/ndk_sysroot.go spells out these four
components explicitly:
// The platform needs to provide the following artifacts for the NDK:
// 1. Bionic headers.
// 2. Platform API headers.
// 3. NDK stub shared libraries.
// 4. Bionic static libraries.
The file ndk_sysroot.go registers three module types and a singleton:
// build/soong/cc/ndk_sysroot.go (lines 80-85)
func RegisterNdkModuleTypes(ctx android.RegistrationContext) {
ctx.RegisterModuleType("ndk_headers", NdkHeadersFactory)
ctx.RegisterModuleType("ndk_library", NdkLibraryFactory)
ctx.RegisterModuleType("preprocessed_ndk_headers", preprocessedNdkHeadersFactory)
ctx.RegisterParallelSingletonType("ndk", NdkSingleton)
}
The NdkSingleton walks every module in the tree, collecting headers, stub
libraries, and static libraries. It writes three timestamp files that the
top-level Makefile depends on:
ndk_headers.timestamp-- depends only on headers (used by.tidychecks)ndk_base.timestamp-- depends on headers + stub shared librariesndk.timestamp-- depends on the base + static libraries
Building with m ndk triggers generation of all sysroot artifacts.
11.2 NDK API Surface¶
11.2.1 Overview of NDK Libraries¶
The NDK API surface is the union of every ndk_library module declared in AOSP.
These are the libraries that app developers can link against. Searching the tree
for ndk_library { reveals the complete list:
| Library | First API | Source location |
|---|---|---|
libc |
9 | bionic/libc/Android.bp |
libm |
9 | bionic/libm/Android.bp |
libdl |
9 | bionic/libdl/Android.bp |
liblog |
9 | system/logging/liblog/Android.bp |
libz |
9 | external/zlib/Android.bp |
libandroid |
9 | frameworks/base/native/android/Android.bp |
libEGL |
9 | frameworks/native/opengl/libs/Android.bp |
libGLESv1_CM |
9 | frameworks/native/opengl/libs/Android.bp |
libGLESv2 |
9 | frameworks/native/opengl/libs/Android.bp |
libGLESv3 |
9 | frameworks/native/opengl/libs/Android.bp |
libmediandk |
21 | frameworks/av/media/ndk/Android.bp |
libcamera2ndk |
24 | frameworks/av/camera/ndk/Android.bp |
libnativewindow |
26 | frameworks/native/libs/nativewindow/Android.bp |
libaaudio |
26 | frameworks/av/media/libaaudio/Android.bp |
libvulkan |
26 | frameworks/native/vulkan/libvulkan/Android.bp |
libbinder_ndk |
29 | frameworks/native/libs/binder/ndk/Android.bp |
libsync |
26 | system/core/libsync/Android.bp |
libneuralnetworks |
27 | packages/modules/NeuralNetworks/runtime/Android.bp |
libicu |
31 | external/icu/libicu/Android.bp |
libnativehelper |
34 | system/extras/module_ndk_libs/libnativehelper/Android.bp |
Each entry in this table corresponds to a Soong ndk_library block such as:
// frameworks/av/camera/ndk/Android.bp (lines 51-56)
ndk_library {
name: "libcamera2ndk",
symbol_file: "libcamera2ndk.map.txt",
first_version: "24",
unversioned_until: "current",
}
11.2.2 API Categories¶
The NDK APIs span a wide range of functionality. Here is a conceptual grouping:
graph TD
NDK["NDK API Surface"]
NDK --> BIONIC["Bionic<br/>libc, libm, libdl"]
NDK --> GRAPHICS["Graphics<br/>libEGL, libGLESv2,<br/>libGLESv3, libvulkan"]
NDK --> MEDIA["Media<br/>libmediandk, libaaudio,<br/>libOpenSLES, libOpenMAXAL"]
NDK --> CAMERA["Camera<br/>libcamera2ndk"]
NDK --> WINDOW["Windowing<br/>libnativewindow,<br/>libandroid"]
NDK --> IPC["IPC<br/>libbinder_ndk"]
NDK --> UTIL["Utility<br/>liblog, libz,<br/>libsync, libicu"]
NDK --> ML["Machine Learning<br/>libneuralnetworks"]
style NDK fill:#333,color:white
style BIONIC fill:#4a90d9,color:white
style GRAPHICS fill:#7b68ee,color:white
style MEDIA fill:#50c878,color:white
style CAMERA fill:#ff8c00,color:white
style WINDOW fill:#dc143c,color:white
style IPC fill:#8b4513,color:white
style UTIL fill:#666,color:white
style ML fill:#9932cc,color:white
11.2.3 Key NDK APIs¶
This section examines the most important NDK APIs that applications use.
AHardwareBuffer¶
AHardwareBuffer provides a cross-process handle to GPU-allocated memory.
Introduced in API 26 as part of libnativewindow, it allows sharing graphical
buffers between the CPU, GPU, camera, and video decoder without copying.
Key functions (from libnativewindow):
AHardwareBuffer_allocate()-- allocate a buffer with specified format and usage flagsAHardwareBuffer_lock()-- map the buffer for CPU accessAHardwareBuffer_sendHandleToUnixSocket()-- share across processesAHardwareBuffer_recvHandleFromUnixSocket()-- receive from another process
ANativeWindow¶
ANativeWindow is the native side of android.view.Surface. It is the primary
interface for applications that render frames directly (OpenGL ES, Vulkan, or
software rendering). Available since API 9 through libandroid:
ANativeWindow_fromSurface()-- convert a Java Surface to a native handleANativeWindow_setBuffersGeometry()-- configure buffer dimensionsANativeWindow_lock()/ANativeWindow_unlockAndPost()-- software rendering
AAudio¶
AAudio (Android Audio) replaced OpenSL ES as the recommended low-latency audio
API starting with API 26. Defined in libaaudio:
AAudioStreamBuilder_create()-- create a stream builderAAudioStreamBuilder_setPerformanceMode()-- request low latencyAAudioStream_requestStart()/AAudioStream_requestStop()-- control playback
The NDK headers for AAudio are declared in:
// frameworks/av/media/libaaudio/Android.bp (lines 24-31)
ndk_headers {
name: "libAAudio_headers",
from: "include",
to: "",
srcs: ["include/aaudio/AAudio.h"],
license: "include/aaudio/NOTICE",
}
ACamera¶
The Camera NDK, introduced at API 24 in libcamera2ndk, exposes the Camera2
API to native code. We will examine its implementation in detail in Section 8.6.
ASensor¶
The sensor API is part of libandroid and provides access to accelerometer,
gyroscope, and other hardware sensors:
ASensorManager_getInstance()-- get the sensor managerASensorManager_getDefaultSensor()-- get a specific sensorASensorEventQueue_enableSensor()-- start receiving events
11.2.4 Native App Glue¶
The NDK includes a helper library called "native app glue" that simplifies writing purely native applications. It is shipped as source code at:
prebuilts/ndk/current/sources/android/native_app_glue/
android_native_app_glue.c
android_native_app_glue.h
The glue library provides a threading model where the application runs its main
loop in a separate thread from the Activity's UI thread. The core data structure
is struct android_app:
// prebuilts/ndk/current/sources/android/native_app_glue/
// android_native_app_glue.h (lines 109-183)
struct android_app {
void* userData;
void (*onAppCmd)(struct android_app* app, int32_t cmd);
int32_t (*onInputEvent)(struct android_app* app, AInputEvent* event);
ANativeActivity* activity;
AConfiguration* config;
void* savedState;
size_t savedStateSize;
ALooper* looper;
AInputQueue* inputQueue;
ANativeWindow* window;
ARect contentRect;
int activityState;
int destroyRequested;
// ... private implementation fields
};
The app receives lifecycle events through command codes:
| Command | Meaning |
|---|---|
APP_CMD_INIT_WINDOW |
A new ANativeWindow is ready |
APP_CMD_TERM_WINDOW |
The window is being destroyed |
APP_CMD_GAINED_FOCUS |
The activity has gained input focus |
APP_CMD_LOST_FOCUS |
The activity has lost input focus |
APP_CMD_RESUME |
The activity has been resumed |
APP_CMD_PAUSE |
The activity has been paused |
APP_CMD_SAVE_STATE |
The app should save state |
APP_CMD_DESTROY |
The activity is being destroyed |
The application entry point is android_main() rather than main():
// prebuilts/ndk/current/sources/android/native_app_glue/
// android_native_app_glue.h (line 346)
extern void android_main(struct android_app* app);
11.2.5 Symbol Map Files¶
Every NDK library is controlled by a .map.txt symbol file. This file is the
definitive specification of the library's API surface. Here is an excerpt from
the Camera NDK symbol file:
// frameworks/av/camera/ndk/libcamera2ndk.map.txt (excerpt)
LIBCAMERA2NDK {
global:
ACameraCaptureSession_abortCaptures;
ACameraCaptureSession_capture;
ACameraCaptureSession_captureV2; # introduced=33
ACameraCaptureSession_logicalCamera_capture; # introduced=29
ACameraCaptureSession_close;
ACameraCaptureSession_getDevice;
ACameraCaptureSession_setRepeatingRequest;
ACameraCaptureSession_stopRepeating;
ACameraCaptureSession_updateSharedOutput; # introduced=28
ACameraDevice_close;
ACameraDevice_createCaptureRequest;
ACameraDevice_createCaptureRequest_withPhysicalIds; # introduced=29
ACameraDevice_createCaptureSession;
ACameraDevice_getId;
ACameraManager_create;
ACameraManager_delete;
ACameraManager_deleteCameraIdList;
ACameraManager_getCameraCharacteristics;
ACameraManager_getCameraIdList;
ACameraManager_openCamera;
ACameraManager_registerAvailabilityCallback;
ACameraManager_unregisterAvailabilityCallback;
ACameraMetadata_copy;
ACameraMetadata_free;
ACameraMetadata_getAllTags;
ACameraMetadata_getConstEntry;
ACameraMetadata_getTagFromName; # introduced=35
ACameraMetadata_isLogicalMultiCamera; # introduced=29
ACameraMetadata_fromCameraMetadata; # introduced=30
ACameraOutputTarget_create;
ACameraOutputTarget_free;
ACaptureRequest_addTarget;
ACaptureRequest_copy; # introduced=28
ACaptureRequest_free;
ACaptureRequest_getAllTags;
ACaptureRequest_getConstEntry;
ACaptureRequest_setEntry_double;
ACaptureRequest_setEntry_float;
ACaptureRequest_setEntry_i32;
ACaptureRequest_setEntry_i64;
ACaptureRequest_setEntry_rational;
ACaptureRequest_setEntry_u8;
ACaptureSessionOutputContainer_add;
ACaptureSessionOutputContainer_create;
ACaptureSessionOutputContainer_free;
ACaptureSessionOutputContainer_remove;
ACaptureSessionOutput_create;
ACaptureSessionOutput_free;
local:
*;
};
Key aspects of the symbol map format:
global:-- symbols listed here are exported from the stub librarylocal: *;-- all other symbols are hidden (this is the default catch-all)# introduced=N-- the symbol was added at API level N; thendkstubgentool excludes it from stubs for earlier API levels# systemapi-- the symbol is only available to system apps, not regular third-party apps- Symbols without an
# introduced=annotation are available from the library'sfirst_version(e.g., API 24 forlibcamera2ndk)
This format allows precise per-symbol API level tracking within a single file.
When ndkstubgen generates stubs for API 28, it includes all symbols that
were introduced at or before API 28, but excludes symbols introduced at API 29
or later.
11.2.6 Bionic NDK Headers¶
The bionic C library contributes the largest collection of NDK headers. In
bionic/libc/Android.bp, there are multiple ndk_headers modules:
// bionic/libc/Android.bp (lines 2084-2089)
ndk_headers {
name: "common_libc",
from: "include",
to: "",
srcs: ["include/**/*.h"],
license: "NOTICE",
}
Additional header modules cover kernel UAPI headers, architecture-specific headers, and more:
// bionic/libc/Android.bp (lines 2097-2106)
ndk_headers {
name: "libc_uapi",
from: "kernel/uapi",
to: "",
srcs: [
"kernel/uapi/asm-generic/**/*.h",
// ...
],
license: "NOTICE",
}
These bionic headers form the foundation of the NDK sysroot and include:
- Standard C library headers (
stdio.h,stdlib.h,string.h, etc.) - POSIX headers (
pthread.h,unistd.h,sys/mman.h, etc.) - Linux kernel UAPI headers (
linux/*.h,asm/*.h) - Android-specific extensions (
android/log.h,android/dlext.h)
11.2.7 CPU Features¶
The cpufeatures library allows native code to query CPU capabilities at
runtime. Located at:
The primary API consists of two functions:
// prebuilts/ndk/current/sources/android/cpufeatures/cpu-features.h (line 58)
extern AndroidCpuFamily android_getCpuFamily(void);
// cpu-features.h (line 65)
extern uint64_t android_getCpuFeatures(void);
The android_getCpuFamily() function returns one of:
ANDROID_CPU_FAMILY_ARMANDROID_CPU_FAMILY_ARM64ANDROID_CPU_FAMILY_X86ANDROID_CPU_FAMILY_X86_64
The android_getCpuFeatures() function returns a bitmask of CPU capabilities.
For ARM64, the flags include:
// cpu-features.h (lines 246-254)
enum {
ANDROID_CPU_ARM64_FEATURE_FP = (1 << 0),
ANDROID_CPU_ARM64_FEATURE_ASIMD = (1 << 1),
ANDROID_CPU_ARM64_FEATURE_AES = (1 << 2),
ANDROID_CPU_ARM64_FEATURE_PMULL = (1 << 3),
ANDROID_CPU_ARM64_FEATURE_SHA1 = (1 << 4),
ANDROID_CPU_ARM64_FEATURE_SHA2 = (1 << 5),
ANDROID_CPU_ARM64_FEATURE_CRC32 = (1 << 6),
};
For x86/x86_64 architectures:
// cpu-features.h (lines 260-271)
enum {
ANDROID_CPU_X86_FEATURE_SSSE3 = (1 << 0),
ANDROID_CPU_X86_FEATURE_POPCNT = (1 << 1),
ANDROID_CPU_X86_FEATURE_MOVBE = (1 << 2),
ANDROID_CPU_X86_FEATURE_SSE4_1 = (1 << 3),
ANDROID_CPU_X86_FEATURE_SSE4_2 = (1 << 4),
ANDROID_CPU_X86_FEATURE_AES_NI = (1 << 5),
ANDROID_CPU_X86_FEATURE_AVX = (1 << 6),
ANDROID_CPU_X86_FEATURE_RDRAND = (1 << 7),
ANDROID_CPU_X86_FEATURE_AVX2 = (1 << 8),
ANDROID_CPU_X86_FEATURE_SHA_NI = (1 << 9),
};
This is invaluable for libraries that provide hand-optimized SIMD paths -- applications can check feature flags at startup and branch to the most efficient code path for the current CPU.
11.3 NDK Build Integration¶
The NDK build integration in AOSP is handled by four key Go source files in
build/soong/cc/:
| File | Lines | Purpose |
|---|---|---|
ndk_library.go |
611 | Stub shared library generation |
ndk_headers.go |
276 | Header installation into sysroot |
ndk_sysroot.go |
320 | Sysroot assembly singleton |
ndk_abi.go |
102 | ABI dump and diff monitoring |
11.3.1 The ndk_library Module Type¶
The ndk_library module type is the core build primitive for NDK stub
libraries. Each NDK library is declared as a pair: an ndk_library module that
generates stubs, and a cc_library_shared module that provides the real
implementation. The stub is what app developers link against; the real library
is what runs on the device.
The module type is implemented by NdkLibraryFactory() in
build/soong/cc/ndk_library.go:
// build/soong/cc/ndk_library.go (lines 605-611)
func NdkLibraryFactory() android.Module {
module := newStubLibrary()
android.InitAndroidArchModule(module, android.DeviceSupported,
android.MultilibBoth)
return module
}
Properties¶
The ndk_library module type accepts these properties:
// build/soong/cc/ndk_library.go (lines 91-117)
type libraryProperties struct {
// Relative path to the symbol map.
Symbol_file *string `android:"path"`
// The first API level a library was available.
First_version *string
// The first API level that library should have the version script
// applied.
Unversioned_until *string
// DO NOT USE THIS
// NDK libraries should not export their headers.
Export_header_libs []string
}
The symbol_file property points to a .map.txt file that lists every exported
symbol and the API level at which it was introduced. This is the source of truth
for the NDK API surface. For example, libcamera2ndk.map.txt lists every
function in the Camera NDK and the API level at which it became available.
The first_version property specifies the earliest API level for which stubs
should be generated. The build system generates a separate stub library for
every API level from first_version through the current level plus a "future"
level.
Stub Generation Process¶
sequenceDiagram
participant BP as Android.bp
participant SG as ndkstubgen tool
participant CC as Clang compiler
participant LD as Linker
participant SR as NDK Sysroot
BP->>SG: symbol_file (.map.txt) + API level
SG->>SG: Parse symbol definitions
SG-->>CC: Generated stub.c + stub.map
CC->>LD: Compile stub.c to stub.o
LD->>LD: Link with version script (stub.map)
LD-->>SR: Install stub .so to sysroot/<triple>/<api>/
The stub generation begins in the compile() method of stubDecorator:
// build/soong/cc/ndk_library.go (lines 474-507)
func (c *stubDecorator) compile(ctx ModuleContext, flags Flags,
deps PathDeps) Objects {
if !strings.HasSuffix(String(c.properties.Symbol_file), ".map.txt") {
ctx.PropertyErrorf("symbol_file", "must end with .map.txt")
}
// ...
symbolFile := String(c.properties.Symbol_file)
nativeAbiResult := ParseNativeAbiDefinition(ctx, symbolFile,
c.apiLevel, "")
objs := CompileStubLibrary(ctx, flags, nativeAbiResult.StubSrc,
ctx.getSharedFlags())
c.versionScriptPath = nativeAbiResult.VersionScript
// ...
}
The ParseNativeAbiDefinition() function invokes the ndkstubgen tool:
// build/soong/cc/ndk_library.go (lines 258-286)
func ParseNativeAbiDefinition(ctx android.ModuleContext,
symbolFile string, apiLevel android.ApiLevel,
genstubFlags string) NdkApiOutputs {
stubSrcPath := android.PathForModuleGen(ctx, "stub.c")
versionScriptPath := android.PathForModuleGen(ctx, "stub.map")
symbolFilePath := android.PathForModuleSrc(ctx, symbolFile)
symbolListPath := android.PathForModuleGen(ctx,
"abi_symbol_list.txt")
apiLevelsJson := android.GetApiLevelsJson(ctx)
ctx.Build(pctx, android.BuildParams{
Rule: genStubSrc,
Description: "generate stubs " + symbolFilePath.Rel(),
Outputs: []android.WritablePath{stubSrcPath,
versionScriptPath, symbolListPath},
Input: symbolFilePath,
Implicits: []android.Path{apiLevelsJson},
Args: map[string]string{
"arch": ctx.Arch().ArchType.String(),
"apiLevel": apiLevel.String(),
"apiMap": apiLevelsJson.String(),
"flags": genstubFlags,
},
})
// ...
}
This invokes the genStubSrc rule:
// build/soong/cc/ndk_library.go (lines 38-43)
genStubSrc = pctx.AndroidStaticRule("genStubSrc",
blueprint.RuleParams{
Command: "$ndkStubGenerator --arch $arch --api $apiLevel " +
"--api-map $apiMap $flags $in $out",
CommandDeps: []string{"$ndkStubGenerator"},
}, "arch", "apiLevel", "apiMap", "flags")
The tool reads the .map.txt symbol file and produces:
- A
stub.csource file containing placeholder implementations of every exported function - A
stub.mapversion script that controls which symbols are exported - An
abi_symbol_list.txtenumerating all symbols for ABI monitoring
Stub Compilation Flags¶
Stub libraries are compiled with special flags that suppress warnings about the placeholder implementations:
// build/soong/cc/ndk_library.go (lines 220-232)
var stubLibraryCompilerFlags = []string{
"-Wno-incompatible-library-redeclaration",
"-Wno-incomplete-setjmp-declaration",
"-Wno-builtin-requires-header",
"-Wno-invalid-noreturn",
"-Wall",
"-Werror",
"-fno-unwind-tables",
}
The -fno-unwind-tables flag is notable: since stubs are never actually
executed, there is no need for unwinding information. This reduces the size of
the generated stubs.
Version Management¶
Each ndk_library produces stubs for every API level from first_version to
the current release:
// build/soong/cc/ndk_library.go (lines 145-155)
func ndkLibraryVersions(ctx android.BaseModuleContext,
from android.ApiLevel) []string {
versionStrs := []string{}
for _, version := range ctx.Config().FinalApiLevels() {
if version.GreaterThanOrEqualTo(from) {
versionStrs = append(versionStrs, version.String())
}
}
versionStrs = append(versionStrs,
android.FutureApiLevel.String())
return versionStrs
}
This means that libcamera2ndk with first_version: "24" generates stubs for
API 24, 25, 26, ..., current, and "future". Each versioned stub exports only
the symbols that were available at that API level.
Stub Installation¶
Stubs are installed into a versioned path within the sysroot:
// build/soong/cc/ndk_library.go (lines 562-569)
func getVersionedLibraryInstallPath(ctx ModuleContext,
apiLevel android.ApiLevel) android.OutputPath {
return getUnversionedLibraryInstallPath(ctx).Join(ctx,
apiLevel.String())
}
This produces paths like:
sysroot/usr/lib/aarch64-linux-android/24/libcamera2ndk.so
sysroot/usr/lib/aarch64-linux-android/26/libaaudio.so
sysroot/usr/lib/aarch64-linux-android/29/libbinder_ndk.so
11.3.2 The ndk_headers Module Type¶
The ndk_headers module type installs header files into the NDK sysroot. It is
implemented in build/soong/cc/ndk_headers.go.
Properties¶
// build/soong/cc/ndk_headers.go (lines 39-71)
type headerProperties struct {
// Base directory of the headers being installed.
From *string
// Install path within the sysroot relative to usr/include.
To *string
// List of headers to install. Glob compatible.
Srcs []string `android:"path"`
// Source paths that should be excluded.
Exclude_srcs []string `android:"path"`
// Path to the NOTICE file associated with the headers.
License *string `android:"path"`
// Set to true if the headers should skip verification.
Skip_verification *bool
}
The from and to properties control how header paths are mapped from the
source tree into the sysroot. The comment in the source explains the mapping:
// ndk_headers {
// name: "foo",
// from: "include",
// to: "",
// srcs: ["include/foo/bar/baz.h"],
// }
//
// Will install $SYSROOT/usr/include/foo/bar/baz.h.
Header Verification¶
Every NDK header is verified to be self-contained and valid C. This happens in
the NdkSingleton in ndk_sysroot.go:
// build/soong/cc/ndk_sysroot.go (lines 121-158)
func verifyNdkHeaderIsCCompatible(ctx android.SingletonContext,
src android.Path, dest android.Path) android.Path {
// ...
ctx.Build(pctx, android.BuildParams{
Rule: verifyCCompat,
Description: fmt.Sprintf("Verifying C compatibility of %s",
src),
Output: output,
Input: dest,
Implicits: []android.Path{
getNdkHeadersTimestampFile(ctx)},
Args: map[string]string{
"ccCmd": "${config.ClangBin}/clang",
"flags": fmt.Sprintf(
"-target aarch64-linux-android%d --sysroot %s",
android.FutureApiLevel.FinalOrFutureInt(),
getNdkSysrootBase(ctx).String(),
),
},
})
return output
}
This compiles each header with -fsyntax-only to ensure it parses cleanly as
standalone C code. Headers that have been granted skip_verification: true
bypass this check -- but the comment in the property definition notes that this
should be extremely rare.
Preprocessed Headers¶
Some NDK headers require preprocessing before installation (e.g., architecture-
specific definitions). The preprocessed_ndk_headers module type handles this:
// build/soong/cc/ndk_headers.go (lines 192-215)
type preprocessedHeadersProperties struct {
// The preprocessor to run.
Preprocessor *string
// Source path to the files to be preprocessed.
Srcs []string
// Install path within the sysroot relative to usr/include.
To *string
// Path to the NOTICE file.
License *string
}
11.3.3 ABI Monitoring¶
NDK ABI stability is not just a policy -- it is enforced by automated
monitoring in the build system. The implementation lives in
build/soong/cc/ndk_abi.go.
ABI Dump Generation¶
The system uses STG (Symbol/Type Graph), a tool that extracts ABI information from ELF binaries using DWARF debug information:
// build/soong/cc/ndk_library.go (lines 51-55)
stg = pctx.AndroidStaticRule("stg",
blueprint.RuleParams{
Command: "$stg -S :$symbolList --file-filter :$headersList " +
"--elf $in -o $out",
CommandDeps: []string{"$stg"},
}, "symbolList", "headersList")
The headersList is critical: it tells STG to only monitor types that are
defined in NDK public headers. Types from internal headers are excluded from
monitoring. This prevents false positives from internal implementation details
leaking through DWARF.
The header filtering logic is in ndk_sysroot.go:
// build/soong/cc/ndk_sysroot.go (lines 186-196)
func writeNdkAbiSrcFilter(ctx android.BuilderContext,
headerSrcPaths android.Paths,
outputFile android.WritablePath) {
var filterBuilder strings.Builder
filterBuilder.WriteString("[decl_file_allowlist]\n")
for _, headerSrcPath := range headerSrcPaths {
filterBuilder.WriteString(headerSrcPath.String())
filterBuilder.WriteString("\n")
}
android.WriteFileRule(ctx, outputFile, filterBuilder.String())
}
ABI Diff Detection¶
When an ABI dump exists for a given API level, the build compares it against
the prebuilt reference dump stored in prebuilts/abi-dumps/ndk/:
// build/soong/cc/ndk_library.go (lines 57-65)
stgdiff = pctx.AndroidStaticRule("stgdiff",
blueprint.RuleParams{
Command: "$stgdiff $args --stg $in -o $out || " +
"(cat $out && echo 'Run " +
"$$ANDROID_BUILD_TOP/development/tools/ndk/" +
"update_ndk_abi.sh to update the ABI dumps.' " +
"&& false)",
CommandDeps: []string{"$stgdiff"},
}, "args")
If stgdiff detects an ABI change, the build fails with an error message
telling the developer to run update_ndk_abi.sh. This is an intentional
friction point: breaking NDK ABI is a serious matter that requires explicit
acknowledgment.
The diff logic checks two things:
- Current API level: the built ABI must match the prebuilt dump for this level exactly. Any change is an error.
- Next API level: the ABI must be a superset of the current level. New additions are allowed, but removals or modifications are not.
// build/soong/cc/ndk_library.go (lines 397-471)
func (this *stubDecorator) diffAbi(ctx ModuleContext) {
// Catch any ABI changes compared to the checked-in definition
// ...
ctx.Build(pctx, android.BuildParams{
Rule: stgdiff,
// ...
Args: map[string]string{
"args": "--format=small",
},
})
// Also ensure the next API level is compatible
// ...
ctx.Build(pctx, android.BuildParams{
Rule: stgdiff,
// ...
Args: map[string]string{
"args": "--format=small --ignore=interface_addition",
},
})
}
The --ignore=interface_addition flag is key: it allows new symbols to appear
in the next API level but flags any removal or signature change.
ABI Monitoring Flow¶
graph TD
SRC["Implementation .so<br/>(from real cc_library_shared)"] --> STG["STG Tool<br/>Extract ABI from DWARF"]
HDR["NDK Header List<br/>(ndk_abi_headers.txt)"] --> STG
SYM["Symbol List<br/>(abi_symbol_list.txt)"] --> STG
STG --> DUMP["ABI Dump<br/>(abi.stg)"]
PREBUILT["Prebuilt ABI Dump<br/>(prebuilts/abi-dumps/ndk/)"] --> DIFF["stgdiff Tool"]
DUMP --> DIFF
DIFF -->|Match| PASS["Build passes"]
DIFF -->|Mismatch| FAIL["Build FAILS<br/>Run update_ndk_abi.sh"]
style FAIL fill:#dc143c,color:white
style PASS fill:#50c878,color:white
Bionic Exception¶
Interestingly, bionic libraries are currently exempted from ABI monitoring:
// build/soong/cc/ndk_library.go (lines 336-352)
func (this *stubDecorator) canDumpAbi(ctx ModuleContext) bool {
if runtime.GOOS == "darwin" {
return false
}
if strings.HasPrefix(ctx.ModuleDir(), "bionic/") {
// Bionic has enough uncommon implementation details like
// ifuncs and asm code that the ABI tracking here has a ton
// of false positives. That's causing pretty extreme friction
// for development there, so disabling it until the workflow
// can be improved.
//
// http://b/358653811
return false
}
return ctx.Config().ReleaseNdkAbiMonitored()
}
This is a pragmatic concession: bionic's use of ifuncs (indirect functions for runtime dispatch) and hand-written assembly generates DWARF information that confuses the STG tool. Bionic ABI stability is maintained through other means (CTS tests, manual review).
11.3.4 The NDK Known Libraries Registry¶
Every ndk_library module registers itself in a global list of known NDK
libraries:
// build/soong/cc/ndk_library.go (lines 195-218)
func getNDKKnownLibs(config android.Config) *[]string {
return config.Once(ndkKnownLibsKey, func() interface{} {
return &[]string{}
}).(*[]string)
}
func (c *stubDecorator) compilerInit(ctx BaseModuleContext) {
c.baseCompiler.compilerInit(ctx)
name := ctx.baseModuleName()
// ...
ndkKnownLibsLock.Lock()
defer ndkKnownLibsLock.Unlock()
ndkKnownLibs := getNDKKnownLibs(ctx.Config())
for _, lib := range *ndkKnownLibs {
if lib == name {
return
}
}
*ndkKnownLibs = append(*ndkKnownLibs, name)
}
This list is used by the build system to validate that SDK-built modules only
link against approved NDK libraries. A mutex (ndkKnownLibsLock) protects the
list because compilerInit() runs during the parallel BeginMutator phase.
11.3.5 End-to-End: How an NDK Library Is Built¶
Let us trace the complete lifecycle of libcamera2ndk from declaration to
sysroot installation:
sequenceDiagram
participant BP as frameworks/av/camera/ndk/Android.bp
participant SOONG as Soong Build System
participant STUBGEN as ndkstubgen
participant CLANG as Clang
participant SYSROOT as NDK Sysroot
participant STG as STG Tool
participant ABI as prebuilts/abi-dumps/
BP->>SOONG: ndk_library "libcamera2ndk"<br/>first_version: "24"
SOONG->>SOONG: Generate versions [24, 25, ..., current, future]
loop For each API level
SOONG->>STUBGEN: libcamera2ndk.map.txt + api level
STUBGEN-->>SOONG: stub.c + stub.map + symbol_list.txt
SOONG->>CLANG: Compile stub.c with stub flags
CLANG-->>SOONG: stub.o
SOONG->>CLANG: Link with version script
CLANG-->>SYSROOT: sysroot/usr/lib/<triple>/<api>/libcamera2ndk.so
end
Note over BP: Also: ndk_headers "libcamera2ndk_headers"
BP->>SYSROOT: Copy include/camera/*.h to<br/>sysroot/usr/include/camera/
SOONG->>STG: Real libcamera2ndk.so + header list
STG-->>ABI: Compare ABI dump
ABI-->>SOONG: Pass / Fail
11.4 LL-NDK -- Low-Level NDK¶
11.4.1 What Is LL-NDK?¶
LL-NDK (Low-Level NDK) is a subset of system libraries that are available to both NDK applications and vendor/product native code. While regular NDK libraries are only accessible to apps running in the app linker namespace, LL-NDK libraries are visible across namespace boundaries.
The canonical LL-NDK libraries are the fundamental system libraries that everything depends on:
| Library | Purpose |
|---|---|
libc.so |
C standard library (bionic) |
libm.so |
Math library |
libdl.so |
Dynamic linker interface |
liblog.so |
Android logging |
libz.so |
zlib compression |
libnativewindow.so |
Window/buffer management |
libsync.so |
Fence synchronization |
libvulkan.so |
Vulkan graphics API |
libEGL.so |
EGL interface |
libGLESv1_CM.so |
OpenGL ES 1.x |
libGLESv2.so |
OpenGL ES 2.0+ |
libGLESv3.so |
OpenGL ES 3.x |
libmediandk.so |
Media framework |
libbinder_ndk.so |
Binder NDK interface |
11.4.2 LLNDK Declaration in Soong¶
A library declares itself as LL-NDK by including an llndk block within its
cc_library or cc_library_shared definition. For example, libnativewindow:
// frameworks/native/libs/nativewindow/Android.bp (lines 72-80)
cc_library {
name: "libnativewindow",
llndk: {
symbol_file: "libnativewindow.map.txt",
unversioned: true,
override_export_include_dirs: [
"include",
],
export_llndk_headers: [
And libbinder_ndk:
// frameworks/native/libs/binder/ndk/Android.bp (lines 77-85)
cc_library {
name: "libbinder_ndk",
// ...
llndk: {
symbol_file: "libbinder_ndk.map.txt",
},
Similarly, libmediandk:
// frameworks/av/media/ndk/Android.bp (lines 87-91)
cc_library_shared {
name: "libmediandk",
llndk: {
symbol_file: "libmediandk.map.txt",
},
11.4.3 LLNDK Properties¶
The LL-NDK property structure is defined in
build/soong/cc/llndk_library.go:
// build/soong/cc/llndk_library.go (lines 30-63)
type llndkLibraryProperties struct {
// Relative path to the symbol map.
Symbol_file *string `android:"path,arch_variant"`
// Whether to export headers as -isystem instead of -I.
Export_headers_as_system *bool
// Whether the system library uses symbol versions.
Unversioned *bool
// List of llndk headers to re-export.
Export_llndk_headers []string
// Override export include dirs for the LLNDK variant.
Override_export_include_dirs []string
// Whether this module can be directly depended upon by
// vendor/product libraries.
Private *bool
// If true, provide headers to other LLNDK modules.
Llndk_headers *bool
// Marks this module as having been distributed through an apex.
Moved_to_apex *bool
}
The Private property is noteworthy: when set to true, the library is
accessible to other VNDK libraries but not directly to vendor code. This allows
the platform to use a library as an internal implementation detail of the VNDK
without exposing it to all vendor modules.
11.4.4 LLNDK Mutator¶
The llndkMutator in llndk_library.go marks modules as LL-NDK during the
build:
// build/soong/cc/llndk_library.go (lines 224-249)
func llndkMutator(mctx android.BottomUpMutatorContext) {
m, ok := mctx.Module().(*Module)
if !ok {
return
}
if shouldSkipLlndkMutator(mctx, m) {
return
}
lib, isLib := m.linker.(*libraryDecorator)
prebuiltLib, isPrebuiltLib := m.linker.(*prebuiltLibraryLinker)
if m.InVendorOrProduct() && isLib && lib.HasLLNDKStubs() {
m.VendorProperties.IsLLNDK = true
}
if m.InVendorOrProduct() && isPrebuiltLib &&
prebuiltLib.HasLLNDKStubs() {
m.VendorProperties.IsLLNDK = true
}
// ...
}
The mutator skips modules that are disabled, not device targets, or NativeBridge targets:
// build/soong/cc/llndk_library.go (lines 252-263)
func shouldSkipLlndkMutator(mctx android.BottomUpMutatorContext,
m *Module) bool {
if !m.Enabled(mctx) {
return true
}
if !m.Device() {
return true
}
if m.Target().NativeBridge == android.NativeBridgeEnabled {
return true
}
return false
}
11.4.5 LLNDK Libraries List Generation¶
The llndk_libraries_txt singleton module generates a text file listing all
LL-NDK libraries. This file is used by Make and by the linker configuration
generator:
// build/soong/cc/llndk_library.go (lines 127-138)
// llndk_libraries_txt is a singleton module whose content is a list
// of LLNDK libraries generated by Soong but can be referenced by
// other modules.
func llndkLibrariesTxtFactory() android.SingletonModule {
m := &llndkLibrariesTxtModule{}
android.InitAndroidArchModule(m, android.DeviceSupported,
android.MultilibCommon)
return m
}
The Make variable LLNDK_LIBRARIES is set from this module:
// build/soong/cc/llndk_library.go (lines 210-212)
func (txt *llndkLibrariesTxtModule) MakeVars(
ctx android.MakeVarsContext) {
ctx.Strict("LLNDK_LIBRARIES",
strings.Join(txt.moduleNames, " "))
}
11.4.6 LL-NDK vs NDK: Architecture Comparison¶
graph TD
subgraph "Application Process"
APP["App .so"] --> NDKLIB["NDK Libraries<br/>(stub at build time,<br/>real at runtime)"]
APP --> LLNDKLIB1["LL-NDK Libraries<br/>(libc, liblog, ...)"]
end
subgraph "Vendor Process"
VEN["Vendor .so"] --> LLNDKLIB2["LL-NDK Libraries<br/>(same libc, liblog, ...)"]
VEN --> VNDKLIB["VNDK Libraries"]
end
subgraph "System Partition"
LLNDKLIB1 --> REAL["Real system .so files<br/>/system/lib64/"]
LLNDKLIB2 --> REAL
end
style APP fill:#4a90d9,color:white
style VEN fill:#ff8c00,color:white
style LLNDKLIB1 fill:#50c878,color:white
style LLNDKLIB2 fill:#50c878,color:white
style VNDKLIB fill:#9932cc,color:white
The key difference: LL-NDK libraries live on the system partition but are
accessible to code on the vendor partition. Regular NDK libraries like
libcamera2ndk are only available in the app linker namespace. LL-NDK
libraries form the minimal shared ABI between system and vendor partitions.
11.4.7 Moved-to-Apex LLNDK Libraries¶
Some LL-NDK libraries have been moved into APEX modules. The
movedToApexLlndkLibraries singleton tracks these:
// build/soong/cc/llndk_library.go (lines 74-103)
func (s *movedToApexLlndkLibraries) GenerateBuildActions(
ctx android.SingletonContext) {
movedToApexLlndkLibrariesMap := make(map[string]bool)
ctx.VisitAllModuleProxies(func(module android.ModuleProxy) {
if library, ok := android.OtherModuleProvider(ctx, module,
LinkableInfoProvider); ok {
if library.HasLLNDKStubs &&
library.IsLLNDKMovedToApex {
movedToApexLlndkLibrariesMap[
library.ImplementationModuleName] = true
}
}
})
// ...
}
This generates the LLNDK_MOVED_TO_APEX_LIBRARIES Make variable, which the
linker configuration generator uses to set up namespace fallback paths to
APEX directories.
11.5 VNDK -- Vendor NDK¶
11.5.1 The Vendor Stability Problem¶
Before Android 8.0 (Oreo), vendors could link against any library on the system partition. This created a fragile coupling: when Google updated system libraries in a platform release, vendor code often broke because it depended on internal symbols that changed. This forced a painful "big-bang" integration cycle for every Android release.
The VNDK (Vendor Native Development Kit) was introduced in Android 8.0 to solve this problem. It defines a set of system libraries that vendor code is permitted to use, with the guarantee that these libraries maintain ABI compatibility across platform updates.
11.5.2 VNDK Architecture¶
graph TD
subgraph "System Partition (/system)"
SYSLIBS["System-only Libraries<br/>(libgui, libui, libsurfaceflinger, ...)"]
LLNDK["LL-NDK Libraries<br/>(libc, libm, libdl, liblog, ...)"]
VNDK_CORE["VNDK-Core Libraries<br/>(libcutils, libutils, libbase, ...)"]
VNDK_SP["VNDK-SP Libraries<br/>(Same-Process HALs:<br/>libhardware, libc++, ...)"]
end
subgraph "Vendor Partition (/vendor)"
VENDOR["Vendor Libraries<br/>& HAL Implementations"]
end
VENDOR -->|Allowed| LLNDK
VENDOR -->|Allowed| VNDK_CORE
VENDOR -->|Allowed| VNDK_SP
VENDOR -.->|BLOCKED| SYSLIBS
style SYSLIBS fill:#dc143c,color:white
style LLNDK fill:#50c878,color:white
style VNDK_CORE fill:#4a90d9,color:white
style VNDK_SP fill:#7b68ee,color:white
style VENDOR fill:#ff8c00,color:white
The VNDK is divided into several categories:
| Category | Description | Example |
|---|---|---|
| VNDK-Core | Standard VNDK libraries | libcutils, libutils, libbase |
| VNDK-SP | Same-Process VNDK libraries (can be loaded into vendor processes alongside vendor libs) | libhardware, libc++, libhidlbase |
| VNDK-Private | VNDK libraries not directly usable by vendor code | Internal dependencies of VNDK |
| LL-NDK | Lowest-level NDK (cross-partition) | libc, libm, liblog |
11.5.3 VNDK Declaration in Soong¶
A library declares itself as VNDK by including a vndk block. The properties
are defined in build/soong/cc/vndk.go:
// build/soong/cc/vndk.go (lines 45-76)
type VndkProperties struct {
Vndk struct {
// declared as a VNDK or VNDK-SP module.
Enabled *bool
// declared as a VNDK-SP module, which is a subset of VNDK.
// All these modules are allowed to link to VNDK-SP or LL-NDK
// modules only.
Support_system_process *bool
// declared as a VNDK-private module.
// Only available to other VNDK modules, not to vendor code.
Private *bool
// Extending another module
Extends *string
}
}
A typical VNDK declaration looks like:
For VNDK-SP (Same-Process) libraries:
cc_library_shared {
name: "libhardware",
vendor_available: true,
vndk: {
enabled: true,
support_system_process: true,
},
// ...
}
11.5.4 VNDK Link-Type Checking¶
The build system enforces VNDK dependency rules at build time. The linking constraints are:
| Module type | Can link to |
|---|---|
| Vendor | LL-NDK, VNDK-Core, VNDK-SP, other vendor libs |
| VNDK-Core | LL-NDK, VNDK-Core, VNDK-SP |
| VNDK-SP | LL-NDK, VNDK-SP only |
| System | Any system library |
These rules create a strict hierarchy:
graph BT
LLNDK_LAYER["LL-NDK<br/>(libc, libm, liblog, ...)"]
VNDKSP_LAYER["VNDK-SP<br/>(libc++, libhardware, ...)"]
VNDKCORE_LAYER["VNDK-Core<br/>(libcutils, libutils, ...)"]
VENDOR_LAYER["Vendor Libraries"]
VENDOR_LAYER --> VNDKCORE_LAYER
VENDOR_LAYER --> VNDKSP_LAYER
VENDOR_LAYER --> LLNDK_LAYER
VNDKCORE_LAYER --> VNDKSP_LAYER
VNDKCORE_LAYER --> LLNDK_LAYER
VNDKSP_LAYER --> LLNDK_LAYER
style LLNDK_LAYER fill:#50c878,color:white
style VNDKSP_LAYER fill:#7b68ee,color:white
style VNDKCORE_LAYER fill:#4a90d9,color:white
style VENDOR_LAYER fill:#ff8c00,color:white
If a VNDK-SP library attempts to link against a VNDK-Core library, the build fails with a link-type error. This strict hierarchy prevents the circular dependency problems that plagued pre-Oreo Android.
11.5.5 VNDK Library List Files¶
The build system generates several text files that enumerate the VNDK:
// build/soong/cc/vndk.go (lines 23-29)
const (
llndkLibrariesTxt = "llndk.libraries.txt"
vndkCoreLibrariesTxt = "vndkcore.libraries.txt"
vndkSpLibrariesTxt = "vndksp.libraries.txt"
vndkPrivateLibrariesTxt = "vndkprivate.libraries.txt"
vndkProductLibrariesTxt = "vndkproduct.libraries.txt"
)
Each file is versioned:
// build/soong/cc/vndk.go (lines 78-83)
func insertVndkVersion(filename string, vndkVersion string) string {
if index := strings.LastIndex(filename, "."); index != -1 {
return filename[:index] + "." + vndkVersion +
filename[index:]
}
return filename
}
So for VNDK version 34, the file names become vndkcore.libraries.34.txt,
vndksp.libraries.34.txt, etc.
11.5.6 VNDK Prebuilt Snapshots¶
When Google releases a new platform version, it also ships a VNDK snapshot --
a set of prebuilt VNDK libraries for the previous version. This allows vendors
to use an older platform's VNDK without rebuilding. The vndk_prebuilt_shared
module type handles this:
// build/soong/cc/vndk_prebuilt.go (lines 51-73)
type vndkPrebuiltProperties struct {
VndkProperties
// VNDK snapshot version.
Version *string
// Target arch name of the snapshot.
Target_arch *string
// If the prebuilt snapshot lib is built with 32-bit binder.
Binder32bit *bool
// Prebuilt files for each arch.
Srcs []string `android:"arch_variant"`
// Flags for linking.
Export_flags []string `android:"arch_variant"`
// Check the prebuilt ELF files.
Check_elf_files *bool `android:"arch_variant"`
}
A VNDK prebuilt declaration:
// Example from build/soong/cc/vndk_prebuilt.go comments
vndk_prebuilt_shared {
name: "libfoo",
version: "27",
target_arch: "arm64",
vendor_available: true,
product_available: true,
vndk: {
enabled: true,
},
export_include_dirs: [
"include/external/libfoo/vndk_include"
],
arch: {
arm64: {
srcs: ["arm/lib64/libfoo.so"],
},
arm: {
srcs: ["arm/lib/libfoo.so"],
},
},
}
The prebuilt module matches against the device configuration:
// build/soong/cc/vndk_prebuilt.go (lines 186-198)
func (p *vndkPrebuiltLibraryDecorator) MatchesWithDevice(
config android.DeviceConfig) bool {
arches := config.Arches()
if len(arches) == 0 ||
arches[0].ArchType.String() != p.arch() {
return false
}
if config.BinderBitness() != p.binderBit() {
return false
}
if len(p.properties.Srcs) == 0 {
return false
}
return true
}
11.5.7 Linker Namespace Isolation¶
The VNDK's stability guarantee is enforced at runtime through the dynamic
linker's namespace isolation. The configuration is generated by the
linkerconfig tool located at system/linkerconfig/.
The static ld.config.txt at system/core/rootdir/etc/ld.config.txt now
contains only a redirect:
# This file is no longer in use.
# Please update linker configuration generator instead.
# You can find the code from /system/linkerconfig
The generated configuration (visible in test golden files at
system/linkerconfig/testdata/golden_output/vendor_with_vndk/ld.config.txt)
shows the namespace architecture:
For the vendor section:
[vendor]
additional.namespaces = ...,system,vndk
namespace.default.isolated = true
namespace.default.search.paths = /odm/${LIB}
namespace.default.search.paths += /vendor/${LIB}
namespace.default.search.paths += /vendor/${LIB}/hw
namespace.default.search.paths += /vendor/${LIB}/egl
Vendor code in the default namespace can only load libraries from
/odm/${LIB} and /vendor/${LIB}. To access system libraries, it must go
through explicit links to other namespaces:
namespace.default.links = rs,system,vndk,...
namespace.default.link.system.shared_libs = libc.so:libm.so:libdl.so:
liblog.so:libbinder_ndk.so:libmediandk.so:libnativewindow.so:
libvulkan.so:libEGL.so:libGLESv1_CM.so:libGLESv2.so:libGLESv3.so:
libsync.so:libvndksupport.so:...
namespace.default.link.vndk.shared_libs = libcutils.so:libutils.so:
libbase.so:libhidlbase.so:libc++.so:...
The link.system.shared_libs list corresponds to LL-NDK libraries.
The link.vndk.shared_libs list corresponds to VNDK libraries.
graph TD
subgraph "Vendor Process Namespaces"
DEF["default namespace<br/>/vendor/lib64/<br/>/odm/lib64/"]
SYS["system namespace<br/>/system/lib64/"]
VNDK_NS["vndk namespace<br/>/apex/com.android.vndk.v*/lib64/"]
end
DEF -->|"LL-NDK libs<br/>(libc, liblog, ...)"| SYS
DEF -->|"VNDK libs<br/>(libcutils, ...)"| VNDK_NS
DEF -.->|"BLOCKED:<br/>libgui, libui,<br/>other internal libs"| SYS
style DEF fill:#ff8c00,color:white
style SYS fill:#4a90d9,color:white
style VNDK_NS fill:#9932cc,color:white
11.5.8 VNDK-SP: Same-Process Libraries¶
VNDK-SP (Same-Process) libraries are a special subset that can be loaded into the same process as vendor code without going through an IPC boundary. This is necessary for HALs that are loaded as shared libraries directly into system processes (e.g., the graphics HAL loaded into SurfaceFlinger).
The key constraint on VNDK-SP is tighter than regular VNDK: VNDK-SP libraries can only depend on other VNDK-SP libraries or LL-NDK libraries. This prevents circular dependencies between system and vendor code loaded in the same process.
11.5.9 The VNDK Deprecation Trend¶
Starting with Android 14, Google has been gradually reducing the VNDK's scope.
The Vendor API Level concept (RELEASE_BOARD_API_LEVEL) replaces the VNDK
version, and the VNDK APEX (com.android.vndk.v*) provides a cleaner packaging
mechanism than the flat directory structure. For builds without VNDK, the linker
configuration simplifies significantly -- vendor code links directly against
system libraries, with namespace isolation relying on stub libraries and the
linker config generator rather than a separate VNDK directory.
11.6 NDK Framework Bindings¶
The NDK does not merely expose low-level system functions. It also provides C bindings to major Android framework services: Camera, Media, and Binder. These bindings follow a consistent architecture: a C API layer that wraps internal C++ framework objects, with strict symbol visibility control.
11.6.1 Camera NDK (libcamera2ndk)¶
The Camera NDK is located at frameworks/av/camera/ndk/. It exposes the
Camera2 API -- the same camera pipeline used by the Java
android.hardware.camera2 package -- through C functions.
Source Structure¶
frameworks/av/camera/ndk/
Android.bp # Build rules
NdkCameraCaptureSession.cpp # Capture session API
NdkCameraDevice.cpp # Device open/close/create request
NdkCameraManager.cpp # Camera enumeration and callbacks
NdkCameraMetadata.cpp # Metadata (settings, results)
NdkCaptureRequest.cpp # Capture request construction
impl/ # Internal implementation
ACameraCaptureSession.cpp
ACameraDevice.cpp
ACameraManager.cpp
ACameraMetadata.cpp
ndk_vendor/ # Vendor variant (uses AIDL HAL)
impl/
ACameraDevice.cpp
ACameraManager.cpp
utils.cpp
include/camera/ # Public NDK headers
NdkCameraCaptureSession.h
NdkCameraDevice.h
NdkCameraError.h
NdkCameraManager.h
NdkCameraMetadata.h
NdkCameraMetadataTags.h
NdkCameraWindowType.h
NdkCaptureRequest.h
libcamera2ndk.map.txt # Symbol export map
NDK and Header Declarations¶
The Camera NDK declares both its stub library and its headers in the same
Android.bp:
// frameworks/av/camera/ndk/Android.bp (lines 51-64)
ndk_library {
name: "libcamera2ndk",
symbol_file: "libcamera2ndk.map.txt",
first_version: "24",
unversioned_until: "current",
}
ndk_headers {
name: "libcamera2ndk_headers",
from: "include/camera",
to: "camera",
srcs: ["include/camera/**/*.h"],
license: "NOTICE",
}
Implementation Pattern¶
The Camera NDK functions follow a consistent pattern: a thin C wrapper that
delegates to an internal C++ implementation. Here is ACameraManager_create():
// frameworks/av/camera/ndk/NdkCameraManager.cpp (lines 38-41)
EXPORT
ACameraManager* ACameraManager_create() {
ATRACE_CALL();
return new ACameraManager();
}
And ACameraDevice_close():
// frameworks/av/camera/ndk/NdkCameraDevice.cpp (lines 31-39)
EXPORT
camera_status_t ACameraDevice_close(ACameraDevice* device) {
ATRACE_CALL();
if (device == nullptr) {
ALOGE("%s: invalid argument! device is null",
__FUNCTION__);
return ACAMERA_ERROR_INVALID_PARAMETER;
}
delete device;
return ACAMERA_OK;
}
The EXPORT macro is defined as __attribute__((visibility("default"))), and
the library is compiled with -fvisibility=hidden. This ensures that only
functions explicitly marked with EXPORT appear in the shared library's dynamic
symbol table:
// frameworks/av/camera/ndk/Android.bp (lines 102-108)
cflags: [
"-DEXPORT=__attribute__((visibility(\"default\")))",
"-Wall",
"-Werror",
"-Wextra",
"-fvisibility=hidden",
],
Vendor vs Non-Vendor Variants¶
The Camera NDK has two variants. The standard library (libcamera2ndk) uses
the framework's internal CameraService binder interface:
// frameworks/av/camera/ndk/NdkCameraManager.cpp (lines 27-32)
#ifdef __ANDROID_VNDK__
#include "ndk_vendor/impl/ACameraManager.h"
#else
#include "impl/ACameraManager.h"
#endif
The vendor variant (libcamera2ndk_vendor) uses the AIDL camera service HAL
interface instead, allowing vendor code to access the camera without going
through the system camera service.
Camera NDK Call Flow¶
sequenceDiagram
participant App as Native App
participant CNDK as libcamera2ndk.so
participant Impl as ACameraManager (impl)
participant CS as CameraService (Binder)
participant HAL as Camera HAL
App->>CNDK: ACameraManager_create()
CNDK->>Impl: new ACameraManager()
App->>CNDK: ACameraManager_getCameraIdList()
CNDK->>Impl: getCameraIdList()
Impl->>CS: getCameraIdList() [Binder IPC]
CS->>HAL: Query available cameras
HAL-->>CS: Camera ID list
CS-->>Impl: Camera ID list
Impl-->>CNDK: ACameraIdList*
CNDK-->>App: camera_status_t
App->>CNDK: ACameraManager_openCamera()
CNDK->>Impl: openCamera()
Impl->>CS: connectDevice() [Binder IPC]
CS->>HAL: open()
HAL-->>CS: Device handle
CS-->>Impl: ICameraDeviceUser
Impl-->>CNDK: ACameraDevice*
CNDK-->>App: camera_status_t
11.6.2 Media NDK (libmediandk)¶
The Media NDK at frameworks/av/media/ndk/ provides native access to media
codecs, extractors, DRM, and image readers.
Source Structure¶
frameworks/av/media/ndk/
Android.bp # Build rules
NdkMediaCodec.cpp # MediaCodec wrapper
NdkMediaCodecInfo.cpp # Codec capability queries
NdkMediaCodecStore.cpp # Codec enumeration
NdkMediaCrypto.cpp # DRM/crypto support
NdkMediaDataSource.cpp # Data source abstraction
NdkMediaDrm.cpp # DRM session management
NdkMediaExtractor.cpp # Container format parsing
NdkMediaFormat.cpp # Key-value format metadata
NdkMediaMuxer.cpp # Container muxing
NdkImage.cpp # Image buffer access
NdkImageReader.cpp # Image reader (camera, video)
libmediandk.map.txt # Symbol export map
NDK Declarations¶
// frameworks/av/media/ndk/Android.bp (lines 50-63)
ndk_library {
name: "libmediandk",
symbol_file: "libmediandk.map.txt",
first_version: "21",
unversioned_until: "current",
}
ndk_headers {
name: "libmediandk_headers",
from: "include/media",
to: "media",
srcs: ["include/media/**/*.h"],
license: "NOTICE",
}
The Media NDK is one of the most widely used NDK libraries. It was introduced at API 21 and has been expanded significantly over the years. The library is also marked as LL-NDK, making it accessible to vendor code:
// frameworks/av/media/ndk/Android.bp (lines 87-91)
cc_library_shared {
name: "libmediandk",
llndk: {
symbol_file: "libmediandk.map.txt",
},
Key APIs¶
The Media NDK exposes several major API families:
MediaCodec -- hardware-accelerated video/audio encoding and decoding:
AMediaCodec_createDecoderByType()-- create a decoder for a MIME typeAMediaCodec_configure()-- configure with format parametersAMediaCodec_start()/AMediaCodec_stop()-- lifecycleAMediaCodec_dequeueInputBuffer()/AMediaCodec_queueInputBuffer()AMediaCodec_dequeueOutputBuffer()/AMediaCodec_releaseOutputBuffer()
MediaExtractor -- container format demuxing:
AMediaExtractor_new()-- create an extractorAMediaExtractor_setDataSource()-- set the inputAMediaExtractor_getTrackCount()/AMediaExtractor_getTrackFormat()AMediaExtractor_readSampleData()-- read compressed samples
ImageReader -- acquiring image buffers from camera or video:
AImageReader_new()-- create a reader with format/dimensionsAImageReader_acquireNextImage()-- acquire the next available imageAImage_getPlaneData()-- access pixel data
11.6.3 Binder NDK (libbinder_ndk)¶
The Binder NDK at frameworks/native/libs/binder/ndk/ provides a C interface
to Android's Binder IPC mechanism. This is critical for AIDL services that
need to be accessed from native code.
Source Structure¶
frameworks/native/libs/binder/ndk/
Android.bp # Build rules
ibinder.cpp # AIBinder implementation
ibinder_jni.cpp # JNI integration
libbinder.cpp # AServiceManager, etc.
parcel.cpp # AParcel data marshaling
parcel_jni.cpp # Parcel JNI bridge
persistable_bundle.cpp # PersistableBundle support
process.cpp # Process state management
service_manager.cpp # Service registration/lookup
binder_rpc.cpp # RPC Binder support
stability.cpp # Stability enforcement
status.cpp # AStatus wrapper
include_ndk/android/ # NDK headers
binder_ibinder.h
binder_ibinder_jni.h
binder_parcel.h
binder_parcel_jni.h
binder_status.h
persistable_bundle.h
include_cpp/ # C++ convenience wrappers
include_platform/ # Platform-internal headers
NDK and LLNDK Declarations¶
The Binder NDK is both an NDK library (for apps) and an LL-NDK library (for vendor code):
// frameworks/native/libs/binder/ndk/Android.bp (lines 77-85, 271-279)
cc_library {
name: "libbinder_ndk",
// ...
llndk: {
symbol_file: "libbinder_ndk.map.txt",
},
// ...
stubs: {
symbol_file: "libbinder_ndk.map.txt",
versions: [
"29",
"30",
],
},
}
ndk_library {
name: "libbinder_ndk",
symbol_file: "libbinder_ndk.map.txt",
first_version: "29",
// ...
}
Implementation Pattern¶
The Binder NDK wraps libbinder's C++ classes in C-compatible types. The
implementation in ibinder.cpp shows the pattern:
// frameworks/native/libs/binder/ndk/ibinder.cpp (lines 17-47)
#include <android/binder_ibinder.h>
#include <android/binder_ibinder_platform.h>
#include <android/binder_stability.h>
#include <android/binder_status.h>
#include <binder/Functional.h>
#include <binder/IPCThreadState.h>
// ...
using ::android::IBinder;
using ::android::Parcel;
using ::android::sp;
using ::android::status_t;
// ...
AIBinder::AIBinder(const AIBinder_Class* clazz) : mClazz(clazz) {}
AIBinder::~AIBinder() {}
Key APIs¶
The Binder NDK provides:
Service Management:
AServiceManager_addService()-- register a serviceAServiceManager_getService()-- look up a service by nameAServiceManager_waitForService()-- block until a service appears
Binder Objects:
AIBinder_Class_define()-- define a binder interface classAIBinder_new()-- create a local binder objectAIBinder_prepareTransaction()/AIBinder_transact()-- IPC callsAIBinder_linkToDeath()/AIBinder_unlinkToDeath()-- death notifications
Parcels:
AParcel_writeInt32()/AParcel_readInt32()-- marshaling primitivesAParcel_writeString()/AParcel_readString()-- string marshalingAParcel_writeStrongBinder()/AParcel_readStrongBinder()-- pass binders
AIDL Integration:
AIDL-generated code for NDK backends produces C++ wrappers that call through
the libbinder_ndk C API. This allows services defined in AIDL to be
implemented and consumed in pure native code without any Java dependency.
Binder NDK Call Flow¶
sequenceDiagram
participant Client as Client Process
participant BNDK as libbinder_ndk.so
participant Binder as libbinder.so
participant Driver as /dev/binder
participant Server as Server Process
Client->>BNDK: AServiceManager_getService("foo")
BNDK->>Binder: ServiceManager::getService()
Binder->>Driver: ioctl(BINDER_WRITE_READ)
Driver->>Server: Deliver transaction
Server-->>Driver: Reply
Driver-->>Binder: Reply data
Binder-->>BNDK: sp<IBinder>
BNDK-->>Client: AIBinder*
Client->>BNDK: AIBinder_prepareTransaction(binder, &parcel)
Client->>BNDK: AParcel_writeInt32(parcel, 42)
Client->>BNDK: AIBinder_transact(binder, code, &in, &out, 0)
BNDK->>Binder: IBinder::transact()
Binder->>Driver: ioctl(BINDER_WRITE_READ)
Driver->>Server: Deliver transaction
Server-->>Driver: Reply
Driver-->>Binder: Reply parcel
Binder-->>BNDK: Parcel
BNDK-->>Client: AParcel* (output)
11.6.4 Framework Binding Architecture Summary¶
All three framework bindings share a common architecture:
graph TD
subgraph "NDK Layer (C API)"
CAMERA_C["Camera NDK<br/>ACameraManager_*<br/>ACameraDevice_*"]
MEDIA_C["Media NDK<br/>AMediaCodec_*<br/>AMediaExtractor_*"]
BINDER_C["Binder NDK<br/>AIBinder_*<br/>AParcel_*"]
end
subgraph "Implementation Layer (C++)"
CAMERA_IMPL["ACameraManager<br/>ACameraDevice<br/>ACaptureRequest"]
MEDIA_IMPL["libmedia<br/>libstagefright<br/>libmedia_codeclist"]
BINDER_IMPL["libbinder<br/>IPCThreadState<br/>Parcel"]
end
subgraph "Service Layer (Binder IPC)"
CAMERA_SVC["CameraService"]
MEDIA_SVC["MediaCodecService<br/>MediaDrmService"]
SM["ServiceManager"]
end
CAMERA_C --> CAMERA_IMPL
MEDIA_C --> MEDIA_IMPL
BINDER_C --> BINDER_IMPL
CAMERA_IMPL --> CAMERA_SVC
MEDIA_IMPL --> MEDIA_SVC
BINDER_IMPL --> SM
style CAMERA_C fill:#ff8c00,color:white
style MEDIA_C fill:#ff8c00,color:white
style BINDER_C fill:#ff8c00,color:white
style CAMERA_IMPL fill:#4a90d9,color:white
style MEDIA_IMPL fill:#4a90d9,color:white
style BINDER_IMPL fill:#4a90d9,color:white
The pattern is always:
- C header (
NdkFoo.h) -- defines the public API with opaque pointer types - C source (
NdkFoo.cpp) -- thin wrappers marked withEXPORT - C++ implementation (
impl/AFoo.cpp) -- actual logic using framework APIs - Symbol map (
libfoo.map.txt) -- controls which symbols are exported - Visibility control --
-fvisibility=hidden+EXPORTmacro
11.7 NDK Translation Packages¶
11.7.1 What Are NDK Translation Packages?¶
NDK translation packages are a build-system mechanism for packaging libraries
and binaries required by NativeBridge -- the system that translates native
code from one architecture to another (e.g., running ARM code on an x86
device). The ndk_translation_package module type, introduced in 2025 at
build/soong/cc/ndk_translation_package.go, gathers translation-related
dependencies and produces a distributable zip archive.
11.7.2 The ndk_translation_package Module Type¶
The module type is registered as:
// build/soong/cc/ndk_translation_package.go (lines 28-29)
func init() {
android.RegisterModuleType("ndk_translation_package",
NdkTranslationPackageFactory)
}
The factory creates a module that targets multiple architectures:
// build/soong/cc/ndk_translation_package.go (lines 32-37)
func NdkTranslationPackageFactory() android.Module {
module := &ndkTranslationPackage{}
module.AddProperties(&module.properties)
android.InitAndroidMultiTargetsArchModule(module,
android.DeviceSupported, android.MultilibCommon)
return module
}
11.7.3 Properties¶
The ndk_translation_package module type has a rich set of dependency
properties that reflect the multi-architecture nature of translation:
// build/soong/cc/ndk_translation_package.go (lines 46-80)
type ndkTranslationPackageProperties struct {
// Dependencies with native bridge variants that should be
// packaged (e.g. arm and arm64 on an x86_64 device)
Native_bridge_deps proptools.Configurable[[]string]
// Non-native bridge variants that should be packaged
// (e.g. x86 and x86_64 on an x86_64 device)
Device_both_deps []string
// Non-native bridge variants with lib64
Device_64_deps []string
// Non-native bridge variants with lib32
Device_32_deps []string
// Non-native bridge first variant
Device_first_deps []string
// First variant, always into lib/ directories
Device_first_to_32_deps []string
// Dependencies for build file generation only
Device_both_extra_allowed_deps []string
Device_32_extra_allowed_deps []string
// Version for sysprops
Version *string
// Path to Android.bp generator
Android_bp_gen_path *string
// Path to product.mk generator
Product_mk_gen_path *string
// Whether to generate build files (default true)
Generate_build_files *bool
}
11.7.4 Dependency Resolution¶
The DepsMutator maps each dependency category to the appropriate architecture
variant:
// build/soong/cc/ndk_translation_package.go (lines 108-127)
func (n *ndkTranslationPackage) DepsMutator(
ctx android.BottomUpMutatorContext) {
for index, t := range ctx.MultiTargets() {
if t.NativeBridge == android.NativeBridgeEnabled {
ctx.AddFarVariationDependencies(t.Variations(),
ndkTranslationPackageTag,
n.properties.Native_bridge_deps.GetOrDefault(
ctx, nil)...)
} else if t.Arch.ArchType == android.X86_64 {
ctx.AddFarVariationDependencies(t.Variations(),
ndkTranslationPackageTag,
n.properties.Device_64_deps...)
ctx.AddFarVariationDependencies(t.Variations(),
ndkTranslationPackageTag,
n.properties.Device_both_deps...)
} else if t.Arch.ArchType == android.X86 {
ctx.AddFarVariationDependencies(t.Variations(),
ndkTranslationPackageTag,
n.properties.Device_32_deps...)
// ...
}
if index == 0 { // Primary arch
ctx.AddFarVariationDependencies(t.Variations(),
ndkTranslationPackageTag,
n.properties.Device_first_deps...)
}
}
}
This allows the package to collect:
- NativeBridge variants -- ARM/ARM64 libraries compiled for an x86 device that will be used by the translation layer
- Device variants -- x86/x86_64 libraries needed by the host side of the translation
11.7.5 RISC-V Consideration¶
The dependency tag includes a special allowance for disabled RISC-V modules:
// build/soong/cc/ndk_translation_package.go (lines 90-93)
func (_ ndkTranslationPackageDepTag) AllowDisabledModuleDependency(
target android.Module) bool {
return target.Target().NativeBridge ==
android.NativeBridgeEnabled &&
target.Target().Arch.ArchType == android.Riscv64
}
This is forward-looking: RISC-V native bridge support is still emerging, and some translation dependencies may not have RISC-V variants yet. Rather than breaking the build, the module type gracefully handles missing RISC-V dependencies.
11.7.6 Package Generation¶
The GenerateAndroidBuildActions method collects all dependency files and
packages them into a zip archive:
// build/soong/cc/ndk_translation_package.go (lines 129-181)
func (n *ndkTranslationPackage) GenerateAndroidBuildActions(
ctx android.ModuleContext) {
var files []android.PackagingSpec
var files64 []android.PackagingSpec
ctx.VisitDirectDepsProxy(func(child android.ModuleProxy) {
tag := ctx.OtherModuleDependencyTag(child)
info := android.OtherModuleProviderOrDefault(ctx, child,
android.InstallFilesProvider)
// ... categorize files by architecture
files = append(files, info.PackagingSpecs...)
})
outZip := android.PathForModuleOut(ctx,
ctx.ModuleName()+".zip")
builder := android.NewRuleBuilder(pctx, ctx)
cmd := builder.Command().
BuiltTool("soong_zip").
FlagWithOutput("-o ", outZip)
// Generate build files if enabled
if proptools.BoolDefault(
n.properties.Generate_build_files, true) {
outBp := n.genAndroidBp(ctx, files)
outArm64ArmMk, outArm64Mk := n.genProductMk(ctx,
files, files64, extraFiles, extraFiles64)
// ...
}
for _, file := range files {
cmd.
FlagWithArg("-e ", "system/"+
file.RelPathInPackage()).
FlagWithInput("-f ", file.SrcPath())
}
builder.Build("ndk_translation_package.zip", ...)
}
11.7.7 Build File Generation¶
The package generates two types of build files:
- Android.bp -- for building the translation package as part of the platform build
- product.mk -- for inclusion in device makefiles
The Android.bp generator:
// build/soong/cc/ndk_translation_package.go (lines 184-199)
func (n *ndkTranslationPackage) genAndroidBp(
ctx android.ModuleContext,
files []android.PackagingSpec) android.Path {
genDir := android.PathForModuleOut(ctx, "android_bp_dir")
generator := android.PathForModuleSrc(ctx,
proptools.String(n.properties.Android_bp_gen_path))
builder := android.NewRuleBuilder(pctx, ctx).Sbox(
genDir,
android.PathForModuleOut(ctx,
"Android.bp.sbox.textproto"),
)
outBp := genDir.Join(ctx, "Android.bp")
builder.Command().
Input(generator).
Implicits(specsToSrcPaths(files)).
Flag(strings.Join(
filesRelativeToInstallDir(ctx, files), " ")).
FlagWithOutput("> ", outBp)
builder.Build("ndk_translation_package.Android.bp", ...)
return outBp
}
The product.mk generator creates two variants -- one for ARM64+ARM and one for ARM64-only:
// build/soong/cc/ndk_translation_package.go (lines 203-239)
func (n *ndkTranslationPackage) genProductMk(
ctx android.ModuleContext,
files, files64, extraFiles, extraFiles64
[]android.PackagingSpec) (android.Path, android.Path) {
// Both arches
// ...
builder.Command().
Input(generator).
FlagWithArg("--version=",
proptools.String(n.properties.Version)).
Flag("--arm64 --arm").
// ...
// ARM64 only
// ...
builder.Command().
Input(generator).
FlagWithArg("--version=",
proptools.String(n.properties.Version)).
Flag("--arm64").
// ...
}
11.7.8 NDK Translation Package Architecture¶
graph TD
subgraph "ndk_translation_package Module"
PROPS["Properties<br/>native_bridge_deps<br/>device_64_deps<br/>device_32_deps"]
DEPS["DepsMutator<br/>Resolve multi-arch<br/>dependencies"]
GEN["GenerateAndroidBuildActions<br/>Package into .zip"]
end
subgraph "Inputs"
NB_ARM["NativeBridge ARM libs<br/>(translated libraries)"]
NB_ARM64["NativeBridge ARM64 libs<br/>(translated libraries)"]
HOST_X86["Host x86 libs<br/>(translator runtime)"]
HOST_X64["Host x86_64 libs<br/>(translator runtime)"]
end
subgraph "Outputs"
ZIP["ndk_translation.zip<br/>system/lib/<br/>system/lib64/<br/>system/bin/"]
BP["Android.bp<br/>(generated)"]
MK["product_arm64_arm.mk<br/>product_arm64.mk<br/>(generated)"]
end
NB_ARM --> DEPS
NB_ARM64 --> DEPS
HOST_X86 --> DEPS
HOST_X64 --> DEPS
PROPS --> DEPS
DEPS --> GEN
GEN --> ZIP
GEN --> BP
GEN --> MK
style PROPS fill:#4a90d9,color:white
style ZIP fill:#50c878,color:white
style BP fill:#ff8c00,color:white
style MK fill:#ff8c00,color:white
11.7.9 Connection to NativeBridge¶
The NDK translation package is the packaging layer for NativeBridge
implementations. The NativeBridge interface itself is defined in
frameworks/libs/binary_translation/native_bridge/ and provides the
NativeBridgeCallbacks structure that translation engines implement. The
translation package bundles all the shared libraries, configuration files, and
host-side tools that a NativeBridge implementation needs to run on the device.
On a device with NativeBridge enabled (e.g., an x86 device running ARM apps),
the translation package provides the libraries that the libnativebridge.so
runtime loads to perform instruction translation. The Native_bridge_deps
property specifically targets the translated (guest) architecture variants,
while the Device_*_deps properties target the host architecture variants.
11.8 Try It: Write a Native NDK App¶
This section walks through creating a minimal native Android application that uses several NDK APIs. We will build a native activity that initializes a window, logs messages, and queries sensor information.
11.8.1 Project Structure¶
11.8.2 The Manifest¶
A native activity requires a specific manifest configuration:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.nativedemo">
<application
android:label="Native Demo"
android:hasCode="false">
<activity
android:name="android.app.NativeActivity"
android:exported="true"
android:configChanges=
"orientation|keyboardHidden|screenSize">
<meta-data
android:name="android.app.lib_name"
android:value="native-demo" />
<intent-filter>
<action android:name=
"android.intent.action.MAIN" />
<category android:name=
"android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
Key points:
android:hasCode="false"-- no Java/Kotlin codeandroid:name="android.app.NativeActivity"-- the platform's built-in native activity hostandroid.app.lib_name-- the name of the shared library (withoutlibprefix and.sosuffix)
11.8.3 The Build File¶
For an AOSP tree build using Soong:
cc_library_shared {
name: "libnative-demo",
srcs: ["src/main.cpp"],
shared_libs: [
"libandroid",
"liblog",
"libnativewindow",
],
static_libs: [
"libandroid_native_app_glue",
],
sdk_version: "current",
stl: "c++_shared",
cflags: [
"-Wall",
"-Werror",
],
}
For an external NDK build using CMake:
cmake_minimum_required(VERSION 3.18)
project(native-demo)
add_library(native-demo SHARED src/main.cpp)
# Find the native_app_glue
find_library(log-lib log)
find_library(android-lib android)
target_link_libraries(native-demo
android
log
nativewindow
)
# Enable native app glue
set(APP_GLUE_DIR ${ANDROID_NDK}/sources/android/native_app_glue)
add_library(app-glue STATIC ${APP_GLUE_DIR}/android_native_app_glue.c)
target_include_directories(app-glue PUBLIC ${APP_GLUE_DIR})
target_link_libraries(native-demo app-glue)
11.8.4 The Application Code¶
// src/main.cpp -- Minimal NDK native activity
#include <android/log.h>
#include <android/native_activity.h>
#include <android/sensor.h>
#include <android_native_app_glue.h>
#include <cassert>
#include <cstring>
#define LOG_TAG "NativeDemo"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
// Application state
struct AppState {
struct android_app* app;
ASensorManager* sensorManager;
const ASensor* accelerometer;
ASensorEventQueue* sensorEventQueue;
bool windowReady;
bool running;
};
// Handle sensor events
static int handleSensorEvents(int /* fd */, int /* events */,
void* data) {
auto* state = static_cast<AppState*>(data);
ASensorEvent event;
while (ASensorEventQueue_getEvents(
state->sensorEventQueue, &event, 1) > 0) {
if (event.type == ASENSOR_TYPE_ACCELEROMETER) {
LOGI("Accelerometer: x=%.2f y=%.2f z=%.2f",
event.acceleration.x,
event.acceleration.y,
event.acceleration.z);
}
}
return 1; // Continue receiving events
}
// Initialize sensors
static void initSensors(AppState* state) {
state->sensorManager = ASensorManager_getInstance();
if (state->sensorManager == nullptr) {
LOGW("No sensor manager available");
return;
}
state->accelerometer = ASensorManager_getDefaultSensor(
state->sensorManager, ASENSOR_TYPE_ACCELEROMETER);
if (state->accelerometer == nullptr) {
LOGW("No accelerometer available");
return;
}
state->sensorEventQueue =
ASensorManager_createEventQueue(
state->sensorManager, state->app->looper,
LOOPER_ID_USER, handleSensorEvents, state);
LOGI("Sensors initialized successfully");
}
// Enable accelerometer
static void enableSensors(AppState* state) {
if (state->accelerometer != nullptr &&
state->sensorEventQueue != nullptr) {
ASensorEventQueue_enableSensor(
state->sensorEventQueue, state->accelerometer);
// Set event rate to 60 Hz
ASensorEventQueue_setEventRate(
state->sensorEventQueue, state->accelerometer,
(1000L / 60) * 1000);
LOGI("Accelerometer enabled");
}
}
// Disable accelerometer
static void disableSensors(AppState* state) {
if (state->accelerometer != nullptr &&
state->sensorEventQueue != nullptr) {
ASensorEventQueue_disableSensor(
state->sensorEventQueue, state->accelerometer);
LOGI("Accelerometer disabled");
}
}
// Handle application commands
static void handleCmd(struct android_app* app, int32_t cmd) {
auto* state = static_cast<AppState*>(app->userData);
switch (cmd) {
case APP_CMD_INIT_WINDOW:
LOGI("Window initialized: %p", app->window);
if (app->window != nullptr) {
// Query window properties
int32_t width = ANativeWindow_getWidth(app->window);
int32_t height =
ANativeWindow_getHeight(app->window);
int32_t format =
ANativeWindow_getFormat(app->window);
LOGI("Window size: %dx%d, format: %d",
width, height, format);
state->windowReady = true;
}
break;
case APP_CMD_TERM_WINDOW:
LOGI("Window terminated");
state->windowReady = false;
break;
case APP_CMD_GAINED_FOCUS:
LOGI("Gained focus -- enabling sensors");
enableSensors(state);
break;
case APP_CMD_LOST_FOCUS:
LOGI("Lost focus -- disabling sensors");
disableSensors(state);
break;
case APP_CMD_RESUME:
LOGI("Activity resumed");
state->running = true;
break;
case APP_CMD_PAUSE:
LOGI("Activity paused");
state->running = false;
break;
case APP_CMD_DESTROY:
LOGI("Activity destroyed");
break;
case APP_CMD_CONFIG_CHANGED:
LOGI("Configuration changed");
break;
case APP_CMD_LOW_MEMORY:
LOGW("Low memory warning");
break;
}
}
// Handle input events
static int32_t handleInput(struct android_app* /* app */,
AInputEvent* event) {
int32_t type = AInputEvent_getType(event);
if (type == AINPUT_EVENT_TYPE_MOTION) {
float x = AMotionEvent_getX(event, 0);
float y = AMotionEvent_getY(event, 0);
int32_t action =
AMotionEvent_getAction(event) &
AMOTION_EVENT_ACTION_MASK;
switch (action) {
case AMOTION_EVENT_ACTION_DOWN:
LOGI("Touch DOWN at (%.1f, %.1f)", x, y);
return 1;
case AMOTION_EVENT_ACTION_MOVE:
// Suppress move logs to avoid spam
return 1;
case AMOTION_EVENT_ACTION_UP:
LOGI("Touch UP at (%.1f, %.1f)", x, y);
return 1;
}
}
return 0; // Event not handled
}
// Main entry point -- called by native_app_glue
void android_main(struct android_app* app) {
LOGI("=== Native Demo Starting ===");
AppState state = {};
state.app = app;
state.running = true;
app->userData = &state;
app->onAppCmd = handleCmd;
app->onInputEvent = handleInput;
// Initialize sensors
initSensors(&state);
// Main event loop
LOGI("Entering main loop");
while (!app->destroyRequested) {
int events;
struct android_poll_source* source;
// Block if not running (paused/stopped),
// poll without blocking if running
int timeout = state.running ? 0 : -1;
while (ALooper_pollOnce(timeout, nullptr, &events,
reinterpret_cast<void**>(&source))
>= 0) {
if (source != nullptr) {
source->process(app, source);
}
if (app->destroyRequested) {
break;
}
}
// Application rendering/logic would go here
if (state.running && state.windowReady) {
// In a real app, you would:
// 1. Lock the ANativeWindow buffer
// 2. Draw to the buffer
// 3. Unlock and post the buffer
//
// Or use EGL/Vulkan for GPU rendering
}
}
// Cleanup
if (state.sensorEventQueue != nullptr) {
ASensorManager_destroyEventQueue(
state.sensorManager, state.sensorEventQueue);
}
LOGI("=== Native Demo Exiting ===");
}
11.8.5 Code Walkthrough¶
Entry Point and Threading Model¶
The native app glue library spawns a new thread and calls android_main() on
it. The main UI thread is handled by the glue's internal android_app_entry()
function, which forwards lifecycle callbacks from the NativeActivity to the
application thread via a pipe.
The ALooper_pollOnce() call is the heart of the event loop. It waits for
events from three sources:
LOOPER_ID_MAIN(command pipe) -- lifecycle events likeAPP_CMD_INIT_WINDOWLOOPER_ID_INPUT(input queue) -- touch, key, and motion eventsLOOPER_ID_USERand above -- custom sources like sensor events
NDK APIs Used¶
This example uses four NDK libraries:
liblog--__android_log_print()for logginglibandroid--ANativeWindow_*for window access,ASensor*for sensors,AInputEvent_*andAMotionEvent_*for input,ALooper_*for the event looplibnativewindow--ANativeWindow(shared withlibandroid)- Native app glue (static library) --
android_app, event loop glue
Event Flow¶
sequenceDiagram
participant UI as UI Thread (NativeActivity)
participant Pipe as Command Pipe
participant App as App Thread (android_main)
participant Looper as ALooper
UI->>Pipe: Write APP_CMD_INIT_WINDOW
App->>Looper: ALooper_pollOnce()
Looper->>App: LOOPER_ID_MAIN
App->>App: source->process()
App->>App: handleCmd(APP_CMD_INIT_WINDOW)
Note over App: Window is ready,<br/>start rendering
UI->>Pipe: Write APP_CMD_GAINED_FOCUS
App->>Looper: ALooper_pollOnce()
Looper->>App: LOOPER_ID_MAIN
App->>App: handleCmd(APP_CMD_GAINED_FOCUS)
App->>App: enableSensors()
Note over App: Sensor events start<br/>arriving on LOOPER_ID_USER
11.8.6 Building and Running¶
Building within AOSP¶
If the module is placed in the AOSP tree (e.g., under
packages/apps/NativeDemo/), build it with:
The output .so will be at:
Package it into an APK using aapt2 or the Android Gradle plugin.
Building with the Standalone NDK¶
If building outside the AOSP tree using the NDK toolchain:
# Set NDK path
export ANDROID_NDK=/path/to/android-ndk-r27
# Create build directory
mkdir build && cd build
# Configure with CMake
cmake -DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK/build/cmake/android.toolchain.cmake \
-DANDROID_ABI=arm64-v8a \
-DANDROID_PLATFORM=android-26 \
..
# Build
cmake --build .
Running on Device¶
# Install the APK
adb install native-demo.apk
# Launch
adb shell am start -n com.example.nativedemo/.NativeActivity
# Watch logs
adb logcat -s NativeDemo:V
Expected output:
I NativeDemo: === Native Demo Starting ===
I NativeDemo: Sensors initialized successfully
I NativeDemo: Entering main loop
I NativeDemo: Window initialized: 0x7a3c4d1000
I NativeDemo: Window size: 1080x2340, format: 1
I NativeDemo: Gained focus -- enabling sensors
I NativeDemo: Accelerometer enabled
I NativeDemo: Accelerometer: x=0.18 y=0.24 z=9.77
I NativeDemo: Touch DOWN at (540.0, 1170.0)
I NativeDemo: Touch UP at (540.0, 1170.0)
11.8.7 Extension Points¶
From this minimal example, a real application would add:
-
EGL/Vulkan rendering -- replace the
ANativeWindow_lock()path witheglCreateWindowSurface()orvkCreateAndroidSurfaceKHR()for GPU rendering. -
AAudio playback -- add
libaaudiotoshared_libsand useAAudioStreamBuilderfor low-latency audio output. -
Camera capture -- add
libcamera2ndkand useACameraManagerto open a camera and stream frames to the window. -
AIDL services -- add
libbinder_ndkand use AIDL-generated NDK stubs to communicate with system services. -
Neural Networks -- add
libneuralnetworksfor on-device ML inference using the NNAPI.
11.8.8 Debugging NDK Applications¶
Logcat Filtering¶
Use tag-based filtering to focus on your application's output:
# Filter by tag
adb logcat -s NativeDemo:V
# Filter by PID
adb logcat --pid=$(adb shell pidof com.example.nativedemo)
# Show native crashes
adb logcat -s DEBUG:V
Address Sanitizer (ASan)¶
The NDK supports ASan for detecting memory errors. Add to your build:
Or with CMake:
target_compile_options(native-demo PRIVATE -fsanitize=address)
target_link_options(native-demo PRIVATE -fsanitize=address)
ASan detects:
- Heap buffer overflows
- Stack buffer overflows
- Use after free
- Double free
- Memory leaks (with LeakSanitizer)
GDB / LLDB Debugging¶
For debugging native crashes:
# Start the app in debug mode
adb shell am start -D -n com.example.nativedemo/.NativeActivity
# Attach lldb-server
adb forward tcp:1234 tcp:1234
lldb
(lldb) platform select remote-android
(lldb) platform connect connect://localhost:1234
(lldb) process attach --name native-demo
Simpleperf Profiling¶
For performance analysis of NDK code:
# Record CPU profile
adb shell simpleperf record -p $(adb shell pidof com.example.nativedemo) \
--duration 5 -o /data/local/tmp/perf.data
# Pull and report
adb pull /data/local/tmp/perf.data
simpleperf report -i perf.data
11.8.9 Common Pitfalls¶
-
Missing
sdk_version-- if you forget to setsdk_version: "current", your module links against platform libraries instead of NDK stubs. This means it may use symbols that are not available on all devices. -
ABI differences across API levels -- structures like
ANativeWindowmay have different sizes at different API levels. Always use accessor functions rather than accessing struct members directly. -
Thread safety -- the native app glue uses a pipe to communicate between the UI thread and the app thread. Accessing
android_appfields from both threads without proper synchronization causes races. Always use the mutex: -
Forgetting to handle
APP_CMD_TERM_WINDOW-- if you hold a reference toANativeWindowpast this callback, subsequent operations on it will crash. Always null out your window pointer in theTERM_WINDOWhandler. -
Linking non-NDK libraries -- if your native code tries to
dlopen("libgui.so")or link against a non-NDK library, the dynamic linker will reject it at runtime on devices running Android 7.0+. The linker namespace isolation prevents access to libraries not on the NDK list.
Summary¶
This chapter has examined the Android NDK from the platform builder's perspective -- not as a download from developer.android.com, but as a set of build rules, header modules, stub generators, and ABI monitors embedded in the AOSP source tree.
The key architectural layers we have covered are:
| Layer | Stability scope | Key Soong module types |
|---|---|---|
| NDK | App developers | ndk_library, ndk_headers |
| LL-NDK | Apps + vendor code | llndk: block in cc_library |
| VNDK | Vendor code | vndk: block in cc_library, vndk_prebuilt_shared |
| NDK Translation | NativeBridge | ndk_translation_package |
The build system enforces stability through:
- Symbol maps (
.map.txt) that precisely define the exported API surface - Stub libraries that apps link against at build time
- ABI monitoring via STG dumps and
stgdiffthat catches incompatible changes - Header verification that ensures every NDK header is self-contained and valid C
- Linker namespace isolation that prevents runtime access to non-NDK libraries
The framework bindings for Camera, Media, and Binder demonstrate the standard
pattern for exposing complex C++ services through stable C APIs: opaque pointer
types, EXPORT-marked wrapper functions, fvisibility=hidden, and version
scripts.
Key source files for further exploration:
| File | Purpose |
|---|---|
build/soong/cc/ndk_library.go |
Stub library generation |
build/soong/cc/ndk_headers.go |
Header installation |
build/soong/cc/ndk_sysroot.go |
Sysroot assembly |
build/soong/cc/ndk_abi.go |
ABI monitoring |
build/soong/cc/llndk_library.go |
LL-NDK support |
build/soong/cc/vndk.go |
VNDK properties |
build/soong/cc/vndk_prebuilt.go |
VNDK snapshots |
build/soong/cc/ndk_translation_package.go |
Translation packaging |
frameworks/av/camera/ndk/ |
Camera NDK implementation |
frameworks/av/media/ndk/ |
Media NDK implementation |
frameworks/native/libs/binder/ndk/ |
Binder NDK implementation |
system/linkerconfig/ |
Linker namespace configuration |
prebuilts/ndk/current/sources/android/ |
App glue and CPU features |