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 Dynamic Loader d'Android les résoudra hâtivement une fois votre bibliothèque chargée. Si les symboles sont introuvables, l'application est abandonnée. 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. Elle permet uniquement de reporter une erreur de temps de chargement à une erreur d'appel. L'avantage est que vous pouvez protéger cet appel au moment de l'exécution et revenir en douceur, que ce soit en utilisant une autre implémentation, 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 utilisant dlopen() et dlsym() pour une utilisation conditionnelle d'une API sans cette fonctionnalité:

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

La lecture est un peu désordonnée, les noms de fonctions sont dupliqués (et si vous écrivez en C, les signatures), la création est effectuée avec succès, mais la création de remplacement est toujours effectuée au moment de l'exécution si vous saisissez accidentellement le nom de la fonction transmis à dlsym. 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 est d'essayer de compiler sans garde (ou avec __builtin_available(android 1, *)) et de faire ce que le message d'erreur vous indique. 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 comportera probablement des sections de code qui ne seront 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 pour qu'il nécessite 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 (bien que macro-remplacée) fonctionne. Même les opérations triviales comme if (!__builtin_available(...)) ne fonctionneront pas (Clang émettra l'avertissement unsupported-availability-guard, ainsi que unguarded-availability). Cela pourrait s'améliorer dans une prochaine 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 où elles sont utilisées. Clang émettra cet avertissement même si la fonction comportant l'appel d'API n'est appelée que depuis 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 une valeur par défaut plus sûre.

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 pour Android rencontrera probablement toujours ce problème. Il n'est donc pas prévu que le comportement par défaut ne soit jamais modifié.

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. Cela dit, si vous faites partie de ces développeurs, pour l'instant, vous devez continuer à utiliser dlsym() pour appeler ces API si votre minSdkVersion est antérieur à 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.

Les développeurs sont susceptibles de rencontrer davantage de développeurs lorsque 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 de 23, vous devez revenir à dlopen et dlsym, car la bibliothèque n'existe pas sur l'appareil sur les appareils qui ne sont compatibles qu'avec 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 exigez ce comportement dans vos en-têtes publics, veillez à le documenter afin que vos utilisateurs sachent qu'ils devront activer la fonctionnalité et qu'ils soient conscients des risques associés.