AIDL pour les HAL

Android 11 permet d'utiliser AIDL pour les HAL sous Android. Cela permet d'implémenter certaines parties d'Android sans HIDL. Faites passer les HAL à l'utilisation d'AIDL uniquement dans la mesure du possible (lorsque les HAL en amont utilisent HIDL, HIDL doit être utilisé).

Les HAL qui utilisent 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 d'IPC à utiliser.

Motivation

AIDL existe depuis plus longtemps que HIDL et est utilisé à de nombreux autres endroits, par exemple entre les composants du framework Android ou dans les applications. Maintenant qu'AIDL est stable, 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.

  • L'utilisation d'un seul langage d'IPC implique de n'avoir qu'une seule chose à apprendre, à déboguer, à optimiser et à sécuriser.
  • AIDL permet 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 gérer les versions du 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'ajouter des bibliothèques pour chaque version de l'interface).
    • Les interfaces d'extension peuvent être associées au moment de l'exécution plutôt que dans le système de types. Il n'est donc pas nécessaire de redéfinir 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 décide de la stabiliser. Auparavant, une copie entière de l'interface devait être créée en HIDL.

Compiler avec l'environnement d'exécution AIDL

AIDL dispose de trois backends différents: Java, NDK et CPP. Pour utiliser la version stable d'AIDL, vous devez toujours utiliser la copie système de libbinder dans system/lib*/libbinder.so et parler sur /dev/binder. Pour le code sur l'image du fournisseur, cela signifie que libbinder (à partir du VNDK) ne peut pas être utilisé: cette bibliothèque possède une API C++ instable et des composants internes instables. À la place, le code de fournisseur natif doit utiliser le backend NDK d'AIDL, être associé à libbinder_ndk (qui repose sur le système libbinder.so) et être associé aux 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 HAL AIDL

Pour utiliser une interface AIDL entre le système et le fournisseur, deux modifications sont nécessaires:

  • 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 effectuer ces modifications.

Lorsque vous apportez ces modifications, l'interface doit figurer dans le fichier manifeste VINTF pour fonctionner. Testez cette option (et les exigences associées, telles que la vérification que les interfaces publiées sont figées) à l'aide de l'vts_treble_vintf_vendor_test de test VSS. Vous pouvez utiliser une interface @VintfStability sans ces exigences en appelant AIBinder_forceDowngradeToLocalStability dans le backend du NDK, android::Stability::forceDowngradeToLocalStability dans le backend C++ ou android.os.Binder#forceDowngradeToSystemStability dans le backend Java sur un objet de liaison avant qu'il ne soit envoyé à un autre processus. Il n'est pas possible de revenir à une version antérieure d'un service au niveau de stabilité du fournisseur en Java, car toutes les applications s'exécutent dans un contexte système.

En outre, 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.

Notez que l'utilisation de backends dans l'exemple de code ci-dessous est correcte, car il existe trois backends (Java, NDK et CPP). Le code ci-dessous indique comment sélectionner spécifiquement le backend CPP pour le désactiver.

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

Rechercher des interfaces HAL AIDL

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

  • matériel/interfaces
  • Frameworks/matériel/interfaces
  • système/matériel/interfaces

Vous devez placer les interfaces d'extension dans d'autres sous-répertoires hardware/interfaces de vendor ou hardware.

Interfaces des extensions

Android dispose d'un ensemble d'interfaces AOSP officielles avec chaque version. Lorsque les partenaires Android souhaitent ajouter des fonctionnalités à ces interfaces, ils ne doivent pas les modifier directement, car cela signifierait que leur environnement d'exécution Android est incompatible avec l'environnement d'exécution Android AOSP. Pour les appareils GMS, le fait d'éviter de modifier ces interfaces garantit également que l'image GSI peut continuer à fonctionner.

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

  • au moment de l'exécution, consultez la section Extensions associées.
  • autonome, enregistrée dans le monde entier et dans VINTF.

Cependant, une extension est enregistrée. Lorsque des composants spécifiques au fournisseur (qui ne font pas partie des composants AOSP en amont) utilisent l'interface, il n'y a aucun risque de conflit de fusion. Toutefois, lorsque des modifications en aval des composants AOSP en amont sont apportées, des conflits de fusion peuvent survenir et les stratégies suivantes sont recommandées:

  • les ajouts d'interface peuvent être intégrés en amont vers AOSP dans la prochaine version.
  • des ajouts d'interface qui offrent une plus grande flexibilité, sans conflits de fusion, peuvent être ajoutés en amont dans la prochaine version.

Parcelables de l'extension: ParcelableHolder

ParcelableHolder est un Parcelable qui peut contenir un autre Parcelable. Le principal cas d'utilisation de ParcelableHolder consiste à rendre Parcelable extensible. Par exemple, une image que les responsables de la mise en œuvre d'appareils s'attendent à pouvoir étendre un Parcelable défini par AOSP, AospDefinedParcelable, pour inclure leurs fonctionnalités à valeur ajoutée.

Auparavant, sans ParcelableHolder, les responsables de la mise en œuvre d'appareils ne pouvaient pas modifier une interface AIDL stable définie par AOSP, car l'ajout d'autres champs comportait une erreur:

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

Comme indiqué dans le code précédent, cette pratique est rompue, car les champs ajoutés par l'outil d'implémentation de l'appareil peuvent avoir un conflit lorsque le Parcelable est révisé dans les prochaines versions d'Android.

À l'aide de ParcelableHolder, le propriétaire d'un fragmentable peut définir un point d'extension dans un Parcelable.

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

Les responsables de la mise en œuvre de l'appareil peuvent ensuite définir leur propre Parcelable pour leur extension.

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

Enfin, le nouveau Parcelable peut être associé au Parcelable d'origine avec le 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'instances de serveur HAL AIDL

Par convention, les services AIDL HAL ont un nom d'instance au format $package.$type/$instance. Par exemple, une instance du vibreur HAL 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 de tests VTS, tous les HAL AIDL déclarés doivent être 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 une classe HAL existante de HIDL en AIDL

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

Fonctionnalités hidl2aidl:

  • Créer des fichiers .aidl en fonction des fichiers .hal pour le package donné
  • Créer des règles de compilation pour le package AIDL que vous venez de créer avec tous les backends activés
  • Créer des méthodes de traduction dans les backends Java, CPP et NDK pour la traduction des types HIDL vers les types AIDL
  • Créer 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. Créez l'outil situé dans system/tools/hidl/hidl2aidl.

    Créer 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 des 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 licence et la date appropriées.

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

    Par 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 gérés à résoudre en premier.
    • Les fichiers .aidl générés peuvent contenir des avertissements et des suggestions nécessitant une action. Ces commentaires commencent par //.
    • Profitez-en pour nettoyer le pack et y apporter des améliorations.
    • Vérifiez l'annotation @JavaDerive pour identifier les fonctionnalités dont vous pourriez avoir besoin, telles que toString ou equals.
  4. Ne créez que les cibles dont vous avez besoin.

    • Désactivez les backends qui ne seront pas utilisés. Privilégiez le backend NDK plutôt que le backend CPP. Consultez la section Choisir un environnement d'exécution.
    • Supprimez les bibliothèques de traduction ou tout code généré qui ne sera pas utilisé.
  5. Consultez les différences majeures entre AIDL et HIDL.

    • L'utilisation du Status intégré d'AIDL et des exceptions améliore généralement l'interface et supprime la nécessité 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 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 du type approprié est déjà ajouté (par exemple, android.hardware.foo.IFoo/default serait déjà marqué comme hal_foo_service). Toutefois, si un client de framework accepte 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

Les attributs HAL doivent être ajoutés lorsque nous créons un nouveau type de HAL. Un attribut HAL spécifique peut être associé à plusieurs types de services (chacun pouvant avoir plusieurs instances, comme nous venons de le voir). Pour un HAL, foo, nous avons 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, si le serveur système est un client de ce HAL, cela 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é, nous créons également un domaine tel que hal_foo_default à titre de référence ou des exemples de HAL. Cependant, certains appareils utilisent ces domaines pour leurs propres serveurs. La distinction entre les domaines pour plusieurs serveurs n'a d'importance que si plusieurs serveurs diffusent la même interface et ont besoin d'un ensemble d'autorisations différent dans leurs implémentations. Dans toutes ces macros, hal_foo n'est pas en réalité un objet sepolicy. À la place, ce jeton est utilisé par ces macros pour faire référence au groupe d'attributs associé à une paire client/serveur.

Toutefois, jusqu'à présent, nous n'avons pas associé hal_foo_service et hal_foo (la paire d'attributs de hal_attribute(foo)). 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 accéder au HAL et les processus hal_foo_server peuvent enregistrer le HAL. L'application de ces règles d'enregistrement est effectuée par le gestionnaire de contexte (servicemanager). Notez que les noms des services peuvent ne pas toujours correspondre aux attributs HAL. Par exemple, nous pouvons voir hal_attribute_service(hal_foo, hal_foo2_service). Toutefois, étant donné que cela implique que les services sont toujours utilisés ensemble, nous pouvons envisager de supprimer hal_foo2_service et d'utiliser hal_foo_service pour tous nos contextes de service. La plupart des HAL qui définissent plusieurs hal_attribute_service sont dues au fait que le nom d'attribut HAL d'origine n'est pas assez général et ne peut pas être modifié.

En résumé, voici un exemple d'HAL:

    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 des extensions associées

Une extension peut être associée à n'importe quelle interface de liaison, 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 conforme à vos attentes. Les extensions ne peuvent être définies qu'à partir du processus servant une liaison.

Les extensions associées doivent être utilisées chaque fois qu'une extension modifie les fonctionnalités d'une HAL existante. Lorsqu'une fonctionnalité entièrement nouvelle est nécessaire, ce mécanisme n'a pas besoin d'être utilisé, et une interface d'extension peut être enregistrée directement auprès du gestionnaire de services. Les interfaces d'extension associées sont plus logiques 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 mettre en miroir la hiérarchie d'interface de liaison d'un autre service nécessiterait une tenue de registre complète pour fournir des fonctionnalités équivalentes aux extensions directement associées.

Pour définir une extension sur la liaison, utilisez les API suivantes:

  • Dans le backend du NDK: AIBinder_setExtension
  • Dans le backend Java: android.os.Binder.setExtension
  • Dans le backend CPP: android::Binder::setExtension
  • Dans le backend Rust: binder::Binder::set_extension

Pour obtenir une extension sur une liaison, utilisez les API suivantes:

  • Dans le backend du NDK: AIBinder_getExtension
  • Dans le backend Java: android.os.IBinder.getExtension
  • Dans le backend CPP: android::IBinder::getExtension
  • Dans le 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.

Différences majeures 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 à C++.
  • Toutes les interfaces AIDL ont des é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/NDK et ServiceSpecificException dans le backend Java. Consultez la page Gestion des erreurs.
  • AIDL ne démarre pas automatiquement les pools de threads lorsque des objets de liaison sont envoyés. Ils doivent être démarrés manuellement (voir Gestion des threads).
  • AIDL ne s'annule pas en cas d'erreurs de transport décochées (HIDL Return abandonne en cas d'erreurs non cochées).
  • AIDL ne peut déclarer qu'un seul type par fichier.
  • Les arguments AIDL peuvent être spécifiés comme "in/out/inout" en plus du paramètre de sortie (il n'y a pas de "rappels synchrones").
  • AIDL utilise un 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, des modifications rétrocompatibles sont effectuées. AIDL n'a pas de concept explicite de versions majeures. Ce concept est intégré aux noms de package. Par exemple, AIDL peut utiliser le nom de package bluetooth2.
  • Par défaut, AIDL n'hérite pas de la priorité "Temps réel". La fonction setInheritRt doit être utilisée par liaison pour activer l'héritage de priorité en temps réel.