HIDL exige que chaque interface écrite en HIDL soit versionnée. Une fois qu'une interface HAL est publiée, elle est gelée et toute autre modification doit être apportée à une nouvelle version de cette interface. Si une interface publiée donnée ne peut pas être modifiée, elle peut être étendue par une autre interface.
Structure du code HIDL
Le code HIDL est organisé en types, interfaces et packages définis par l'utilisateur :
- Types définis par l'utilisateur (UDT) . HIDL donne accès à un ensemble de types de données primitifs qui peuvent être utilisés pour composer des types plus complexes via des structures, des unions et des énumérations . Les UDT sont passés aux méthodes des interfaces et peuvent être définis au niveau d'un package (commun à toutes les interfaces) ou localement à une interface.
- Interfaces . En tant que bloc de construction de base de HIDL, une interface se compose d'UDT et de déclarations de méthode. Les interfaces peuvent également hériter d'une autre interface.
- Forfaits . Organise les interfaces HIDL associées et les types de données sur lesquels elles opèrent. Un package est identifié par un nom et une version et comprend les éléments suivants :
- Fichier de définition de type de données appelé
types.hal
. - Zéro ou plusieurs interfaces, chacune dans son propre fichier
.hal
.
- Fichier de définition de type de données appelé
Le fichier de définition de type de données types.hal
contient uniquement des UDT (tous les UDT au niveau du package sont conservés dans un seul fichier). Les représentations dans la langue cible sont disponibles pour toutes les interfaces du package.
Philosophie de gestion des versions
Un package HIDL (tel que android.hardware.nfc
), après avoir été publié pour une version donnée (telle que 1.0
), est immuable ; il ne peut pas être modifié. Les modifications des interfaces dans le package ou toute modification de ses UDT ne peuvent avoir lieu que dans un autre package.
Dans HIDL, la gestion des versions s'applique au niveau du package, pas au niveau de l'interface, et toutes les interfaces et UDT d'un package partagent la même version. Les versions de package suivent la version sémantique sans les composants de niveau de correctif et de métadonnées de construction. Dans un package donné, un changement de version mineur implique que la nouvelle version du package est rétrocompatible avec l'ancien package et un changement de version majeure implique que la nouvelle version du package n'est pas rétrocompatible avec l'ancien package.
Conceptuellement, un package peut être lié à un autre package de plusieurs manières :
- Pas du tout .
- Extensibilité rétrocompatible au niveau du package . Cela se produit pour les nouvelles uprevs de version mineure (prochaine révision incrémentée) d'un paquet ; le nouveau package a le même nom et la même version majeure que l'ancien package, mais une version mineure supérieure. Fonctionnellement, le nouveau package est un sur-ensemble de l'ancien package, ce qui signifie :
- Les interfaces de niveau supérieur du package parent sont présentes dans le nouveau package, bien que les interfaces puissent avoir de nouvelles méthodes, de nouveaux UDT locaux d'interface (l'extension au niveau de l'interface décrite ci-dessous) et de nouveaux UDT dans
types.hal
. - De nouvelles interfaces peuvent également être ajoutées au nouveau package.
- Tous les types de données du package parent sont présents dans le nouveau package et peuvent être gérés par les méthodes (éventuellement réimplémentées) de l'ancien package.
- De nouveaux types de données peuvent également être ajoutés pour être utilisés soit par de nouvelles méthodes d'interfaces existantes mises à jour, soit par de nouvelles interfaces.
- Les interfaces de niveau supérieur du package parent sont présentes dans le nouveau package, bien que les interfaces puissent avoir de nouvelles méthodes, de nouveaux UDT locaux d'interface (l'extension au niveau de l'interface décrite ci-dessous) et de nouveaux UDT dans
- Extensibilité rétrocompatible au niveau de l'interface . Le nouveau package peut également étendre le package d'origine en se composant d'interfaces logiquement séparées qui fournissent simplement des fonctionnalités supplémentaires, et non celles de base. A cet effet, les éléments suivants peuvent être souhaitables :
- Les interfaces du nouveau package doivent recourir aux types de données de l'ancien package.
- Les interfaces du nouveau package peuvent étendre les interfaces d'un ou plusieurs anciens packages.
- Prolongez l'incompatibilité descendante d'origine . Il s'agit d'une version majeure du package et il n'est pas nécessaire qu'il y ait de corrélation entre les deux. Dans la mesure du possible, il peut être exprimé avec une combinaison de types de l'ancienne version du package et l'héritage d'un sous-ensemble d'interfaces de l'ancien package.
Interfaces structurantes
Pour une interface bien structurée, l'ajout de nouveaux types de fonctionnalités qui ne font pas partie de la conception d'origine devrait nécessiter une modification de l'interface HIDL. Inversement, si vous pouvez ou prévoyez d'apporter une modification des deux côtés de l'interface qui introduit de nouvelles fonctionnalités sans modifier l'interface elle-même, l'interface n'est pas structurée.
Treble prend en charge les composants fournisseur et système compilés séparément dans lesquels le vendor.img
sur un périphérique et le system.img
peuvent être compilés séparément. Toutes les interactions entre vendor.img
et system.img
doivent être explicitement et complètement définies afin qu'elles puissent continuer à fonctionner pendant de nombreuses années. Cela inclut de nombreuses surfaces API, mais une surface majeure est le mécanisme IPC que HIDL utilise pour la communication interprocessus sur la frontière system.img
/ vendor.img
.
Conditions
Toutes les données transmises via HIDL doivent être explicitement définies. Pour garantir qu'une implémentation et un client peuvent continuer à travailler ensemble même lorsqu'ils sont compilés séparément ou développés indépendamment, les données doivent respecter les exigences suivantes :
- Peut être décrit directement en HIDL (en utilisant des structs enums, etc.) avec des noms sémantiques et une signification.
- Peut être décrit par une norme publique telle que ISO/IEC 7816.
- Peut être décrit par une norme matérielle ou une disposition physique du matériel.
- Peut être des données opaques (telles que des clés publiques, des identifiants, etc.) si nécessaire.
Si des données opaques sont utilisées, elles doivent être lues uniquement par un côté de l'interface HIDL. Par exemple, si le code de vendor.img
donne à un composant sur le system.img
un message de chaîne ou des données vec<uint8_t>
, ces données ne peuvent pas être analysées par le system.img
lui-même ; il ne peut être renvoyé qu'à vendor.img
pour interprétation. Lors du passage d'une valeur de vendor.img
au code du fournisseur sur system.img
ou à un autre périphérique, le format des données et la façon dont elles doivent être interprétées doivent être décrits avec précision et font toujours partie de l'interface .
Des lignes directrices
Vous devriez être capable d'écrire une implémentation ou un client d'un HAL en utilisant uniquement les fichiers .hal (c'est-à-dire que vous ne devriez pas avoir besoin de regarder la source Android ou les normes publiques). Nous vous recommandons de spécifier le comportement exact requis. Des déclarations telles que "une implémentation peut faire A ou B" encouragent les implémentations à s'entremêler avec les clients avec lesquels elles sont développées.
Disposition du code HIDL
HIDL comprend des packages de base et de fournisseur.
Les interfaces HIDL principales sont celles spécifiées par Google. Les packages auxquels ils appartiennent commencent par android.hardware.
et sont nommés par sous-système, potentiellement avec des niveaux de dénomination imbriqués. Par exemple, le package NFC est nommé android.hardware.nfc
et le package caméra est android.hardware.camera
. En général, un package de base porte le nom android.hardware.
[ name1
].[ name2
]…. Les packages HIDL ont une version en plus de leur nom. Par exemple, le package android.hardware.camera
peut être en version 3.4
; c'est important, car la version d'un paquet affecte son emplacement dans l'arborescence des sources.
Tous les packages de base sont placés sous hardware/interfaces/
dans le système de construction. Le paquet android.hardware.
[ name1
].[ name2
]… à la version $m.$n
se trouve sous hardware/interfaces/name1/name2/
… /$m.$n/
; Le package android.hardware.camera
version 3.4
se trouve dans le répertoire hardware/interfaces/camera/3.4/.
Un mappage codé en dur existe entre le préfixe de package android.hardware.
et le chemin hardware/interfaces/
.
Les packages non essentiels (fournisseur) sont ceux produits par le fournisseur SoC ou ODM. Le préfixe des packages non essentiels est vendor.$(VENDOR).hardware.
où $(VENDOR)
fait référence à un fournisseur SoC ou OEM/ODM. Cela correspond au chemin vendor/$(VENDOR)/interfaces
dans l'arborescence (ce mappage est également codé en dur).
Noms complets des types définis par l'utilisateur
Dans HIDL, chaque UDT a un nom complet composé du nom de l'UDT, du nom du package dans lequel l'UDT est défini et de la version du package. Le nom complet est utilisé uniquement lorsque des instances du type sont déclarées et non lorsque le type lui-même est défini. Par exemple, supposons que le package android.hardware.nfc,
version 1.0
définit une structure nommée NfcData
. Sur le site de la déclaration (que ce soit dans types.hal
ou dans la déclaration d'une interface), la déclaration indique simplement :
struct NfcData { vec<uint8_t> data; };
Lors de la déclaration d'une instance de ce type (que ce soit dans une structure de données ou en tant que paramètre de méthode), utilisez le nom de type complet :
android.hardware.nfc@1.0::NfcData
La syntaxe générale est PACKAGE @ VERSION :: UDT
, où :
-
PACKAGE
est le nom séparé par des points d'un package HIDL (par exemple,android.hardware.nfc
). -
VERSION
est le format de version major.minor séparé par des points du package (par exemple,1.0
). -
UDT
est le nom séparé par des points d'un UDT HIDL. Étant donné que HIDL prend en charge les UDT imbriqués et que les interfaces HIDL peuvent contenir des UDT (un type de déclaration imbriquée), des points sont utilisés pour accéder aux noms.
Par exemple, si la déclaration imbriquée suivante a été définie dans le fichier de types communs du package android.hardware.example
version 1.0
:
// types.hal package android.hardware.example@1.0; struct Foo { struct Bar { // … }; Bar cheers; };
Le nom complet de Bar
est android.hardware.example@1.0::Foo.Bar
. Si, en plus d'être dans le package ci-dessus, la déclaration imbriquée se trouvait dans une interface appelée IQuux
:
// IQuux.hal package android.hardware.example@1.0; interface IQuux { struct Foo { struct Bar { // … }; Bar cheers; }; doSomething(Foo f) generates (Foo.Bar fb); };
Le nom complet de Bar
est android.hardware.example@1.0::IQuux.Foo.Bar
.
Dans les deux cas, Bar
ne peut être appelé Bar
que dans le cadre de la déclaration de Foo
. Au niveau du package ou de l'interface, vous devez vous référer à Bar
via Foo
: Foo.Bar
, comme dans la déclaration de la méthode doSomething
ci-dessus. Alternativement, vous pouvez déclarer la méthode de manière plus détaillée comme suit :
// IQuux.hal doSomething(android.hardware.example@1.0::IQuux.Foo f) generates (android.hardware.example@1.0::IQuux.Foo.Bar fb);
Valeurs d'énumération complètes
Si un UDT est un type enum, chaque valeur du type enum a un nom complet qui commence par le nom complet du type enum, suivi de deux-points, puis suivi du nom de la valeur enum. Par exemple, supposons que le package android.hardware.nfc,
version 1.0
définit un type d'énumération NfcStatus
:
enum NfcStatus { STATUS_OK, STATUS_FAILED };
Lorsque vous faites référence à STATUS_OK
, le nom complet est :
android.hardware.nfc@1.0::NfcStatus:STATUS_OK
La syntaxe générale est PACKAGE @ VERSION :: UDT : VALUE
, où :
-
PACKAGE @ VERSION :: UDT
est exactement le même nom qualifié complet pour le type enum. -
VALUE
est le nom de la valeur.
Règles d'auto-inférence
Un nom UDT complet n'a pas besoin d'être spécifié. Un nom UDT peut omettre en toute sécurité les éléments suivants :
- Le package, par exemple
@1.0::IFoo.Type
- Le package et la version, par exemple
IFoo.Type
HIDL tente de compléter le nom en utilisant des règles d'auto-interférence (un numéro de règle inférieur signifie une priorité plus élevée).
Règle 1
Si aucun package ni aucune version ne sont fournis, une recherche de nom local est tentée. Exemple:
interface Nfc { typedef string NfcErrorMessage; send(NfcData d) generates (@1.0::NfcStatus s, NfcErrorMessage m); };
NfcErrorMessage
est recherché localement et le typedef
au-dessus est trouvé. NfcData
est également recherché localement, mais comme il n'est pas défini localement, les règles 2 et 3 sont utilisées. @1.0::NfcStatus
fournit une version, donc la règle 1 ne s'applique pas.
Règle 2
Si la règle 1 échoue et qu'un composant du nom complet est manquant (package, version ou package et version), le composant est rempli automatiquement avec les informations du package actuel. Le compilateur HIDL recherche ensuite dans le fichier actuel (et toutes les importations) pour trouver le nom complet rempli automatiquement. En utilisant l'exemple ci-dessus, supposons que la déclaration de ExtendedNfcData
a été faite dans le même package ( android.hardware.nfc
) à la même version ( 1.0
) que NfcData
, comme suit :
struct ExtendedNfcData { NfcData base; // … additional members };
Le compilateur HIDL remplit le nom du package et le nom de la version du package actuel pour produire le nom UDT complet android.hardware.nfc@1.0::NfcData
. Comme le nom existe dans le package courant (en supposant qu'il soit correctement importé), il est utilisé pour la déclaration.
Un nom dans le package actuel est importé uniquement si l'une des conditions suivantes est vraie :
- Il est importé explicitement avec une instruction d'
import
. - Il est défini dans
types.hal
dans le package actuel
Le même processus est suivi si NfcData
a été qualifié uniquement par le numéro de version :
struct ExtendedNfcData { // autofill the current package name (android.hardware.nfc) @1.0::NfcData base; // … additional members };
Règle 3
Si la règle 2 ne parvient pas à produire une correspondance (l'UDT n'est pas défini dans le package actuel), le compilateur HIDL recherche une correspondance dans tous les packages importés. En utilisant l'exemple ci-dessus, supposons que ExtendedNfcData
est déclaré dans la version 1.1
du package android.hardware.nfc
, 1.1
importe 1.0
comme il se doit (voir Extensions au niveau du package ) et la définition spécifie uniquement le nom UDT :
struct ExtendedNfcData { NfcData base; // … additional members };
Le compilateur recherche tout UDT nommé NfcData
et en trouve un dans android.hardware.nfc
à la version 1.0
, ce qui donne un UDT complet de android.hardware.nfc@1.0::NfcData
. Si plusieurs correspondances sont trouvées pour un UDT partiellement qualifié donné, le compilateur HIDL renvoie une erreur.
Exemple
En utilisant la règle 2, un type importé défini dans le package actuel est préféré à un type importé d'un autre package :
// hardware/interfaces/foo/1.0/types.hal package android.hardware.foo@1.0; struct S {}; // hardware/interfaces/foo/1.0/IFooCallback.hal package android.hardware.foo@1.0; interface IFooCallback {}; // hardware/interfaces/bar/1.0/types.hal package android.hardware.bar@1.0; typedef string S; // hardware/interfaces/bar/1.0/IFooCallback.hal package android.hardware.bar@1.0; interface IFooCallback {}; // hardware/interfaces/bar/1.0/IBar.hal package android.hardware.bar@1.0; import android.hardware.foo@1.0; interface IBar { baz1(S s); // android.hardware.bar@1.0::S baz2(IFooCallback s); // android.hardware.foo@1.0::IFooCallback };
-
S
est interpolé commeandroid.hardware.bar@1.0::S
, et se trouve dansbar/1.0/types.hal
(cartypes.hal
est automatiquement importé). -
IFooCallback
est interpolé en tant queandroid.hardware.bar@1.0::IFooCallback
en utilisant la règle 2, mais il est introuvable carbar/1.0/IFooCallback.hal
n'est pas importé automatiquement (commetypes.hal
). Ainsi, la règle 3 le résout enandroid.hardware.foo@1.0::IFooCallback
à la place, qui est importé viaimport android.hardware.foo@1.0;
).
types.hal
Chaque package HIDL contient un fichier types.hal
contenant des UDT partagés entre toutes les interfaces participant à ce package. Les types HIDL sont toujours publics ; qu'un UDT soit déclaré dans types.hal
ou dans une déclaration d'interface, ces types sont accessibles en dehors de la portée où ils sont définis. types.hal
n'est pas destiné à décrire l'API publique d'un package, mais plutôt à héberger les UDT utilisés par toutes les interfaces du package. En raison de la nature de HIDL, tous les UDT font partie de l'interface.
types.hal
se compose d'UDT et d'instructions import
. Étant donné que types.hal
est mis à la disposition de chaque interface du package (il s'agit d'une importation implicite), ces instructions import
sont par définition au niveau du package. Les UDT dans types.hal
peuvent également incorporer des UDT et des interfaces ainsi importées.
Par exemple, pour un IFoo.hal
:
package android.hardware.foo@1.0; // whole package import import android.hardware.bar@1.0; // types only import import android.hardware.baz@1.0::types; // partial imports import android.hardware.qux@1.0::IQux.Quux; // partial imports import android.hardware.quuz@1.0::Quuz;
Sont importés :
-
android.hidl.base@1.0::IBase
(implicitement) -
android.hardware.foo@1.0::types
(implicitement) - Tout dans
android.hardware.bar@1.0
(y compris toutes les interfaces et sestypes.hal
) -
types.hal
deandroid.hardware.baz@1.0::types
(les interfaces dansandroid.hardware.baz@1.0
ne sont pas importées) -
IQux.hal
ettypes.hal
deandroid.hardware.qux@1.0
-
Quuz
deandroid.hardware.quuz@1.0
(en supposant queQuuz
est défini danstypes.hal
, l'intégralité du fichiertypes.hal
est analysé, mais les types autres queQuuz
ne sont pas importés).
Gestion des versions au niveau de l'interface
Chaque interface d'un package réside dans son propre fichier. Le package auquel appartient l'interface est déclaré en haut de l'interface à l'aide de l'instruction package
. Après la déclaration du package, zéro ou plusieurs importations au niveau de l'interface (partiel ou package entier) peuvent être répertoriées. Par exemple:
package android.hardware.nfc@1.0;
Dans HIDL, les interfaces peuvent hériter d'autres interfaces à l'aide du mot clé extends
. Pour qu'une interface puisse étendre une autre interface, elle doit y avoir accès via une instruction d' import
. Le nom de l'interface en cours d'extension (l'interface de base) suit les règles de qualification du nom de type expliquées ci-dessus. Une interface ne peut hériter que d'une seule interface ; HIDL ne prend pas en charge l'héritage multiple.
Les exemples de version uprev ci-dessous utilisent le package suivant :
// types.hal package android.hardware.example@1.0 struct Foo { struct Bar { vec<uint32_t> val; }; }; // IQuux.hal package android.hardware.example@1.0 interface IQuux { fromFooToBar(Foo f) generates (Foo.Bar b); }
Règles supérieures
Pour définir un package package@major.minor
, A ou tout B doit être vrai :
Règle A | "Est une version mineure de début": Toutes les versions mineures précédentes, package@major.0 , package@major.1 , …, package@major.(minor-1) ne doivent pas être définies. |
---|
Règle B | Tout ce qui suit est vrai :
|
---|
A cause de la règle A :
- Le package peut commencer par n'importe quel numéro de version mineure (par exemple,
android.hardware.biometrics.fingerprint
commence à@2.1
.) - L'exigence "
android.hardware.foo@1.0
n'est pas défini" signifie que le répertoirehardware/interfaces/foo/1.0
ne devrait même pas exister.
Cependant, la règle A n'affecte pas un package avec le même nom de package mais une version majeure différente (par exemple, android.hardware.camera.device
a à la fois @1.0
et @3.2
définis ; @3.2
n'a pas besoin d'interagir avec @1.0
.) Par conséquent, @3.2::IExtFoo
peut étendre @1.0::IFoo
.
Si le nom du package est différent, package@major.minor::IBar
peut s'étendre à partir d'une interface avec un nom différent (par exemple, android.hardware.bar@1.0::IBar
peut étendre android.hardware.baz@2.2::IBaz
). Si une interface ne déclare pas explicitement un super type avec le mot-clé extend
, elle étendra android.hidl.base@1.0::IBase
(sauf IBase
lui-même).
B.2 et B.3 doivent être suivis en même temps. Par exemple, même si android.hardware.foo@1.1::IFoo
étend android.hardware.foo@1.0::IFoo
pour passer la règle B.2, si android.hardware.foo@1.1::IExtBar
étend android.hardware.foo@1.0::IBar
, ce n'est toujours pas un uprev valide.
Amélioration des interfaces
Pour uprev android.hardware.example@1.0
(défini ci-dessus) à @1.1
:
// types.hal package android.hardware.example@1.1; import android.hardware.example@1.0; // IQuux.hal package android.hardware.example@1.1 interface IQuux extends @1.0::IQuux { fromBarToFoo(Foo.Bar b) generates (Foo f); }
Il s'agit d'une import
au niveau du package de la version 1.0
de android.hardware.example
dans types.hal
. Bien qu'aucun nouvel UDT ne soit ajouté dans la version 1.1
du package, les références aux UDT de la version 1.0
sont toujours nécessaires, d'où l'importation au niveau du package dans types.hal
. (Le même effet aurait pu être obtenu avec une importation au niveau de l'interface dans IQuux.hal
.)
Dans extends @1.0::IQuux
dans la déclaration de IQuux
, nous avons spécifié la version d' IQuux
qui est héritée (la désambiguïsation est requise car IQuux
est utilisé pour déclarer une interface et hériter d'une interface). Comme les déclarations sont simplement des noms qui héritent de tous les attributs de package et de version sur le site de la déclaration, la désambiguïsation doit être dans le nom de l'interface de base ; nous aurions également pu utiliser l'UDT pleinement qualifié, mais cela aurait été redondant.
La nouvelle interface IQuux
ne re-déclare pas la méthode fromFooToBar()
elle hérite de @1.0::IQuux
; il répertorie simplement la nouvelle méthode qu'il ajoute fromBarToFoo()
. Dans HIDL, les méthodes héritées ne peuvent pas être déclarées à nouveau dans les interfaces enfants, de sorte que l'interface IQuux
ne peut pas déclarer explicitement la méthode fromFooToBar()
.
Conventions précédentes
Parfois, les noms d'interface doivent renommer l'interface d'extension. Nous recommandons que les extensions, les structures et les unions enum aient le même nom que ce qu'elles étendent, à moins qu'elles ne soient suffisamment différentes pour justifier un nouveau nom. Exemples:
// in parent hal file enum Brightness : uint32_t { NONE, WHITE }; // in child hal file extending the existing set with additional similar values enum Brightness : @1.0::Brightness { AUTOMATIC }; // extending the existing set with values that require a new, more descriptive name: enum Color : @1.0::Brightness { HW_GREEN, RAINBOW };
Si une méthode peut avoir un nouveau nom sémantique (par exemple fooWithLocation
), alors c'est préférable. Sinon, il doit être nommé de la même manière que ce qu'il étend. Par exemple, la méthode foo_1_1
dans @1.1::IFoo
peut remplacer la fonctionnalité de la méthode foo
dans @1.0::IFoo
s'il n'y a pas de meilleur nom alternatif.
Gestion des versions au niveau du package
La gestion des versions HIDL se produit au niveau du package ; après la publication d'un package, il est immuable (son ensemble d'interfaces et d'UDT ne peut pas être modifié). Les packages peuvent être liés les uns aux autres de plusieurs manières, qui sont toutes exprimables via une combinaison d'héritage au niveau de l'interface et de construction d'UDT par composition.
Cependant, un type de relation est strictement défini et doit être appliqué : Héritage rétrocompatible au niveau du package . Dans ce scénario, le package parent est le package hérité et le package enfant est celui qui étend le parent. Les règles d'héritage rétrocompatibles au niveau du package sont les suivantes :
- Toutes les interfaces de niveau supérieur du package parent sont héritées des interfaces du package enfant.
- De nouvelles interfaces peuvent également être ajoutées au nouveau package (aucune restriction concernant les relations avec d'autres interfaces dans d'autres packages).
- De nouveaux types de données peuvent également être ajoutés pour être utilisés soit par de nouvelles méthodes d'interfaces existantes mises à jour, soit par de nouvelles interfaces.
Ces règles peuvent être implémentées à l'aide de l'héritage au niveau de l'interface HIDL et de la composition UDT, mais nécessitent des connaissances au niveau méta pour savoir que ces relations constituent une extension de package rétrocompatible. Cette connaissance est déduite comme suit :
Si un paquet répond à cette exigence, hidl-gen
applique les règles de rétrocompatibilité.