AIDL stable

Android 10 ajoute la prise en charge du langage AIDL (Android Interface Definition Language) stable, une nouvelle façon de suivre l'interface de programme d'application (API)/interface binaire d'application (ABI) fournie par les interfaces AIDL. L'AIDL stable présente les principales différences suivantes par rapport à l'AIDL :

  • Les interfaces sont définies dans le système de construction avec aidl_interfaces .
  • Les interfaces ne peuvent contenir que des données structurées. Les parcelles représentant les types souhaités sont automatiquement créées en fonction de leur définition AIDL et sont automatiquement triées et non triées.
  • Les interfaces peuvent être déclarées stables (rétrocompatibles). Lorsque cela se produit, leur API est suivie et versionnée dans un fichier à côté de l'interface AIDL.

Définir une interface AIDL

Une définition de aidl_interface ressemble à ceci :

aidl_interface {
    name: "my-aidl",
    srcs: ["srcs/aidl/**/*.aidl"],
    local_include_dir: "srcs/aidl",
    imports: ["other-aidl"],
    versions: ["1", "2"],
    stability: "vintf",
    backend: {
        java: {
            enabled: true,
            platform_apis: true,
        },
        cpp: {
            enabled: true,
        },
        ndk: {
            enabled: true,
        },
    },

}
  • name : Le nom du module d'interface AIDL qui identifie de manière unique une interface AIDL.
  • srcs : La liste des fichiers sources AIDL qui composent l'interface. Le chemin d'un Foo de type AIDL défini dans un package com.acme doit être <base_path>/com/acme/Foo.aidl , où <base_path> peut être n'importe quel répertoire lié au répertoire où se trouve Android.bp . Dans l'exemple ci-dessus, <base_path> est srcs/aidl .
  • local_include_dir : chemin d'accès à partir duquel le nom du package commence. Il correspond à <base_path> expliqué ci-dessus.
  • imports : Une liste des modules aidl_interface que ceci utilise. Si une de vos interfaces AIDL utilise une interface ou un parcelable d'une autre aidl_interface , mettez son nom ici. Il peut s'agir du nom seul, pour faire référence à la dernière version, ou du nom avec le suffixe de version (tel que -V1 ) pour faire référence à une version spécifique. La spécification d'une version est prise en charge depuis Android 12
  • versions : Les versions précédentes de l'interface qui sont figées sous api_dir , A partir d'Android 11, les versions sont figées sous aidl_api/ name . S'il n'y a pas de versions figées d'une interface, cela ne doit pas être spécifié et il n'y aura pas de vérification de compatibilité.
  • stability : Le drapeau facultatif pour la promesse de stabilité de cette interface. Actuellement, ne prend en charge que "vintf" . S'il n'est pas défini, cela correspond à une interface stable dans ce contexte de compilation (ainsi, une interface chargée ici ne peut être utilisée qu'avec des choses compilées ensemble, par exemple sur system.img). S'il vaut "vintf" , cela correspond à une promesse de stabilité : l'interface doit rester stable tant qu'elle est utilisée.
  • gen_trace : L'indicateur facultatif pour activer ou désactiver le traçage. La valeur par défaut est false .
  • host_supported : l'indicateur facultatif qui, lorsqu'il est défini sur true , rend les bibliothèques générées disponibles pour l'environnement hôte.
  • unstable : Le drapeau optionnel utilisé pour marquer que cette interface n'a pas besoin d'être stable. Lorsqu'il est défini sur true , le système de construction ne crée pas de vidage d'API pour l'interface et n'exige pas qu'il soit mis à jour.
  • backend.<type>.enabled : Ces drapeaux basculent chacun des backends pour lesquels le compilateur AIDL va générer du code. Actuellement, trois backends sont pris en charge : java , cpp et ndk . Les backends sont tous activés par défaut. Lorsqu'un backend spécifique n'est pas nécessaire, il doit être désactivé explicitement.
  • backend.<type>.apex_available : La liste des noms APEX pour lesquels la bibliothèque de stub générée est disponible.
  • backend.[cpp|java].gen_log : L'indicateur facultatif qui contrôle s'il faut générer du code supplémentaire pour collecter des informations sur la transaction.
  • backend.[cpp|java].vndk.enabled : Le drapeau facultatif pour faire de cette interface une partie de VNDK. La valeur par défaut est false .
  • backend.java.platform_apis : L'indicateur facultatif qui contrôle si la bibliothèque de stub Java est construite par rapport aux API privées de la plate-forme. Cela doit être défini sur "true" lorsque stability est définie sur "vintf" .
  • backend.java.sdk_version : L'indicateur facultatif pour spécifier la version du SDK sur laquelle la bibliothèque de stub Java est construite. La valeur par défaut est "system_current" . Cela ne doit pas être défini lorsque backend.java.platform_apis est vrai.
  • backend.java.platform_apis : l'indicateur facultatif qui doit être défini sur true lorsque les bibliothèques générées doivent être construites sur l'API de la plate-forme plutôt que sur le SDK.

Pour chaque combinaison des versions et des backends activés, une bibliothèque de stub est créée. Voir Règles de dénomination des modules pour savoir comment faire référence à la version spécifique de la bibliothèque de stub pour un backend spécifique.

Ecriture de fichiers AIDL

Les interfaces dans AIDL stable sont similaires aux interfaces traditionnelles, à l'exception qu'elles ne sont pas autorisées à utiliser des parcelables non structurés (car ils ne sont pas stables !). La principale différence dans l'AIDL stable est la façon dont les parcelles sont définies. Auparavant, les colis étaient déclarés à terme ; dans AIDL stable, les champs parcelables et les variables sont définis explicitement.

// in a file like 'some/package/Thing.aidl'
package some.package;

parcelable SubThing {
    String a = "foo";
    int b;
}

Une valeur par défaut est actuellement prise en charge (mais pas obligatoire) pour boolean , char , float , double , byte , int , long et String . Dans Android 12, les valeurs par défaut des énumérations définies par l'utilisateur sont également prises en charge. Lorsqu'une valeur par défaut n'est pas spécifiée, une valeur semblable à 0 ou vide est utilisée. Les énumérations sans valeur par défaut sont initialisées à 0 même s'il n'y a pas d'énumérateur zéro.

Utilisation des bibliothèques de stub

Après avoir ajouté des bibliothèques de stub en tant que dépendance à votre module, vous pouvez les inclure dans vos fichiers. Voici des exemples de bibliothèques de stub dans le système de construction ( Android.mk peut également être utilisé pour les définitions de module héritées) :

cc_... {
    name: ...,
    shared_libs: ["my-module-name-cpp"],
    ...
}
# or
java_... {
    name: ...,
    // can also be shared_libs if desire is to load a library and share
    // it among multiple users or if you only need access to constants
    static_libs: ["my-module-name-java"],
    ...
}

Exemple en C++ :

#include "some/package/IFoo.h"
#include "some/package/Thing.h"
...
    // use just like traditional AIDL

Exemple en Java :

import some.package.IFoo;
import some.package.Thing;
...
    // use just like traditional AIDL

Interfaces de gestion des versions

La déclaration d'un module avec le nom foo crée également une cible dans le système de construction que vous pouvez utiliser pour gérer l'API du module. Une fois construit, foo-freeze-api ajoute une nouvelle définition d'API sous api_dir ou aidl_api/ name , selon la version d'Android, et ajoute un fichier .hash , tous deux représentant la version nouvellement gelée de l'interface. La construction de ceci met également à jour la propriété versions pour refléter la version supplémentaire. Une fois la propriété versions spécifiée, le système de construction exécute des vérifications de compatibilité entre les versions gelées et également entre le Top of Tree (ToT) et la dernière version gelée.

De plus, vous devez gérer la définition de l'API de la version ToT. Chaque fois qu'une API est mise à jour, exécutez foo-update-api pour mettre à jour aidl_api/ name /current qui contient la définition de l'API de la version ToT.

Pour maintenir la stabilité d'une interface, les propriétaires peuvent ajouter de nouveaux :

  • Méthodes jusqu'à la fin d'une interface (ou méthodes avec de nouvelles séries explicitement définies)
  • Éléments à la fin d'une parcelle (nécessite l'ajout d'une valeur par défaut pour chaque élément)
  • Valeurs constantes
  • Dans Android 11, les enquêteurs
  • Dans Android 12, champs jusqu'à la fin d'une union

Aucune autre action n'est autorisée et personne d'autre ne peut modifier une interface (sinon elles risquent d'entrer en collision avec les modifications apportées par un propriétaire).

Pour tester que toutes les interfaces sont gelées pour la publication, vous pouvez créer avec l'ensemble de variables d'environnement suivant :

  • AIDL_FROZEN_REL=true m ... - la construction nécessite le gel de toutes les interfaces AIDL stables qui n'ont pas de owner: champ spécifié.
  • AIDL_FROZEN_OWNERS="aosp test" - la construction nécessite que toutes les interfaces AIDL stables soient gelées avec le owner: champ spécifié comme "aosp" ou "test".

Utiliser des interfaces versionnées

Méthodes d'interface

Lors de l'exécution, lorsque vous essayez d'appeler de nouvelles méthodes sur un ancien serveur, les nouveaux clients obtiennent soit une erreur, soit une exception, selon le backend.

  • cpp backend obtient ::android::UNKNOWN_TRANSACTION .
  • ndk backend obtient STATUS_UNKNOWN_TRANSACTION .
  • java backend obtient android.os.RemoteException avec un message indiquant que l'API n'est pas implémentée.

Pour les stratégies permettant de gérer cela, voir interroger les versions et utiliser les valeurs par défaut .

Colisables

Lorsque de nouveaux champs sont ajoutés aux parcelles, les anciens clients et serveurs les suppriment. Lorsque de nouveaux clients et serveurs reçoivent d'anciens colisables, les valeurs par défaut des nouveaux champs sont automatiquement renseignées. Cela signifie que des valeurs par défaut doivent être spécifiées pour tous les nouveaux champs d'un colisable.

Les clients ne doivent pas s'attendre à ce que les serveurs utilisent les nouveaux champs à moins qu'ils ne sachent que le serveur implémente la version dans laquelle le champ est défini (voir interroger les versions ).

Énumérations et constantes

De même, les clients et les serveurs devraient rejeter ou ignorer les valeurs constantes et les énumérateurs non reconnus, selon le cas, car d'autres pourraient être ajoutés à l'avenir. Par exemple, un serveur ne doit pas abandonner lorsqu'il reçoit un énumérateur dont il n'a pas connaissance. Il doit soit l'ignorer, soit renvoyer quelque chose pour que le client sache qu'il n'est pas pris en charge dans cette implémentation.

Les syndicats

Essayer d'envoyer une union avec un nouveau champ échoue si le récepteur est ancien et ne connaît pas le champ. L'implémentation ne verra jamais l'union avec le nouveau champ. L'échec est ignoré s'il s'agit d'une transaction à sens unique ; sinon, l'erreur est BAD_VALUE (pour le backend C++ ou NDK) ou IllegalArgumentException (pour le backend Java). L'erreur est reçue si le client envoie un ensemble d'union au nouveau champ à un ancien serveur, ou lorsqu'il s'agit d'un ancien client qui reçoit l'union d'un nouveau serveur.

Règles de nommage des modules

Dans Android 11, pour chaque combinaison des versions et des backends activés, un module de bibliothèque de stub est automatiquement créé. Pour faire référence à un module de bibliothèque stub spécifique pour la liaison, n'utilisez pas le nom du module aidl_interface , mais le nom du module de bibliothèque stub, qui est ifacename - version - backend , où

  • ifacename : nom du module aidl_interface
  • la version est l'une des
    • V version-number pour les versions gelées
    • V latest-frozen-version-number + 1 pour la version tip-of-tree (encore à geler)
  • le backend est l'un des
    • java pour le backend Java,
    • cpp pour le backend C++,
    • ndk ou ndk_platform pour le backend NDK. Le premier est destiné aux applications et le second à l'utilisation de la plate-forme.

Supposons qu'il existe un module avec le nom foo et que sa dernière version est 2 , et qu'il prend en charge à la fois NDK et C++. Dans ce cas, AIDL génère ces modules :

  • Basé sur la version 1
    • foo-V1-(java|cpp|ndk|ndk_platform)
  • Basé sur la version 2 (la dernière version stable)
    • foo-V2-(java|cpp|ndk|ndk_platform)
  • Basé sur la version ToT
    • foo-V3-(java|cpp|ndk|ndk_platform)

Par rapport à Android 11,

  • foo- backend , qui faisait référence à la dernière version stable devient foo- V2 - backend
  • foo-unstable- backend , qui faisait référence à la version ToT devient foo- V3 - backend

Les noms des fichiers de sortie sont toujours les mêmes que les noms des modules.

  • Basé sur la version 1 : foo-V1-(cpp|ndk|ndk_platform).so
  • Basé sur la version 2 : foo-V2-(cpp|ndk|ndk_platform).so
  • Basé sur la version ToT : foo-V3-(cpp|ndk|ndk_platform).so

Notez que le compilateur AIDL ne crée ni un module de version unstable , ni un module non versionné pour une interface AIDL stable. Depuis Android 12, le nom du module généré à partir d'une interface AIDL stable inclut toujours sa version.

Nouvelles méthodes de méta-interface

Android 10 ajoute plusieurs méthodes de méta-interface pour l'AIDL stable.

Interrogation de la version d'interface de l'objet distant

Les clients peuvent interroger la version et le hachage de l'interface que l'objet distant implémente et comparer les valeurs renvoyées avec les valeurs de l'interface que le client utilise.

Exemple avec le backend cpp :

sp<IFoo> foo = ... // the remote object
int32_t my_ver = IFoo::VERSION;
int32_t remote_ver = foo->getInterfaceVersion();
if (remote_ver < my_ver) {
  // the remote side is using an older interface
}

std::string my_hash = IFoo::HASH;
std::string remote_hash = foo->getInterfaceHash();

Exemple avec le ndk (et le ndk_platform ) :

IFoo* foo = ... // the remote object
int32_t my_ver = IFoo::version;
int32_t remote_ver = 0;
if (foo->getInterfaceVersion(&remote_ver).isOk() && remote_ver < my_ver) {
  // the remote side is using an older interface
}

std::string my_hash = IFoo::hash;
std::string remote_hash;
foo->getInterfaceHash(&remote_hash);

Exemple avec le backend java :

IFoo foo = ... // the remote object
int myVer = IFoo.VERSION;
int remoteVer = foo.getInterfaceVersion();
if (remoteVer < myVer) {
  // the remote side is using an older interface
}

String myHash = IFoo.HASH;
String remoteHash = foo.getInterfaceHash();

Pour le langage Java, le côté distant DOIT implémenter getInterfaceVersion() et getInterfaceHash() comme suit :

class MyFoo extends IFoo.Stub {
    @Override
    public final int getInterfaceVersion() { return IFoo.VERSION; }

    @Override
    public final String getInterfaceHash() { return IFoo.HASH; }
}

En effet, les classes générées ( IFoo , IFoo.Stub , etc.) sont partagées entre le client et le serveur (par exemple, les classes peuvent se trouver dans le chemin de classe de démarrage). Lorsque les classes sont partagées, le serveur est également lié à la version la plus récente des classes, même s'il peut avoir été construit avec une version plus ancienne de l'interface. Si cette méta-interface est implémentée dans la classe partagée, elle renvoie toujours la version la plus récente. Cependant, en implémentant la méthode comme ci-dessus, le numéro de version de l'interface est intégré dans le code du serveur (car IFoo.VERSION est un static final int qui est en ligne lorsqu'il est référencé) et ainsi la méthode peut renvoyer la version exacte que le serveur a été construit avec.

Gérer les anciennes interfaces

Il est possible qu'un client soit mis à jour avec la version la plus récente d'une interface AIDL mais que le serveur utilise l'ancienne interface AIDL. Dans de tels cas, l'appel d'une méthode sur une ancienne interface renvoie UNKNOWN_TRANSACTION .

Avec AIDL stable, les clients ont plus de contrôle. Côté client, vous pouvez définir une implémentation par défaut sur une interface AIDL. Une méthode dans l'implémentation par défaut est invoquée uniquement lorsque la méthode n'est pas implémentée du côté distant (car elle a été construite avec une ancienne version de l'interface). Étant donné que les valeurs par défaut sont définies globalement, elles ne doivent pas être utilisées à partir de contextes potentiellement partagés.

Exemple en C++ sous Android T (AOSP expérimental) et versions ultérieures :

class MyDefault : public IFooDefault {
  Status anAddedMethod(...) {
   // do something default
  }
};

// once per an interface in a process
IFoo::setDefaultImpl(::android::sp<MyDefault>::make());

foo->anAddedMethod(...); // MyDefault::anAddedMethod() will be called if the
                         // remote side is not implementing it

Exemple en Java :

IFoo.Stub.setDefaultImpl(new IFoo.Default() {
    @Override
    public xxx anAddedMethod(...)  throws RemoteException {
        // do something default
    }
}); // once per an interface in a process


foo.anAddedMethod(...);

Vous n'avez pas besoin de fournir l'implémentation par défaut de toutes les méthodes dans une interface AIDL. Les méthodes qui sont garanties d'être implémentées du côté distant (parce que vous êtes certain que la télécommande est construite lorsque les méthodes étaient dans la description de l'interface AIDL) n'ont pas besoin d'être remplacées dans la classe impl par défaut.

Conversion d'AIDL existant en AIDL structuré/stable

Si vous disposez d'une interface AIDL existante et du code qui l'utilise, procédez comme suit pour convertir l'interface en une interface AIDL stable.

  1. Identifiez toutes les dépendances de votre interface. Pour chaque package dont dépend l'interface, déterminez si le package est défini dans une AIDL stable. S'il n'est pas défini, le package doit être converti.

  2. Convertissez tous les colis de votre interface en colis stables (les fichiers d'interface eux-mêmes peuvent rester inchangés). Pour ce faire, exprimez leur structure directement dans les fichiers AIDL. Les classes de gestion doivent être réécrites pour utiliser ces nouveaux types. Cela peut être fait avant de créer un package aidl_interface (ci-dessous).

  3. Créez un package aidl_interface (comme décrit ci-dessus) qui contient le nom de votre module, ses dépendances et toute autre information dont vous avez besoin. Pour le rendre stabilisé (pas seulement structuré), il doit également être versionné. Pour plus d'informations, voir Gestion des versions des interfaces .