This page explains how your app can use new OS functionality when running on new OS versions while preserving compatibility with older devices.
By default, references to NDK APIs in your application are strong references. Android's dynamic loader will eagerly to resolve them when your library is loaded. If the symbols are not found, the app will abort. This is contrary to how Java behaves, where an exception will not be thrown until the missing API is called.
For this reason, the NDK will prevent you from creating strong references to
APIs that are newer than your app's minSdkVersion
. This protects you from
accidentally shipping code that worked during your testing but will fail to load
(UnsatisfiedLinkError
will be thrown from System.loadLibrary()
) on older
devices. On the other hand, it is more difficult to write code that uses APIs
newer than your app's minSdkVersion
, because you must call the APIs using
dlopen()
and dlsym()
rather than a normal function call.
The alternative to using strong references is using weak references. A weak
reference that is not found when the library loaded results in the address of
that symbol being set to nullptr
rather than failing to load. They still
cannot be safely called, but as long as callsites are guarded to prevent calling
the API when it is not available, the rest of your code can be run, and you can
call the API normally without needing to use dlopen()
and dlsym()
.
Weak API references do not require additional support from the dynamic linker, so they can be used with any version of Android.
Enabling weak API references in your build
CMake
Pass -DANDROID_WEAK_API_DEFS=ON
when running CMake. If you're using CMake via
externalNativeBuild
, add the following to your build.gradle.kts
(or the
Groovy equivalent if you're still using build.gradle
):
android {
// Other config...
defaultConfig {
// Other config...
externalNativeBuild {
cmake {
arguments.add("-DANDROID_WEAK_API_DEFS=ON")
// Other config...
}
}
}
}
ndk-build
Add the following to your Application.mk
file:
APP_WEAK_API_DEFS := true
If you do not already have an Application.mk
file, create it in the same
directory as your Android.mk
file. Additional changes to your
build.gradle.kts
(or build.gradle
) file are not necessary for ndk-build.
Other build systems
If you're not using CMake or ndk-build, consult the documentation for your build system to see if there's a recommended way to enable this feature. If your build system doesn't support this option natively, you can enable the feature by passing the following flags when compiling:
-D__ANDROID_UNAVAILABLE_SYMBOLS_ARE_WEAK__ -Werror=unguarded-availability
The first configures the NDK headers to allow weak references. The second turns the warning for unsafe API calls into an error.
See the Build System Maintainers Guide for more information.
Guarded API calls
This feature does not magically make calls to new APIs safe. The only thing it does is defer a load-time error to a call-time error. The benefit is that you can guard that call at runtime and fall back gracefully, whether by using an alternative implementation or notifying the user that that feature of the app is not available on their device, or avoiding that code path entirely.
Clang can emit a warning (unguarded-availability
) when you make an unguarded
call to an API that isn't available for your app's minSdkVersion
. If you're
using ndk-build or our CMake toolchain file, that warning will be automatically
enabled and promoted to an error when enabling this feature.
Here is an example of some code that makes conditional use of an API without
this feature enabled, using dlopen()
and dlsym()
:
void LogImageDecoderResult(int result) {
void* lib = dlopen("libjnigraphics.so", RTLD_LOCAL);
CHECK_NE(lib, nullptr) << "Failed to open libjnigraphics.so: " << dlerror();
auto func = reinterpret_cast<decltype(&AImageDecoder_resultToString)>(
dlsym(lib, "AImageDecoder_resultToString")
);
if (func == nullptr) {
LOG(INFO) << "cannot stringify result: " << result;
} else {
LOG(INFO) << func(result);
}
}
It's a bit messy to read, there's some duplication of function names (and if
you're writing C, the signatures as well), it'll build successfully but always
take the fallback at runtime if you accidentally typo the function name passed
to dlsym
, and you have to use this pattern for every API.
With weak API references, the function above can be rewritten as:
void LogImageDecoderResult(int result) {
if (__builtin_available(android 31, *)) {
LOG(INFO) << AImageDecoder_resultToString(result);
} else {
LOG(INFO) << "cannot stringify result: " << result;
}
}
Under the hood, __builtin_available(android 31, *)
calls
android_get_device_api_level()
, caches the result, and compares it with 31
(which is the API level that introduced AImageDecoder_resultToString()
).
The simplest way to determine which value to use for __builtin_available
is to
attempt to build without the guard (or a guard of
__builtin_available(android 1, *)
) and do what the error message tells you.
For example, an unguarded call to AImageDecoder_createFromAAsset()
with
minSdkVersion 24
will produce:
error: 'AImageDecoder_createFromAAsset' is only available on Android 30 or newer [-Werror,-Wunguarded-availability]
In this case the call should be guarded by __builtin_available(android 30, *)
.
If there is no build error, either the API is always available for your
minSdkVersion
and no guard is needed, or your build is misconfigured and the
unguarded-availability
warning is disabled.
Alternatively, the NDK API reference will say something along the lines of "Introduced in API 30" for each API. If that text is not present, it means that the API is available for all supported API levels.
Avoiding repetition of API guards
If you're using this, you will probably have sections of code in your app that
are only usable on new enough devices. Rather than repeating the
__builtin_available()
check in each of your functions, you can annotate your
own code as requiring a certain API level. For example, the ImageDecoder APIs
themselves were added in API 30, so for functions that make heavy use of those
APIs you can do something like:
#define REQUIRES_API(x) __attribute__((__availability__(android,introduced=x)))
#define API_AT_LEAST(x) __builtin_available(android x, *)
void DecodeImageWithImageDecoder() REQUIRES_API(30) {
// Call any APIs that were introduced in API 30 or newer without guards.
}
void DecodeImageFallback() {
// Pay the overhead to call the Java APIs via JNI, or use third-party image
// decoding libraries.
}
void DecodeImage() {
if (API_AT_LEAST(30)) {
DecodeImageWithImageDecoder();
} else {
DecodeImageFallback();
}
}
Quirks of API guards
Clang is very particular about how __builtin_available
is used. Only a literal
(though possibly macro-replaced) if (__builtin_available(...))
works. Even
trivial operations like if (!__builtin_available(...))
will not work (Clang
will emit the unsupported-availability-guard
warning, as well as
unguarded-availability
). This may improve in a future version of Clang. See
LLVM Issue 33161 for more information.
Checks for unguarded-availability
only apply to the function scope where they
are used. Clang will emit the warning even if the function with the API call is
only ever called from within a guarded scope. To avoid repetition of guards in
your own code, see Avoiding repetition of API guards.
Why isn't this the default?
Unless used correctly, the difference between strong API references and weak API references is that the former will fail quickly and obviously, whereas the latter will not fail until the user takes an action that causes the missing API to be called. When this happens, the error message will not be a clear compile- time "AFoo_bar() is not available" error, it will be a segfault. With strong references, the error message is much clearer, and failing-fast is a safer default.
Because this is a new feature, very little existing code is written to handle this behavior safely. Third-party code that was not written with Android in mind will likely always have this problem, so there are currently no plans for the default behavior to ever change.
We do recommend that you use this, but as it will make problems more difficult to detect and debug, you should accept those risks knowingly rather than the behavior changing without your knowledge.
Caveats
This feature works for most APIs, but there are a few cases where it does not work.
The least likely to be problematic is newer libc APIs. Unlike the rest of the
Android APIs, those are guarded with #if __ANDROID_API__ >= X
in the headers
and not just __INTRODUCED_IN(X)
, which prevents even the weak declaration from
being seen. Since the oldest API level modern NDKs support is r21, the most
commonly needed libc APIs are already available. New libc APIs are added each
release (see status.md), but the newer they are, the more likely they are to
be an edge case that few developers will need. That said, if you are one of
those developers, for now you'll need to continue using dlsym()
to call those
APIs if your minSdkVersion
is older than the API. This is a solvable problem,
but doing so carries the risk of breaking source compatibility for all apps (any
code that contains polyfills of libc APIs will fail to compile due to the
mismatched availability
attributes on the libc and local declarations), so
we're not sure if or when we'll fix it.
The case that more developers are likely to encounter is when the library that
contains the new API is newer than your minSdkVersion
. This feature only
enables weak symbol references; there is no such thing as a weak library
reference. For example, if your minSdkVersion
is 24, you can link
libvulkan.so
and make a guarded call to vkBindBufferMemory2
, because
libvulkan.so
is available on devices starting with API 24. On the other hand,
if your minSdkVersion
were 23, you must fall back to dlopen
and dlsym
because the library will not exist on the device on devices that only support
API 23. We don't know of a good solution for fixing this case, but in the long
term it will resolve itself because we (whenever possible) no longer allow new
APIs to create new libraries.
For library authors
If you are developing a library to be used in Android applications, you should
avoid using this feature in your public headers. It can be safely used in
out-of-line code, but if you rely on __builtin_available
in any code in your
headers, such as inline functions or template definitions, you force all your
consumers to enable this feature. For the same reasons we do not enable this
feature by default in the NDK, you should avoid making that choice on the behalf
of your consumers.
If you do require this behavior in your public headers, make sure to document that so your users both know that they will need to enable the feature and are aware of the risks of doing so.