Android NDK/Native API Guidelines

Warning: Native APIs do not support feature flagging.

Get an API review

Any change that alters the stable native API surface of Android needs API review. To get an API review, assign the CL to android-ndk-api-council@google.com and go/gwsq will automatically assign a reviewer.

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.

For the duration of the compatibility window, the implementation of the API must remain, and must avoid significant behavior changes. Stable APIs can almost never be truly removed (if your API is less broken than invoke_cve(), it probably can't be removed). To discourage developers from using an API you can annotate it with __DEPRECATED_IN(api_level, message), but that should never be done without an alternative for callers to migrate to, and doing so does not allow you to remove or significantly alter the implementation.

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

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 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>);

Note: Don't do this for types though. See the rule for types and enums.

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 __INTRODUCED_IN annotation causes Clang to trigger an error when the method is unavailable. An additional #if guard is not only redundant, but also makes it difficult for the clients to use the APIs conditionally as described in https://developer.android.com/ndk/guides/using-newer-apis.

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.

    They should not be annotated with __INTRODUCED_IN() either.

  2. Documentation for individual enum values should include the first API level that they were added, 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 21).

      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.

APIs must have tests

API surfaceTest suite
NDKCTS
LL-NDKVTS
APEXMTS
SystemAPICTS (APIs currently only callable via dlsym(): b/376759605)
Platformcc_test with TEST_MAPPING. Not required, but you wouldn't
: : really skip writing tests, would you? :
  1. All APIs must be covered by the appropriate test suite. See the table above.

    1. Make a best-effort attempt to cover the API behavior thoroughly.

    2. Add query APIs if needed to test property-setting APIs with no other observable behavior (and consider them either way if they may be useful for developers). This is not a strict requirement, but is preferred without a good reason not to.

      For example:

      typedef struct AFoo AFoo;
      
      void AFoo_setBar(AFoo* _Nonnull foo, bool bar);
      bool AFoo_getBar(const AFoo* _Nonnull foo);
      void AFoo_act(const AFoo* _Nonnull foo);
      

      Whenever possible, a test should be written for AFoo_setBar() which asserts a behavior change in AFoo_act(). When that is not possible (for example, APIs which set hints for performance infrastructure often aren't guaranteed to do anything other than set the hint flag), AFoo_getBar() can be used to test AFoo_setBar() instead.

    3. Exceptions to testing requirements may be granted in the case of infrastructure gaps, or on a case-by-case basis for test-hostile APIs. In such cases there is still a requirement for a test which calls the API even if no behavior can be asserted.

  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.

Tip: Negative tests for APIs which abort on failure can still be tested using death tests.

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. It is easy to tell when new APIs are 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.

  8. Potential pending exceptions should be limited to runtime issues such as OutOfMemory or ClassCastException unless clearly documented what the other potential exceptions are and why they may occur (see also: go/android-api-guidelines#throws).

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 legacy enum params/returns that do not have an explicit backing type.

    • 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 may update their signatures, but are not required to do so (doing so is especially helpful for Rust callers).
    • When an enum has no backing type and one cannot be added, the backing type of an enum is up to the compiler. In those cases, even if a parameter or return value represents an enum, use instead a fixed-type like int32_t.
  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:

    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_callbackNameCallback naming convention.

    Include the “Callback” suffix.

  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 native APIs

As in, always add AObject_fromJava(JNIEnv, jobject) to the native API surface rather than having a long Object#getNativePointer() in the SDK.

Similarly add jobject AObject_toJava(JNIEnv, AObject*) to the native API surface 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 native & 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.

JNI interop APIs should not clear pending exceptions

Any exceptions thrown should be left pending for the caller to handle. Most potential pending exceptions types must be documented. See go/android-ndk-api-guidelines#well-documented.

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(). Inlines which call other APIs should be marked with __INTRODUCED_IN() matching the APIs they call. This looks odd because an inline function is not actually “introduced” in any version of the OS, but doing so allows you to defer the choice between __builtin_available() and build-time guards to the caller. See Avoiding repetition of API guards for an example and further explanation.

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 there's only a single failure mode, return the pointer and use NULL for errors.

Examples:

// Returns NULL when OOM.
AMediaDataSource* _Nullable AMediaDataSource_new();

// Returns NULL when the calling thread has no assigned Looper.
ALooper* _Nullable ALooper_forThread();

In both of these examples a _Nonnull return type and an implementation that aborts is also acceptable. Whichever you choose, document it.

Methods with non-trivial error possibilities {.numbered}

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

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

Abort for trivial programmer errors {.numbered}

For APIs where the only error possibility is the result of a trivial check, such as passing nullptr to a _Nonnull argument, the implementation should abort rather than return an error.

Tip: Such APIs can still be tested using death tests.

A common case where aborts should not be used is for invalid flags or enum values passed to an API that may have new valid inputs added in future releases. Developers should be able to pass new values to the API on an old device and receive a “not supported” error rather than needing to check for availability before making the call to avoid a crash.

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.

C++ exceptions may not cross the API boundary {.numbered}

Stable API boundaries are C only (the C++ ABI is not stable), so C++ exceptions are not allowed to cross the API boundary. You must use an alternative method of error handling for your API (see above).

If your implementation may raise exceptions, they must be caught and handled (probably by translating to error codes) at the API boundary.

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* context argument must be passed to both the setCallback method as well as passed to the callback when invoked.

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* context argument is needed on registerCallback and unregisterCallback and must be passed to the callback when invoked.

Register & unregister must use the pair of function pointer + void* as the unique key. In other words, multiple registrations of the same function pointer should be allowed with different void* user data.

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