AIDL pour les HAL

Android 11 permet d'utiliser AIDL pour les HAL dans Android, ce qui permet d'implémenter des parties d'Android sans HIDL. Migrez les HAL pour qu'elles utilisent exclusivement AIDL lorsque cela est possible (lorsque les HAL en amont utilisent HIDL, HIDL doit être utilisé).

Les HAL utilisant AIDL pour communiquer entre les composants du framework, tels que ceux de system.img, et les composants matériels, tels que ceux de vendor.img, doivent utiliser un AIDL stable. Toutefois, pour communiquer au sein d'une partition, par exemple d'un HAL à un autre, il n'y a aucune restriction sur le mécanisme IPC à utiliser.

Motivation

AIDL existe depuis plus longtemps que HIDL et est utilisé dans de nombreux autres endroits, par exemple entre les composants du framework Android ou dans les applications. Maintenant qu'AIDL est compatible avec la stabilité, il est possible d'implémenter une pile entière avec un seul environnement d'exécution IPC. AIDL dispose également d'un meilleur système de gestion des versions que HIDL. Voici quelques avantages d'AIDL :

  • L'utilisation d'un seul langage IPC signifie qu'il n'y a qu'une seule chose à apprendre, à déboguer, à optimiser et à sécuriser.
  • AIDL est compatible avec la gestion des versions sur place pour les propriétaires d'une interface :
    • Les propriétaires peuvent ajouter des méthodes à la fin des interfaces ou des champs aux Parcelables. Cela signifie qu'il est plus facile de versionner le code au fil des ans et que le coût d'une année sur l'autre est plus faible (les types peuvent être modifiés sur place et il n'est pas nécessaire d'utiliser des bibliothèques supplémentaires pour chaque version d'interface).
    • Les interfaces d'extension peuvent être associées au moment de l'exécution plutôt que dans le système de type. Il n'est donc pas nécessaire de rebaser les extensions en aval sur des versions plus récentes des interfaces.
  • Une interface AIDL existante peut être utilisée directement lorsque son propriétaire choisit de la stabiliser. Auparavant, une copie complète de l'interface devait être créée dans HIDL.

Compiler par rapport à l'environnement d'exécution AIDL

AIDL comporte trois backends différents : Java, NDK et CPP. Pour utiliser AIDL stable, utilisez toujours la copie système de libbinder à system/lib*/libbinder.so et communiquez sur /dev/binder. Pour le code sur l'image vendor, cela signifie que libbinder (à partir du VNDK) ne peut pas être utilisé : cette bibliothèque possède une API C++ et des éléments internes instables. Au lieu de cela, le code du fournisseur natif doit utiliser le backend NDK d'AIDL, établir un lien avec libbinder_ndk (qui est soutenu par le libbinder.so système) et établir un lien avec les bibliothèques NDK créées par les entrées aidl_interface. Pour connaître les noms exacts des modules, consultez les Règles de dénomination des modules.

Écrire une interface AIDL HAL

Pour qu'une interface AIDL puisse être utilisée entre le système et le fournisseur, elle doit être modifiée à deux niveaux :

  • Chaque définition de type doit être annotée avec @VintfStability.
  • La déclaration aidl_interface doit inclure stability: "vintf",.

Seul le propriétaire d'une interface peut apporter ces modifications.

Lorsque vous apportez ces modifications, l'interface doit se trouver dans le fichier manifeste VINTF pour fonctionner. Testez cette exigence (et les exigences associées, comme la vérification que les interfaces publiées sont figées) à l'aide du test VTS (Vendor Test Suite) vts_treble_vintf_vendor_test. Vous pouvez utiliser une interface @VintfStability sans ces exigences en appelant AIBinder_forceDowngradeToLocalStability dans le backend NDK, android::Stability::forceDowngradeToLocalStability dans le backend C++ ou android.os.Binder#forceDowngradeToSystemStability dans le backend Java sur un objet Binder avant qu'il ne soit envoyé à un autre processus.

De plus, pour une portabilité maximale du code et pour éviter des problèmes potentiels tels que des bibliothèques supplémentaires inutiles, désactivez le backend CPP.

Le code montre comment désactiver le backend CPP :

    aidl_interface: {
        ...
        backend: {
            cpp: {
                enabled: false,
            },
        },
    }

Trouver les interfaces AIDL HAL

Les interfaces AIDL stables de l'AOSP pour les HAL se trouvent dans les dossiers aidl des mêmes répertoires de base que les interfaces HIDL :

  • hardware/interfaces concerne les interfaces généralement fournies par le matériel.
  • frameworks/hardware/interfaces concerne les interfaces de haut niveau fournies au matériel.
  • system/hardware/interfaces concerne les interfaces de bas niveau fournies au matériel.

Placez les interfaces d'extension dans d'autres sous-répertoires hardware/interfaces dans vendor ou hardware.

Interfaces d'extension

Android propose un ensemble d'interfaces AOSP officielles à chaque version. Lorsque les partenaires Android souhaitent ajouter des fonctionnalités à ces interfaces, ils ne doivent pas les modifier directement, car cela rend leur environnement d'exécution Android incompatible avec l'environnement d'exécution Android AOSP. Évitez de modifier ces interfaces pour que l'image GSI puisse continuer à fonctionner.

Les extensions peuvent s'enregistrer de deux manières différentes :

Cependant, quelle que soit la manière dont une extension est enregistrée, lorsque des composants spécifiques au fournisseur (c'est-à-dire ne faisant pas partie de l'AOSP en amont) utilisent l'interface, les conflits de fusion ne sont pas possibles. Toutefois, lorsque des modifications en aval sont apportées aux composants AOSP en amont, des conflits de fusion peuvent se produire. Les stratégies suivantes sont alors recommandées :

  • Transférez les ajouts d'interface vers AOSP dans la prochaine version.
  • Ajouts à l'interface en amont qui offrent plus de flexibilité (sans conflits de fusion) dans la prochaine version.

Objets Parcelable d'extension : ParcelableHolder

ParcelableHolder est une instance de l'interface Parcelable qui peut contenir une autre instance de Parcelable.

Le principal cas d'utilisation de ParcelableHolder est de rendre Parcelable extensible. Par exemple, imaginez que les implémenteurs d'appareils s'attendent à pouvoir étendre un Parcelable, AospDefinedParcelable défini par AOSP pour inclure leurs fonctionnalités à valeur ajoutée.

Utilisez l'interface ParcelableHolder pour étendre Parcelable avec vos fonctionnalités à valeur ajoutée. L'interface ParcelableHolder contient une instance de Parcelable. Si vous essayez d'ajouter des champs à Parcelable directement, une erreur se produit :

parcelable AospDefinedParcelable {
  int a;
  String b;
  String x; // ERROR: added by a device implementer
  int[] y; // added by a device implementer
}

Comme le montre le code précédent, cette pratique est incorrecte, car les champs ajoutés par l'implémenteur de l'appareil peuvent être en conflit lorsque Parcelable est révisé dans les prochaines versions d'Android.

À l'aide de ParcelableHolder, le propriétaire d'un élément Parcelable peut définir un point d'extension dans une instance de Parcelable :

parcelable AospDefinedParcelable {
  int a;
  String b;
  ParcelableHolder extension;
}

Les responsables de l'implémentation de l'appareil peuvent ensuite définir leur propre instance Parcelable pour leur extension :

parcelable OemDefinedParcelable {
  String x;
  int[] y;
}

La nouvelle instance Parcelable peut être associée à l'instance Parcelable d'origine à l'aide du champ ParcelableHolder :


// Java
AospDefinedParcelable ap = ...;
OemDefinedParcelable op = new OemDefinedParcelable();
op.x = ...;
op.y = ...;

ap.extension.setParcelable(op);

...

OemDefinedParcelable op = ap.extension.getParcelable(OemDefinedParcelable.class);

// C++
AospDefinedParcelable ap;
OemDefinedParcelable op;
std::shared_ptr<OemDefinedParcelable> op_ptr = make_shared<OemDefinedParcelable>();

ap.extension.setParcelable(op);
ap.extension.setParcelable(op_ptr);

...

std::shared_ptr<OemDefinedParcelable> op_ptr;

ap.extension.getParcelable(&op_ptr);

// NDK
AospDefinedParcelable ap;
OemDefinedParcelable op;
ap.extension.setParcelable(op);

...

std::optional<OemDefinedParcelable> op;
ap.extension.getParcelable(&op);

// Rust
let mut ap = AospDefinedParcelable { .. };
let op = Rc::new(OemDefinedParcelable { .. });

ap.extension.set_parcelable(Rc::clone(&op));

...

let op = ap.extension.get_parcelable::<OemDefinedParcelable>();

Noms d'instance du serveur AIDL HAL

Par convention, les services AIDL HAL ont un nom d'instance au format $package.$type/$instance. Par exemple, une instance du HAL du vibreur est enregistrée en tant que android.hardware.vibrator.IVibrator/default.

Écrire un serveur HAL AIDL

Les serveurs AIDL @VintfStability doivent être déclarés dans le fichier manifeste VINTF, par exemple :

    <hal format="aidl">
        <name>android.hardware.vibrator</name>
        <version>1</version>
        <fqname>IVibrator/default</fqname>
    </hal>

Sinon, ils doivent enregistrer un service AIDL normalement. Lors de l'exécution des tests VTS, il est prévu que tous les HAL AIDL déclarés soient disponibles.

Écrire un client AIDL

Les clients AIDL doivent se déclarer dans la matrice de compatibilité, par exemple :

    <hal format="aidl" optional="true">
        <name>android.hardware.vibrator</name>
        <version>1-2</version>
        <interface>
            <name>IVibrator</name>
            <instance>default</instance>
        </interface>
    </hal>

Convertir un HAL existant de HIDL en AIDL

Utilisez l'outil hidl2aidl pour convertir une interface HIDL en AIDL.

Fonctionnalités hidl2aidl :

  • Créez des fichiers AIDL (.aidl) en fonction des fichiers HAL (.hal) pour le package donné.
  • Créez des règles de compilation pour le package AIDL nouvellement créé avec tous les backends activés.
  • Créez des méthodes de traduction dans les backends Java, CPP et NDK pour traduire les types HIDL en types AIDL.
  • Créez des règles de compilation pour les bibliothèques de traduction avec les dépendances requises.
  • Créez des assertions statiques pour vous assurer que les énumérateurs HIDL et AIDL ont les mêmes valeurs dans les backends CPP et NDK.

Pour convertir un package de fichiers HAL en fichiers AIDL, procédez comme suit :

  1. Compilez l'outil situé dans system/tools/hidl/hidl2aidl.

    La compilation de cet outil à partir de la dernière source offre l'expérience la plus complète. Vous pouvez utiliser la dernière version pour convertir les interfaces sur les anciennes branches des versions précédentes :

    m hidl2aidl
  2. Exécutez l'outil avec un répertoire de sortie, suivi du package à convertir.

    Vous pouvez également utiliser l'argument -l pour ajouter le contenu d'un nouveau fichier de licence en haut de tous les fichiers générés. Veillez à utiliser la bonne licence et la bonne date :

    hidl2aidl -o <output directory> -l <file with license> <package>

    Exemple :

    hidl2aidl -o . -l my_license.txt android.hardware.nfc@1.2
  3. Parcourez les fichiers générés et corrigez les problèmes de conversion :

    • conversion.log contient tous les problèmes non résolus à corriger en premier.
    • Les fichiers AIDL générés peuvent contenir des avertissements et des suggestions qui nécessitent une action. Ces commentaires commencent par //.
    • Nettoyez et améliorez le package.
    • Consultez l'annotation @JavaDerive pour connaître les fonctionnalités qui pourraient être nécessaires, comme toString ou equals.
  4. Créez uniquement les cibles dont vous avez besoin :

    • Désactivez les backends qui ne seront pas utilisés. Préférez le backend NDK au backend CPP. Pour en savoir plus, consultez Compiler avec le runtime AIDL.
    • Supprimez les bibliothèques de traduction ou tout code généré qui ne sera pas utilisé.
  5. Consultez les principales différences entre AIDL et HIDL :

    • L'utilisation des exceptions et de Status intégrés à AIDL améliore généralement l'interface et élimine le besoin d'un autre type d'état spécifique à l'interface.
    • Les arguments d'interface AIDL dans les méthodes ne sont pas @nullable par défaut, contrairement à HIDL.

SEPolicy pour les HAL AIDL

Un type de service AIDL visible par le code du fournisseur doit comporter l'attribut hal_service_type. Sinon, la configuration sepolicy est la même que pour tout autre service AIDL (bien qu'il existe des attributs spéciaux pour les HAL). Voici un exemple de définition d'un contexte de service HAL :

    type hal_foo_service, service_manager_type, hal_service_type;

Pour la plupart des services définis par la plate-forme, un contexte de service avec le type approprié est déjà ajouté (par exemple, android.hardware.foo.IFoo/default est déjà marqué comme hal_foo_service). Toutefois, si un client de framework prend en charge plusieurs noms d'instance, des noms d'instance supplémentaires doivent être ajoutés dans les fichiers service_contexts spécifiques à l'appareil :

    android.hardware.foo.IFoo/custom_instance u:object_r:hal_foo_service:s0

Lorsque vous créez un type de HAL, vous devez ajouter des attributs HAL. Un attribut HAL spécifique peut être associé à plusieurs types de services (chacun pouvant comporter plusieurs instances, comme nous l'avons vu). Pour une HAL, foo, il existe hal_attribute(foo). Cette macro définit les attributs hal_foo_client et hal_foo_server. Pour un domaine donné, les macros hal_client_domain et hal_server_domain associent un domaine à un attribut HAL donné. Par exemple, le serveur système étant un client de cette HAL correspond à la règle hal_client_domain(system_server, hal_foo). Un serveur HAL inclut également hal_server_domain(my_hal_domain, hal_foo).

En règle générale, pour un attribut HAL donné, créez également un domaine tel que hal_foo_default pour les HAL de référence ou d'exemple. Toutefois, certains appareils utilisent ces domaines pour leurs propres serveurs. La distinction entre les domaines pour plusieurs serveurs n'est importante que s'il existe plusieurs serveurs qui diffusent la même interface et qui ont besoin d'un ensemble d'autorisations différent dans leurs implémentations. Dans toutes ces macros, hal_foo n'est pas un objet sepolicy. Ce jeton est plutôt utilisé par ces macros pour faire référence au groupe d'attributs associés à une paire client/serveur.

Toutefois, pour le moment, hal_foo_service et hal_foo (la paire d'attributs de hal_attribute(foo)) ne sont pas associés. Un attribut HAL est associé aux services AIDL HAL à l'aide de la macro hal_attribute_service (les HAL HIDL utilisent la macro hal_attribute_hwservice), par exemple hal_attribute_service(hal_foo, hal_foo_service). Cela signifie que les processus hal_foo_client peuvent s'emparer de la HAL et que les processus hal_foo_server peuvent enregistrer la HAL. L'application de ces règles d'enregistrement est effectuée par le gestionnaire de contexte (servicemanager).

Les noms de service ne correspondent pas toujours aux attributs HAL, par exemple hal_attribute_service(hal_foo, hal_foo2_service). En général, comme cela implique que les services sont toujours utilisés ensemble, vous pouvez supprimer hal_foo2_service et utiliser hal_foo_service pour tous les contextes de service. Lorsque les HAL définissent plusieurs instances hal_attribute_service, c'est parce que le nom de l'attribut HAL d'origine n'est pas assez général et ne peut pas être modifié.

En réunissant tous ces éléments, un exemple de HAL ressemble à ce qui suit :

    public/attributes:
    // define hal_foo, hal_foo_client, hal_foo_server
    hal_attribute(foo)

    public/service.te
    // define hal_foo_service
    type hal_foo_service, hal_service_type, protected_service, service_manager_type

    public/hal_foo.te:
    // allow binder connection from client to server
    binder_call(hal_foo_client, hal_foo_server)
    // allow client to find the service, allow server to register the service
    hal_attribute_service(hal_foo, hal_foo_service)
    // allow binder communication from server to service_manager
    binder_use(hal_foo_server)

    private/service_contexts:
    // bind an AIDL service name to the selinux type
    android.hardware.foo.IFooXxxx/default u:object_r:hal_foo_service:s0

    private/<some_domain>.te:
    // let this domain use the hal service
    binder_use(some_domain)
    hal_client_domain(some_domain, hal_foo)

    vendor/<some_hal_server_domain>.te
    // let this domain serve the hal service
    hal_server_domain(some_hal_server_domain, hal_foo)

Interfaces d'extension associées

Une extension peut être associée à n'importe quelle interface de binder, qu'il s'agisse d'une interface de niveau supérieur enregistrée directement auprès du gestionnaire de services ou d'une sous-interface. Lorsque vous obtenez une extension, vous devez confirmer que son type est celui attendu. Vous ne pouvez définir des extensions qu'à partir du processus servant un binder.

Utilisez des extensions associées chaque fois qu'une extension modifie la fonctionnalité d'une HAL existante. Lorsqu'une toute nouvelle fonctionnalité est nécessaire, ce mécanisme n'est pas nécessaire et vous pouvez enregistrer une interface d'extension directement auprès du gestionnaire de services. Les interfaces d'extension associées sont plus pertinentes lorsqu'elles sont associées à des sous-interfaces, car ces hiérarchies peuvent être profondes ou multi-instances. L'utilisation d'une extension globale pour refléter la hiérarchie de l'interface Binder d'un autre service nécessite une comptabilité étendue pour fournir des fonctionnalités équivalentes à celles des extensions directement attachées.

Pour définir une extension sur un binder, utilisez les API suivantes :

  • Backend NDK : AIBinder_setExtension
  • Backend Java : android.os.Binder.setExtension
  • Backend CPP : android::Binder::setExtension
  • Backend Rust : binder::Binder::set_extension

Pour obtenir une extension sur un binder, utilisez les API suivantes :

  • Backend NDK : AIBinder_getExtension
  • Backend Java : android.os.IBinder.getExtension
  • Backend CPP : android::IBinder::getExtension
  • Backend Rust : binder::Binder::get_extension

Pour en savoir plus sur ces API, consultez la documentation de la fonction getExtension dans le backend correspondant. Vous trouverez un exemple d'utilisation des extensions dans hardware/interfaces/tests/extension/vibrator.

Principales différences entre AIDL et HIDL

Lorsque vous utilisez des HAL AIDL ou des interfaces HAL AIDL, tenez compte des différences par rapport à l'écriture de HAL HIDL.

  • La syntaxe du langage AIDL est plus proche de celle de Java. La syntaxe HIDL est semblable à celle de C++.
  • Toutes les interfaces AIDL disposent d'états d'erreur intégrés. Au lieu de créer des types d'état personnalisés, créez des entiers d'état constants dans les fichiers d'interface et utilisez EX_SERVICE_SPECIFIC dans les backends CPP et NDK, et ServiceSpecificException dans le backend Java. Consultez la section Gestion des erreurs.
  • AIDL ne démarre pas automatiquement les pools de threads lorsque des objets Binder sont envoyés. Vous devez les démarrer manuellement (voir Gestion des threads).
  • AIDL n'abandonne pas les erreurs de transport non vérifiées (HIDL Return abandonne les erreurs non vérifiées).
  • AIDL ne peut déclarer qu'un seul type par fichier.
  • Les arguments AIDL peuvent être spécifiés en tant que in, out ou inout en plus du paramètre de sortie (il n'y a pas de rappels synchrones).
  • AIDL utilise fd comme type primitif au lieu de handle.
  • HIDL utilise des versions majeures pour les modifications incompatibles et des versions mineures pour les modifications compatibles. Dans AIDL, les modifications rétrocompatibles sont effectuées sur place. AIDL ne dispose pas d'un concept explicite de versions majeures. Celles-ci sont plutôt intégrées aux noms de packages. Par exemple, AIDL peut utiliser le nom de package bluetooth2.
  • AIDL n'hérite pas de la priorité en temps réel par défaut. La fonction setInheritRt doit être utilisée par binder pour activer l'héritage de priorité en temps réel.

Tests pour les HAL

Cette section décrit les bonnes pratiques pour tester les HAL. Ces pratiques sont valables même si le test d'intégration de votre HAL ne se trouve pas dans VTS.

Android s'appuie sur VTS pour vérifier les implémentations HAL attendues. VTS permet de s'assurer qu'Android peut être rétrocompatible avec les anciennes implémentations des fournisseurs. Les implémentations qui échouent au VTS présentent des problèmes de compatibilité connus qui pourraient les empêcher de fonctionner avec les futures versions de l'OS.

VTS pour HAL comporte deux parties principales.

1. Vérifier que les HAL de l'appareil sont connus et attendus par Android

Android s'appuie sur une liste statique et précise de toutes les HAL installées. Cette liste est exprimée dans le fichier manifeste VINTF. Des tests spéciaux à l'échelle de la plate-forme vérifient l'intégrité des couches HAL dans l'ensemble du système. Avant d'écrire des tests spécifiques à un HAL, vous devez également exécuter ces tests, car ils peuvent indiquer si un HAL présente des configurations VINTF incohérentes.

Cet ensemble de tests se trouve dans test/vts-testcase/hal/treble/vintf. Si vous travaillez sur une implémentation HAL du fournisseur, utilisez vts_treble_vintf_vendor_test pour la vérifier. Vous pouvez exécuter ce test avec la commande atest vts_treble_vintf_vendor_test.

Ces tests permettent de vérifier les éléments suivants :

  • Chaque interface @VintfStability déclarée dans un fichier manifeste VINTF est figée à une version publiée connue. Cela permet de vérifier que les deux côtés de l'interface s'accordent sur la définition exacte de cette version de l'interface. C'est nécessaire pour le fonctionnement de base.
  • Toutes les HAL déclarées dans un fichier manifeste VINTF sont disponibles sur cet appareil. Tout client disposant des autorisations suffisantes pour utiliser un service HAL déclaré doit pouvoir obtenir et utiliser ces services à tout moment.
  • Tous les HAL déclarés dans un fichier manifeste VINTF utilisent la version de l'interface qu'ils déclarent dans le fichier manifeste.
  • Aucun HAL obsolète n'est utilisé sur un appareil. Android abandonne la prise en charge des versions antérieures des interfaces HAL, comme décrit dans Cycle de vie FCM.
  • Les HAL requis sont présents sur l'appareil. Certaines HAL sont nécessaires au bon fonctionnement d'Android.

2. Vérifier le comportement attendu de chaque HAL

Chaque interface HAL possède ses propres tests VTS pour vérifier le comportement attendu de ses clients. Les scénarios de test s'exécutent sur chaque instance d'une interface HAL déclarée et appliquent un comportement spécifique en fonction de la version de l'interface implémentée.

En C++, vous pouvez obtenir la liste de tous les HAL installés sur le système avec la fonction android::getAidlHalInstanceNames dans libaidlvintf_gtest_helper. En Rust, utilisez binder::get_declared_instances.

Ces tests tentent de couvrir tous les aspects de l'implémentation HAL sur lesquels le framework Android s'appuie ou pourrait s'appuyer à l'avenir.

Ces tests incluent la vérification de la compatibilité des fonctionnalités, de la gestion des erreurs et de tout autre comportement qu'un client peut attendre du service.

Jalons VTS pour le développement HAL

Les tests VTS (ou tout autre test) doivent être tenus à jour lors de la création ou de la modification des interfaces HAL d'Android.

Les tests VTS doivent être terminés et prêts à valider les implémentations des fournisseurs avant d'être figés pour les versions de l'API Android Vendor. Elles doivent être prêtes avant le gel des interfaces afin que les développeurs puissent créer leurs implémentations, les vérifier et fournir des commentaires aux développeurs d'interfaces HAL.

Tester sur Cuttlefish

Lorsque le matériel n'est pas disponible, Android utilise Cuttlefish comme véhicule de développement pour les interfaces HAL. Cela permet de tester l'intégration d'Android de manière évolutive.

hal_implementation_test teste si Cuttlefish dispose d'implémentations des dernières versions de l'interface HAL pour s'assurer qu'Android est prêt à gérer les nouvelles interfaces et que les tests VTS sont prêts à tester les nouvelles implémentations du fournisseur dès que de nouveaux matériels et appareils sont disponibles.