Utiliser des API plus récentes

Cette page explique comment votre application peut utiliser les nouvelles fonctionnalités de l'OS lorsqu'elle s'exécute sur de nouvelles versions d'OS, tout en préservant la compatibilité avec les appareils plus anciens.

Par défaut, les références aux API NDK dans votre application sont des références fortes. Le chargeur dynamique d'Android s'empressera de les résoudre lorsque votre bibliothèque sera chargée. Si les symboles ne sont pas trouvés, l'application s'arrête. Cela est contraire au comportement de Java, où une exception n'est pas générée tant que l'API manquante n'est pas appelée.

C'est pourquoi le NDK vous empêche de créer des références fortes à des API plus récentes que le minSdkVersion de votre application. Cela vous évite de publier accidentellement du code qui a fonctionné lors de vos tests, mais qui ne se charge pas (UnsatisfiedLinkError est généré à partir de System.loadLibrary()) sur les appareils plus anciens. En revanche, il est plus difficile d'écrire du code qui utilise des API plus récentes que la minSdkVersion de votre application, car vous devez appeler les API à l'aide de dlopen() et dlsym() plutôt qu'un appel de fonction normal.

L'alternative aux références fortes est l'utilisation de références faibles. Une référence faible qui n'est pas trouvée lors du chargement de la bibliothèque entraîne la définition de l'adresse de ce symbole sur nullptr au lieu de ne pas réussir à charger. Ils ne peuvent toujours pas être appelés de manière sécurisée, mais tant que les sites d'appel sont protégés pour empêcher l'appel de l'API lorsqu'elle n'est pas disponible, le reste de votre code peut être exécuté et vous pouvez appeler l'API normalement sans avoir à utiliser dlopen() et dlsym().

Les références d'API faibles ne nécessitent pas de prise en charge supplémentaire de la part du liant dynamique. Elles peuvent donc être utilisées avec n'importe quelle version d'Android.

Activer les références d'API faibles dans votre build

CMake

Transmettez -DANDROID_WEAK_API_DEFS=ON lors de l'exécution de CMake. Si vous utilisez CMake via externalNativeBuild, ajoutez les éléments suivants à votre build.gradle.kts (ou l'équivalent Groovy si vous utilisez toujours build.gradle):

android {
    // Other config...

    defaultConfig {
        // Other config...

        externalNativeBuild {
            cmake {
                arguments.add("-DANDROID_WEAK_API_DEFS=ON")
                // Other config...
            }
        }
    }
}

ndk-build

Ajoutez le code ci-dessous à votre fichier Application.mk :

APP_WEAK_API_DEFS := true

Si vous ne disposez pas encore d'un fichier Application.mk, créez-en un dans le même répertoire que votre fichier Android.mk. Aucune modification supplémentaire de votre fichier build.gradle.kts (ou build.gradle) n'est nécessaire pour ndk-build.

Autres systèmes de compilation

Si vous n'utilisez pas CMake ni ndk-build, consultez la documentation de votre système de compilation pour savoir s'il existe une méthode recommandée pour activer cette fonctionnalité. Si votre système de compilation n'est pas compatible avec cette option en mode natif, vous pouvez activer la fonctionnalité en transmettant les indicateurs suivants lors de la compilation:

-D__ANDROID_UNAVAILABLE_SYMBOLS_ARE_WEAK__ -Werror=unguarded-availability

Le premier configure les en-têtes du NDK pour autoriser les références faibles. La seconde transforme l'avertissement pour les appels d'API non sécurisés en erreur.

Pour en savoir plus, consultez le guide des responsables de compilation du système.

Appels d'API protégés

Cette fonctionnalité ne rend pas automatiquement les appels aux nouvelles API sécurisés. La seule chose qu'il fait est de différer une erreur de temps de chargement à une erreur de temps d'appel. L'avantage est que vous pouvez protéger cet appel au moment de l'exécution et effectuer une dégradation élégante, que ce soit en utilisant une implémentation alternative, en informant l'utilisateur que cette fonctionnalité de l'application n'est pas disponible sur son appareil ou en évitant complètement ce chemin de code.

Clang peut émettre un avertissement (unguarded-availability) lorsque vous effectuez un appel non protégé à une API qui n'est pas disponible pour le minSdkVersion de votre application. Si vous utilisez ndk-build ou notre fichier de chaîne d'outils CMake, cet avertissement sera automatiquement activé et transformé en erreur lorsque vous activerez cette fonctionnalité.

Voici un exemple de code qui utilise de manière conditionnelle une API sans que cette fonctionnalité ne soit activée, à l'aide de dlopen() et 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);
    }
}

Il est un peu désordonné à lire, il y a une duplication de noms de fonction (et si vous écrivez du code C, les signatures également), la compilation aboutit, mais le remplacement est toujours effectué au moment de l'exécution si vous avez mal orthographié le nom de la fonction transmise à dlsym, et vous devez utiliser ce modèle pour chaque API.

Avec des références d'API faibles, la fonction ci-dessus peut être réécrite comme suit:

void LogImageDecoderResult(int result) {
    if (__builtin_available(android 31, *)) {
        LOG(INFO) << AImageDecoder_resultToString(result);
    } else {
        LOG(INFO) << "cannot stringify result: " << result;
    }
}

Sous le capot, __builtin_available(android 31, *) appelle android_get_device_api_level(), met en cache le résultat et le compare à 31 (qui est le niveau d'API qui a introduit AImageDecoder_resultToString()).

Le moyen le plus simple de déterminer la valeur à utiliser pour __builtin_available consiste à essayer de compiler sans le garde (ou un garde de __builtin_available(android 1, *)) et à suivre les instructions du message d'erreur. Par exemple, un appel non protégé à AImageDecoder_createFromAAsset() avec minSdkVersion 24 génère:

error: 'AImageDecoder_createFromAAsset' is only available on Android 30 or newer [-Werror,-Wunguarded-availability]

Dans ce cas, l'appel doit être protégé par __builtin_available(android 30, *). Si aucune erreur de compilation ne se produit, l'API est toujours disponible pour votre minSdkVersion et aucune protection n'est nécessaire, ou votre compilation est mal configurée et l'avertissement unguarded-availability est désactivé.

La documentation de référence de l'API NDK indique également quelque chose comme "Introduite dans l'API 30" pour chaque API. Si ce texte n'est pas présent, cela signifie que l'API est disponible pour tous les niveaux d'API compatibles.

Éviter la répétition des protections d'API

Si vous utilisez cette méthode, votre application contiendra probablement des sections de code qui ne sont utilisables que sur des appareils suffisamment récents. Plutôt que de répéter la vérification __builtin_available() dans chacune de vos fonctions, vous pouvez annoter votre propre code comme nécessitant un certain niveau d'API. Par exemple, les API ImageDecoder elles-mêmes ont été ajoutées dans l'API 30. Pour les fonctions qui utilisent beaucoup ces API, vous pouvez procéder comme suit:

#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();
    }
}

Particularités des protections d'API

Clang est très particulier sur la façon dont __builtin_available est utilisé. Seul un if (__builtin_available(...)) littéral (mais éventuellement remplacé par une macro) fonctionne. Même les opérations triviales telles que if (!__builtin_available(...)) ne fonctionneront pas (Clang émet l'avertissement unsupported-availability-guard, ainsi que unguarded-availability). Cela pourrait s'améliorer dans une future version de Clang. Pour en savoir plus, consultez le problème 33161 de LLVM.

Les vérifications de unguarded-availability ne s'appliquent qu'au champ d'application de la fonction dans laquelle elles sont utilisées. Clang émet l'avertissement même si la fonction avec l'appel d'API n'est jamais appelée qu'à partir d'un champ d'application protégé. Pour éviter la répétition des protections dans votre propre code, consultez Éviter la répétition des protections d'API.

Pourquoi ce n'est-il pas le paramètre par défaut ?

Sauf si elles sont utilisées correctement, la différence entre les références d'API fortes et les références d'API faibles est que les premières échouent rapidement et de manière évidente, tandis que les secondes ne le font que lorsque l'utilisateur effectue une action qui entraîne l'appel de l'API manquante. Dans ce cas, le message d'erreur ne sera pas une erreur claire au moment de la compilation "AFoo_bar() n'est pas disponible", mais un segfault. Avec des références fortes, le message d'erreur est beaucoup plus clair, et l'échec rapide est un paramètre par défaut plus sûr.

Comme il s'agit d'une nouvelle fonctionnalité, très peu de code existant est écrit pour gérer ce comportement de manière sécurisée. Le code tiers qui n'a pas été écrit en pensant à Android présentera probablement toujours ce problème. Il n'est donc pas prévu de modifier le comportement par défaut.

Nous vous recommandons de l'utiliser, mais comme cela rend les problèmes plus difficiles à détecter et à déboguer, vous devez accepter ces risques en connaissance de cause plutôt que de voir le comportement changer à votre insu.

Mises en garde

Cette fonctionnalité fonctionne pour la plupart des API, mais elle ne fonctionne pas dans certains cas.

Les API libc les plus récentes sont les moins susceptibles de poser problème. Contrairement au reste des API Android, celles-ci sont protégées par #if __ANDROID_API__ >= X dans les en-têtes et pas seulement par __INTRODUCED_IN(X), ce qui empêche même la déclaration faible d'être vue. Étant donné que le niveau d'API le plus ancien compatible avec les NDK modernes est r21, les API libc les plus couramment requises sont déjà disponibles. De nouvelles API libc sont ajoutées à chaque version (voir status.md), mais plus elles sont récentes, plus elles sont susceptibles d'être un cas particulier dont peu de développeurs auront besoin. Toutefois, si vous faites partie de ces développeurs, vous devrez continuer à utiliser dlsym() pour appeler ces API si votre minSdkVersion est plus ancien que l'API. Il s'agit d'un problème qui peut être résolu, mais cela comporte le risque de rompre la compatibilité source pour toutes les applications (tout code contenant des polyfills d'API libc ne pourra pas être compilé en raison des attributs availability non concordants sur les déclarations libc et locales). Nous ne savons donc pas si nous allons le résoudre et quand.

Le cas le plus courant est celui où la bibliothèque contenant la nouvelle API est plus récente que votre minSdkVersion. Cette fonctionnalité n'active que les références de symboles faibles. Il n'existe pas de référence de bibliothèque faible. Par exemple, si votre minSdkVersion est 24, vous pouvez associer libvulkan.so et effectuer un appel protégé à vkBindBufferMemory2, car libvulkan.so est disponible sur les appareils à partir de l'API 24. En revanche, si votre minSdkVersion était 23, vous devez utiliser dlopen et dlsym, car la bibliothèque n'existera pas sur les appareils qui ne prennent en charge que l'API 23. Nous ne connaissons pas de bonne solution pour résoudre ce problème, mais à long terme, il se résoudra de lui-même, car nous n'autorisons plus (dans la mesure du possible) les nouvelles API à créer de nouvelles bibliothèques.

Pour les auteurs de bibliothèques

Si vous développez une bibliothèque à utiliser dans des applications Android, vous devez éviter d'utiliser cette fonctionnalité dans vos en-têtes publics. Il peut être utilisé en toute sécurité dans le code hors ligne, mais si vous vous appuyez sur __builtin_available dans le code de vos en-têtes, tels que les fonctions intégrées ou les définitions de modèles, vous forcez tous vos consommateurs à activer cette fonctionnalité. Pour les mêmes raisons que nous n'activons pas cette fonctionnalité par défaut dans le NDK, vous devez éviter de faire ce choix au nom de vos consommateurs.

Si vous avez besoin de ce comportement dans vos en-têtes publics, veillez à le documenter pour que vos utilisateurs sachent qu'ils devront activer la fonctionnalité et qu'ils soient conscients des risques encourus.