Android NDK API Guidelines

Warning: NDK APIs do not support feature flagging.

API longevity

Many of the rules in this doc exist to protect API compatibility across releases. Each API surface has its own compatibility guarantees. go/android-api-types is the authoritative source, but briefly:

| API surface | Consumers | Map file annotation | Compatibility | : : : : guarantee : | ----------- | ------------------ | ------------------- | ------------- | | NDK | Apps | introduced-in, or | Forever | : : : no other annotation : : | LL-NDK | Vendors/OEMs | llndk | Forever | | APEX | The system or | apex | Forever | : : other APEX modules : : : | SystemAPI | APEX modules | systemapi | 4 years | | Platform | System image | platform | None |

During the time that compatibility is guaranteed for an API, the ABI and behavior of that API must not be changed. This is a difficult guaranatee to uphold, and that's why these rules are in place.

API Rules

One of the difficulties in concrete rules is applying them to a platform that was developed without strict guidelines from the beginning, so some of the existing APIs may not adhere. In some cases, the right choice might be to go with what is consistent with APIs in the same general area of the code, rather than in the ideal rules laid out herein.

The rules are a work in progress and will be added to in the future as other patterns emerge from future API reviews.

Compatibility

More information on these build requirements can be found at https://android.googlesource.com/platform/ndk/+/master/docs/PlatformApis.md

Headers Must be C Compatible

Even if it is not expected that C is ever used directly, C is the common denominator of (nearly) all FFI systems & tools. It also has a simpler stable ABI story than C++, making it required for NDK’s ABI stability requirements.

Jetpack C++ APIs are exempt from this rule. See go/jetpack-cpp-guidelines (draft) for additional guidelines for Jetpack.

  1. The header contents (after the #include block) should begin with __BEGIN_DECLS and end with __END_DECLS from sys/cdefs.h

  2. Non-typedef’ed structs must be used as struct type not just type

    Prefer typedef’ing structs, see the Naming Conventions section for more details.

  3. Enums must specify their backing type

    For example:

    typedef enum AFoo : int16_t { AFOO_ONE, AFOO_TWO, AFOO_THREE } AFoo;
    AFoo transformAFoo(AFoo param);
    

    This C++ feature is provided for C as a Clang extension, so this should be be used even for NDK headers that must otherwise be C.

    A rare exception to this rule will be made for interfaces defined by third- parties (e.g. Vulkan or ICU) if upstream is not willing to depend on the extension.

  4. Enums used by name must be used as enum type or be typedef'd.

    Prefer typedefs.

  5. Use stdbool.h. C has bool, but it must be included. Do not use integer types for boolean values.

  6. Empty argument lists must be spelled fn(void) rather than fn(). In C, fn(void) declares a function that accepts 0 arguments, whereas fn() declares a function that accepts an unknown number of arguments.

APIs must be tagged with the API Level

Mark methods with __INTRODUCED_IN(<api_level>);

Example:

binder_status_t AIBinder_getExtension(AIBinder* binder, AIBinder** outExt) __INTRODUCED_IN(__ANDROID_API_R__);

DO NOT wrap the new methods with #if __ANDROID_API__ >= <api_level> and #endif.

#if __ANDROID_API__ >= __ANDROID_API_R__

binder_status_t AIBinder_getExtension(AIBinder* binder, AIBinder** outExt) __INTRODUCED_IN(__ANDROID_API_R__);

#endif

The API level is already annotated by the __INTRODUCED_IN macro. With the annotation, the compiler triggers an error when the method shouldn‘t be used (previously it didn’t and that's why guards used to be required). Now, the #if guard is not only redundant, but also makes it impossible for the clients to use the methods even when it is safe to do so (i.e. the existence of the API on the platform is guaranteed). For example, a client should be able to use APIs whose api_level is higher than its target sdk version, using the __builtin_available() macro as shown below.

if (__builtin_available(android __ANDROID_API_R__,*)) {
  binder_status_t ok = AIBinder_getExtension(binder, &ext);
}

Types & enums should be documented with the API level added

  1. Types & enum declaration MUST NOT be guarded by #if __ANDROID_API__ >= <api_level>. This makes opportunistic usage via dlsym or __builtin_available() harder to do.

  2. Their documentation should include what API level they were added in with a comment saying “Introduced in API <number>.” Note that the documentation is not macro-replaced so should use the actual number instead of the codename since that's what users need to know for their build.gradle files.

Export map must be tagged with the API level

Note: Jetpack libraries should use map files to control symbol visibility but do not need any annotations; the guidelines in this section do not apply.

  1. Libraries must have a <name>.map.txt with the symbol being exported

    • The format of these files is defined by go/ndk-api-stubs.
  2. The map file must be used by an appropriate Soong module

    • NDK libraries require an ndk_library module.
    • APEX libraries require the stubs attribute on a cc_library module.
  3. NDK symbols must be marked with # introduced=<api_level>

    • Can be either on each individual symbol or on a version block. Applying tags to the same line as the version definition behaves the same as if each symbol defined in the version specified the same tag.

    • Note that all symbols not otherwise annotated are implicitly introduced in the first_version specified in the ndk_library. If not specified, the API is implicitly available in all supported OS releases (currently back to API 19).

      For example, libaaudio was introduced in API 24, so any symbols that do not have another annotation will default to introduced=24.

  4. Use codenames for preview API levels in map files

    foo; # introduced=Tiramisu
    
    foo; # introduced=33
    

    To find the correct spelling for a codename, see apiLevelsMap in Soong for released codenames (be sure to check the current version of the source for your branch) or Platform_version_active_codenames in out/soong/soong.variables for in-progress codenames.

    After an API level is finalized it is okay to replace codenames with API numbers, but not necessary.

  5. APEX symbols must be marked with apex

    • APEX symbols are defined by APEX modules and exposed to other APEX modules and/or the platform.
    • This is not currently enforced by the tooling, but is required. Failure to follow this makes it possible for approved APEX APIs to be promoted to NDK APIs without notifying the API council.
  6. System API symbols must be marked with systemapi

    • System API symbols are defined by the platform but are usable by mainline and APEX.
  7. LL-NDK symbols must be marked with llndk

  8. Platform-only symbols must be marked with platform-only

    • Platform-only symbols are not exposed to any stable interface and not subject to any other guidelines. These symbols are only usable by other platform libraries; they are not usable by apps, vendors, or APEX.
    • Any version section with the suffix _PRIVATE or _PLATFORM is impliclty platform-only.
    • Map files that are exclusively for the platform (contain no NDK, APEX, or LLNDK APIs; only used for symbol versioning or for visibility control) should not end with .map.txt. The linker will accept a file with any name, and the .map.txt suffix requires NDK API council review for all changes.

NDK APIs must have CTS tests

  1. All NDK APIs are required to be covered by at least one CTS test.

  2. The CTS test must be built against the NDK proper

    1. No includes of any system headers. This includes anything under //frameworks, //system, mainline modules, etc. With rare exceptions, CTS tests for NDK APIs should not include (include_dirs, shared_libs, static_libs, etc) anything outside the NDK or gtest.

      A minimal set of dependencies for CTS tests improves CTS's ability to verify that NDK headers are usable. If a CTS test has //not/included/in/ndk on the include path, the test can pass even if the NDK headers require headers that the NDK does not ship.

    2. Must set an sdk_version in the Android.bp (LOCAL_SDK_VERSION for Android.mk) for the test

  3. Tests must exercise the C APIs themselves, in addition to the C++ wrapper if one is present. The C API is the interface to the system and needs to work as documented, and the C++ wrapper could alter behavior. All exposed APIs must be tested directly.

  4. Add query APIs if needed to test flag-setting APIs. This is not a strict requirement, but is preferred without a good reason not to.

Even if the only test that can be written is a test that the API can be called, that is still a valuable test because it proves that the API was exposed to the NDK. Whenever possible though, the test should verify some observable behavior, even if that means adding more APIs to query state.

APEX/mainline/systemapi must be covered by some other test suite

  1. Non-NDK APIs cannot be tested by CTS but still require tests.

  2. Tests must exercise the C APIs themselves, in addition to the C++ wrapper if one is present. The C API is the interface to the system and needs to work as documented, and the C++ wrapper could alter behavior. All exposed APIs must be tested directly.

Prefer new APIs to changing API behavior

  1. Prefer new APIs to substantially changing behavior or meaning. New APIs are self-documenting as when they were added to the platform, whereas behavior changes are only surfaced in documentation but not otherwise guarded by lint or other automated checks. Behavior changes can be appropriate when backwards compatibility or overall API meaning is preserved.

  2. When a behavior change is required, consider enabling the new behavior based on targetSdkVersion. This helps developers maintain bug compatibility, but note that new APIs are still preferred because the app can only have a single targetSdkVersion and the developer's dependencies could still rely on the old behavior.

Documentation

NDK documentation is published from the contents of the NDK whenever a release is shipped. See go/edit-ndk-docs for more information on editing and previewing the NDK documentation. API authors do not need to manually update DAC.

APIs must be well documented

  1. If error return codes are being used, all possible errors must be listed as well, akin to how the man pages do.

    If your API returns an errno that is produced by a libc call in the implementation, you may link the relevant man7.org page rather than listing every possible errno explicitly, but do be specific about the circumstances when it is not clear. For example, if an API that doesn't obviously require any permissions can return EPERM, you must explain the conditions that will return EPERM.

  2. Thread-safe and thread-hostile objects/methods must be called out explicitly as such.

  3. Object lifetime must be documented for anything that’s not a standard new/free pair.

  4. If ref counting is being used, methods that acquire/release a reference must be documented as such.

  5. Character encoding must be documented for any string handling. UTF-8 is strongly preferred and any other choice should come with significant justification.

    If there is no encoding because the data is a string of arbitrary bytes, use uint8_t* instead of char*. char* should only be used for text.

  6. For NDK APIs, documentation must be in Doxygen syntax. To appear on DAC, all documentation (including @file) must be contained by a @defgroup or @addtogroup block.

    Doxygen is not required for APEX, but API documentation in some form is. Prefer Doxygen when adding new documentation. Note to API reviewers: non-Doxygen API documentation in APEX APIs is non-blocking.

  7. Doxygen comments for NDK APIs must include "Introduced in API <number>.” Note that the documentation is not macro-replaced so should use the actual number instead of the codename since that's what users need to know for their build.gradle files. Non-NDK APIs should consider doing this as well.

ABI stability guidelines

Prefer opaque structs

Opaque structs allow for size changes more naturally and are generally less fragile.

An exception to this is if the type is inherently fixed. For example ARect is not an opaque struct as a rectangle is inherently 4 fields: left, top, right, bottom. Defining an HTTP header as a struct { const char* key; const char* value; } would also be appropriate, as HTTP headers are inherently fixed.

If a non-opaque struct is needed and the previous exception does not apply, explicitly versioned structs may be used. For example:

#include <stdalign.h>
#include <stddef.h>
#include <stdint.h>

// The data contained by the first version of the struct.
typedef struct AFooV1 {
  void* data;
} AFooV1;

// Additional data added in a later release.
typedef struct AFooV2 {
  void* other_data;
} AFooV2;

// The struct used by the APIs. Must be passed or returned as a pointer so the
// size of the argument/return type does not depend on the OS version of the
// device. The documentation must warn the user that the size of the struct can
// change, and that only fields up to `version` will be initialized. New
// versions of the OS must continue initializing old fields to preserve app
// compat.
typedef struct AFoo {
  alignas(alignof(max_align_t)) uint32_t version;
  AFooV1 v1;
  AFooV2 v2;
  // v3, v4, etc.
} AFoo;

Size versioned structs (structs whose first member is sizeof(the_struct)) are not allowed.

  1. Callers only ever see the sizeof() of the struct they were compiled against. This makes it harder to alter behavior for previous versions, as they cannot sizeof() a different version of the header, and it introduces hidden behavior changes. Changing the compiled NDK version will alter the versioned behavior of these structs without warning.

  2. More difficult to document. “This field is available when the structure size is 12” does not work because the size of the struct will often depend on the architecture (pointer size, alignment, and padding).

  3. More error prone. It's harder to spot an accidental read of a field that is not initialized for a given struct version.

malloc/free must come from the same build artifact

Different build artifacts may, and often do, have different implementations of malloc/free.

For example: Chrome may opt to use a different allocator for all of Chrome’s code, however the NDK libraries it uses will be using the platform’s default allocator. These may not match, and cannot free memory allocated by the other.

  1. If a library allocates something, such as an opaque struct, it must also provide a free function.

  2. If a library takes ownership of an allocation, it must also take the free function.

Constants are forever

If a header defines an enum or constant, that value is forever.

  1. For defined steppings in a range (such as priority levels or trim memory levels), leave gaps in the numberings for future refinement.

  2. Enums do not need to specify explicit values for all members, but may choose to do so. The values assigned to each member must remain consistent across all releases, so it may be easier to assign values explicitly rather than preserving order manually.

  3. For configuration data, such as default timeouts, use a getter method or an extern variable instead.

Prefer fixed-size types

Primitive types like long can have varying sizes. This can lead to issues on 32-bit vs. 64-bit builds.

  1. In general, prefer the fixed-size types int32_t, int64_t, etc...

  2. For counts of things, use size_t.

  3. If libc has an existing preference, use that instead (eg, use pid_t if you’re taking a process ID)

  4. Use int32_t or int64_t for enum params/returns that do not have an explicit backing type.

    • The backing size of an enum is up to the compiler. As such, even if a parameter or return value represents an enum use instead a fixed-type like int32_t.
    • New enums should define an explicit backing type for the enum to avoid this requirement.
    • Old enums can adopt the old integer argument type as their backing type. New functions are then encouraged to use the enum directly. Existing APIs are not encouraged to update their signatures, but may do so.
  5. Avoid off_t.

    • The size of an off_t can vary based on the definition of _FILE_OFFSET_BITS. APIs must use off64_t instead of off_t.

API Design Guidelines

Platform Naming conventions

  1. Prefer AClassName for the type (“A” being the prefix for “Android”). The provides a C-compatible pseudo-namespace for NDK APIs.

  2. Typedef structs by default, for example:

    struct AIBinder;
    typedef struct AIBinder AIBinder;
    
  3. Class methods should follow AClassName_methodName naming (lower case beginning the method part of the name regardless of the naming style of the backing implementation).

  4. Callbacks should follow a AClassName_CallbackType naming convention.

  5. “Nested” classes should also follow a AClassName_SubType naming convention

  6. Constants should follow ACLASSNAME_CONSTANT_NAME naming. Even for enum scoped constants the ACLASSNAME_ prefix is needed because C introduces all enum constants to the global scope.

  7. Treat acroynms as words in class and method names, preferring AWidget_getHttpCookie() to AWidget_getHTTPCookie().

  8. Macros (which should be used sparingly) should follow the same rules as constants: ACLASSNAME_MACRO_NAME. If the macros are not associated with a specific “class” of the API, use ANAMESPACE_MACRO_NAME instead, e.g. ACAMERA_MACRO_NAME.

    Exceptions to this rule may be made for cases where the macro is expected to be used very frequently. In that case, there must be a way for the user to opt-out of the non-namespaced macros. This can be done either with a configuration macro (e.g. ANAMESPACE_ENABLE_HELPER_MACROS), or by keeping the macros in their own header (e.g. android/namespace/macros.h) which is not included from any of the other API headers.

  9. Free functions (APIs that are not “methods” of a type in the API) should follow ANamespaceName_functionName naming.

Jetpack C API Naming conventions

Jetpack should prefer C++ over C as much as possible, however if a C API is being made anyway the platform naming conventions should be followed with the following exceptions:

  1. Avoid A prefix, use AX instead

    • The A prefix is exclusively reserved for platform types.
    • This provides at-a-glance clarity for which types may require additional SDK version number checks
    • This avoids potential future type collisions
    • This allows for cleaner wrapping of platform types for compat types. For example, a hypothetical Jetpack wrapper for AChoreographer to handle the type migration that occured between frameCallback and frameCallback64 could then simply use the AXChoreographer name instead of needing to be AChoreographerCompat. Similarly, a backport of ASurfaceTexture that uses JNI upcalls internally to provide the behavior prior to ASurfaceTexture's NDK addition could then just use AXSurfaceTexture name.

APEX naming conventions

  1. Libraries must be named matching their fully-qualified package ID. This is needed to avoid naming collisions. For example, libnativehelper.so should have been named (the library predates the rule) libcom.android.art.nativehelper.so.

  2. All other naming conventions follow the platform rules.

JNI

JNI interop methods should exclusively be in the NDK

As in, always do AObject_fromJava(JNIEnv, jobject) in the NDK rather than having a long Object#getNativePointer() in the SDK.

Similarly do instead jobject AObject_toJava(JNIEnv, AObject*) in the NDK rather than new Object(long nativePtr); in the SDK.

If the native and Java types have the same name, the suffix should be just _fromJava or toJava. If the type names differ, the Java type name should also be in the suffix. e.g. _fromJavaSurface.

Lifetime requirements of Java objects must be explicit in the documentation. If the API does or does not acquire a reference to the Java object, say so.

It is recommended to have JNI interop APIs.

Java objects and native objects should use separate respective lifecycles

To the extent possible, the Java objects and the native objects should have lifecycles native to their respective environments & independent of the other environment.

That is, if a native handle is created from a Java object then the Java object’s lifecycle should not be relevant to the native handle. Similarly, if a Java object is created from a native object the native object should not need to out-live the Java one.

Typically this means both the NDK & Java types should sit on a ref-count system to handle this if the underlying instance is shared.

If the interop just does a copy of the data (such as for a trivial type), then nothing special needs to happen.

Exceptions can be made if it’s impractical for the underlying type to be referenced counted and it’s already scoped to a constrained lifecycle. For example, AParcel_fromJavaParcel adopts the lifecycle of the jobject and as such does not have a corresponding free. This is OK as the lifecycle of a Parcel is already scoped to the duration of a method call in general anyway, so a normal JNI LocalRef will have suitable lifetime for typical usage.

JNI interop APIs should be in their own header with a trailing _jni suffix.

JNI interop APIs should be in their own header with a trailing _jni suffix.

Example: asset_manager.h and asset_manager_jni.h

This helps apps to keep a clean JNI layer in their own code.

Inlines

Inline functions may be used for simple wrappers

Inline functions may be included in headers when they are a trivial wrapper presenting a de-facto overload of another API.

For example, if an API exists that takes a pointer to a memory mapped file, an API that takes an FD that maps the file and calls the other API may be an inline.

Avoid using inlines wherever the implementation is complicated or would benefit from being written in C++ (Jetpack libraries may use C++ in headers).

Inline code cannot use __builtin_available. Using __builtin_available instead of dlopen/dlsym only works if __ANDROID_UNAVAILABLE_SYMBOLS_ARE_WEAK__ is set. There are no plans to ever remove the current behavior, nor to make __ANDROID_UNAVAILABLE_SYMBOLS_ARE_WEAK__ the default, as there are tradeoffs involved that must be made by the app developer. As such, inline code must always work in both configurations, and the only way to do that is with dlopen/dlsym.

Error Handling

Methods that cannot fail {.numbered}

If a method cannot fail, the return type should simply be the output or void if there is no output.

allocation/accessor methods {.numbered}

For allocation/accessor methods where the only meaningful failure is out of memory or similar, return the pointer and use NULL for errors.

Example: only failure is ENOMEM: AMediaDataSource* AMediaDataSource_new();

Example: only failure is not set: ALooper* ALooper_forThread();

Methods with non-trivial error possibilities {.numbered}

  1. For methods with a non-trivial error possibility or multiple unique error types, use an error return value and use an output parameter to return any results

  2. For APIs where the only error possibility is the result of a trivial check, such as a basic getter method where the only failure is a nullptr, do not introduce an error handling path but instead abort on bad parameters.

size_t AList_getSize(const AList*);
status_t AList_getSize(const AList*, const size_t* outSize);

Include a quality abort message {.numbered}

For example, in system_fonts.cpp:

bool AFont_isItalic(const AFont* font) {
    LOG_ALWAYS_FATAL_IF(font == nullptr, "nullptr passed as font argument");
    return font->mItalic;
}

Error return types should be stringifiable

  1. Error return types should have a toString method to stringify them.

    • Due to the C ABI, this name must be globally unique. As such, it should follow the naming scheme of the local type. For example if the error return is for the AFoo type, then the recommended naming for stringification of the error is AFoo_resultToString.
  2. The returned strings should be constants. This is to prevent needing a free method for the error string.

  3. Invalid inputs to the stringify method should return nullptr. This allows new error codes to be added that do not cause a crash on old systems if used explicitly.

Callbacks

Callbacks should be a bare function pointer.

Callbacks should take a void* for caller-provided context.

Use “callback(s)” instead of “listener(s)”

For single callback APIs, use setCallback terminology

If the API only allows for a single callback to be set, use “setCallback” terminology

In such a case, the void* must be passed to both the setCallback method as well as passed to the callback on invoke.

To clear the callback, allow setCallback to take NULL to clear.

For multiple callback APIs, use register/unregister terminology

If the API allows for multiple callbacks to be set, use register/unregister terminology

In such a case, the void* is needed on registerCallback, invoke, and unregisterCallback.

Register & unregister must use the pair of function pointer + void as the unique key*. As in, it must support registering the same function pointer with different void* userData.

Nullability

Document and use _Nonnull or _Nullable

Document parameters & return values with _Nonnull or _Nullable as appropriate.

Note: These cannot practically be added to any existing headers because Clang will diagnose all APIs in the same header as soon as the first annotation is introduced. These annotations should be used for any new headers. If possible, revisit existing headers to add annotations for existing APIs so new APIs can be made safer in the future.

These are defined in clang, see https://clang.llvm.org/docs/AttributeReference.html#nullability-attributes

Use const

Use const when appropriate

For example if a method is a simple getter for an opaque type, the struct pointer argument should be marked const.

Example:

size_t AList_getSize(const AList*);
size_t AList_getSize(AList*);

AFoo_create vs. AFoo_new

Prefer _create over _new

Prefer _create over _new as it works better with _createFrom specializations

For destruction use _destroy

Reference counting

Prefer _acquire/_release over _inc/_dec for ref count naming