HIDL requiere que todas las interfaces escritas en HIDL estén versionadas. Una vez que se publica una interfaz HAL, se congela y se deben realizar cambios adicionales en una nueva versión de esa interfaz. Si bien una determinada interfaz publicada no puede modificarse, puede ampliarse con otra interfaz.
Estructura del código HIDL
El código HIDL está organizado en tipos, interfaces y paquetes definidos por el usuario:
- Tipos definidos por el usuario (UDT) . HIDL brinda acceso a un conjunto de tipos de datos primitivos que se pueden usar para componer tipos más complejos a través de estructuras, uniones y enumeraciones. Los UDT se pasan a los métodos de las interfaces y se pueden definir a nivel de un paquete (común a todas las interfaces) o localmente a una interfaz.
- interfaces Como componente básico de HIDL, una interfaz consta de UDT y declaraciones de métodos. Las interfaces también pueden heredar de otra interfaz.
- paquetes Organiza las interfaces HIDL relacionadas y los tipos de datos en los que operan. Un paquete se identifica por un nombre y una versión e incluye lo siguiente:
- Archivo de definición de tipo de datos denominado
types.hal
. - Cero o más interfaces, cada una en su propio archivo
.hal
.
- Archivo de definición de tipo de datos denominado
El archivo de definición de tipo de datos types.hal
contiene solo UDT (todos los UDT de nivel de paquete se mantienen en un solo archivo). Las representaciones en el idioma de destino están disponibles para todas las interfaces del paquete.
Filosofía de versiones
Un paquete HIDL (como android.hardware.nfc
), después de publicarse para una versión determinada (como 1.0
), es inmutable; no se puede cambiar. Las modificaciones a las interfaces en el paquete o cualquier cambio a sus UDT solo pueden tener lugar en otro paquete.
En HIDL, el control de versiones se aplica a nivel de paquete, no a nivel de interfaz, y todas las interfaces y UDT de un paquete comparten la misma versión. Las versiones del paquete siguen el control de versiones semántico sin el nivel de parche y los componentes de metadatos de compilación. Dentro de un paquete dado, un aumento de versión menor implica que la nueva versión del paquete es compatible con versiones anteriores del paquete anterior y un aumento de versión principal implica que la nueva versión del paquete no es compatible con versiones anteriores del paquete anterior.
Conceptualmente, un paquete puede relacionarse con otro paquete de varias maneras:
- En absoluto
- Extensibilidad compatible con versiones anteriores a nivel de paquete . Esto ocurre para nuevas actualizaciones de versiones secundarias (próxima revisión incrementada) de un paquete; el nuevo paquete tiene el mismo nombre y versión principal que el paquete anterior, pero una versión secundaria superior. Funcionalmente, el nuevo paquete es un superconjunto del paquete anterior, lo que significa:
- Las interfaces de nivel superior del paquete principal están presentes en el nuevo paquete, aunque las interfaces pueden tener nuevos métodos, nuevos UDT locales de interfaz (la extensión de nivel de interfaz que se describe a continuación) y nuevos UDT en
types.hal
. - También se pueden agregar nuevas interfaces al nuevo paquete.
- Todos los tipos de datos del paquete principal están presentes en el nuevo paquete y pueden ser manejados por los métodos (posiblemente reimplementados) del paquete anterior.
- También se pueden agregar nuevos tipos de datos para que los utilicen nuevos métodos de interfaces existentes actualizadas o interfaces nuevas.
- Las interfaces de nivel superior del paquete principal están presentes en el nuevo paquete, aunque las interfaces pueden tener nuevos métodos, nuevos UDT locales de interfaz (la extensión de nivel de interfaz que se describe a continuación) y nuevos UDT en
- Extensibilidad compatible con versiones anteriores a nivel de interfaz . El nuevo paquete también puede ampliar el paquete original al constar de interfaces lógicamente separadas que simplemente brindan funcionalidad adicional, y no la principal. Para este propósito, lo siguiente puede ser deseable:
- Las interfaces del nuevo paquete necesitan recurrir a los tipos de datos del paquete anterior.
- Las interfaces en el paquete nuevo pueden extender las interfaces de uno o más paquetes antiguos.
- Ampliar la incompatibilidad con versiones anteriores original . Esta es una actualización de la versión principal del paquete y no es necesario que haya ninguna correlación entre los dos. En la medida en que exista, se puede expresar con una combinación de tipos de la versión anterior del paquete y la herencia de un subconjunto de interfaces del paquete anterior.
Estructuración de interfaces
Para una interfaz bien estructurada, agregar nuevos tipos de funcionalidad que no son parte del diseño original debería requerir una modificación a la interfaz HIDL. Por el contrario, si puede o espera realizar un cambio en ambos lados de la interfaz que introduce una nueva funcionalidad sin cambiar la interfaz en sí, entonces la interfaz no está estructurada.
Treble admite componentes del sistema y del proveedor compilados por separado en los que el vendor.img
de un dispositivo y el system.img
se pueden compilar por separado. Todas las interacciones entre vendor.img
y system.img
deben definirse explícita y minuciosamente para que puedan seguir funcionando durante muchos años. Esto incluye muchas superficies API, pero una superficie importante es el mecanismo IPC que HIDL usa para la comunicación entre procesos en el límite system.img
/ vendor.img
.
Requisitos
Todos los datos pasados a través de HIDL deben definirse explícitamente. Para garantizar que una implementación y un cliente puedan continuar trabajando juntos incluso cuando se compilan por separado o se desarrollan de forma independiente, los datos deben cumplir con los siguientes requisitos:
- Se puede describir en HIDL directamente (usando enumeraciones de estructuras, etc.) con nombres semánticos y significado.
- Puede ser descrito por un estándar público como ISO/IEC 7816.
- Puede ser descrito por un estándar de hardware o diseño físico de hardware.
- Pueden ser datos opacos (como claves públicas, identificaciones, etc.) si es necesario.
Si se utilizan datos opacos, deben ser leídos solo por un lado de la interfaz HIDL. Por ejemplo, si el código de vendor.img
le da a un componente en system.img
un mensaje de cadena o datos vec<uint8_t>
, esos datos no pueden ser analizados por system.img
mismo; solo se puede devolver a vendor.img
para que lo interprete. Al pasar un valor de vendor.img
al código del proveedor en system.img
o a otro dispositivo, el formato de los datos y cómo se interpretarán deben describirse exactamente y aún forman parte de la interfaz .
Pautas
Debería poder escribir una implementación o un cliente de una HAL usando solo los archivos .hal (es decir, no debería necesitar mirar la fuente de Android o los estándares públicos). Recomendamos especificar el comportamiento requerido exacto. Declaraciones como "una implementación puede hacer A o B" fomentan que las implementaciones se entrelacen con los clientes con los que se desarrollan.
Diseño de código HIDL
HIDL incluye paquetes básicos y de proveedores.
Las interfaces Core HIDL son las especificadas por Google. Los paquetes a los que pertenecen comienzan con android.hardware.
y son nombrados por subsistema, potencialmente con niveles anidados de nombres. Por ejemplo, el paquete NFC se llama android.hardware.nfc
y el paquete de la cámara es android.hardware.camera
. En general, un paquete principal tiene el nombre android.hardware.
[ name1
].[ name2
]…. Los paquetes HIDL tienen una versión además de su nombre. Por ejemplo, el paquete android.hardware.camera
puede estar en la versión 3.4
; esto es importante, ya que la versión de un paquete afecta su ubicación en el árbol de fuentes.
Todos los paquetes principales se colocan en hardware/interfaces/
en el sistema de compilación. El paquete android.hardware.
[ name1
].[ name2
]… en la versión $m.$n
está en hardware/interfaces/name1/name2/
… /$m.$n/
; El paquete android.hardware.camera
versión 3.4
está en el directorio hardware/interfaces/camera/3.4/.
Existe una asignación codificada entre el prefijo del paquete android.hardware.
y la ruta hardware/interfaces/
.
Los paquetes no básicos (proveedor) son aquellos producidos por el proveedor SoC u ODM. El prefijo para los paquetes secundarios es vendor.$(VENDOR).hardware.
donde $(VENDOR)
hace referencia a un proveedor de SoC u OEM/ODM. Esto se asigna a la ruta vendor/$(VENDOR)/interfaces
en el árbol (esta asignación también está codificada).
Nombres de tipo definido por el usuario completamente calificados
En HIDL, cada UDT tiene un nombre completo que consta del nombre del UDT, el nombre del paquete donde se define el UDT y la versión del paquete. El nombre totalmente calificado se usa solo cuando se declaran instancias del tipo y no cuando se define el tipo en sí. Por ejemplo, suponga que el paquete android.hardware.nfc,
versión 1.0
, define una estructura denominada NfcData
. En el sitio de la declaración (ya sea en types.hal
o dentro de la declaración de una interfaz), la declaración simplemente establece:
struct NfcData { vec<uint8_t> data; };
Al declarar una instancia de este tipo (ya sea dentro de una estructura de datos o como un parámetro de método), use el nombre de tipo completo:
android.hardware.nfc@1.0::NfcData
La sintaxis general es PACKAGE @ VERSION :: UDT
, donde:
-
PACKAGE
es el nombre separado por puntos de un paquete HIDL (por ejemplo,android.hardware.nfc
). -
VERSION
es el formato de versión mayor.menor separado por puntos del paquete (por ejemplo,1.0
). -
UDT
es el nombre separado por puntos de un HIDL UDT. Dado que HIDL admite UDT anidados y las interfaces HIDL pueden contener UDT (un tipo de declaración anidada), se utilizan puntos para acceder a los nombres.
Por ejemplo, si la siguiente declaración anidada se definió en el archivo de tipos comunes en el paquete android.hardware.example
versión 1.0
:
// types.hal package android.hardware.example@1.0; struct Foo { struct Bar { // … }; Bar cheers; };
El nombre completo de Bar
es android.hardware.example@1.0::Foo.Bar
. Si, además de estar en el paquete anterior, la declaración anidada estuviera en una interfaz llamada IQuux
:
// IQuux.hal package android.hardware.example@1.0; interface IQuux { struct Foo { struct Bar { // … }; Bar cheers; }; doSomething(Foo f) generates (Foo.Bar fb); };
El nombre completo de Bar
es android.hardware.example@1.0::IQuux.Foo.Bar
.
En ambos casos, se puede hacer referencia a Bar
como Bar
solo dentro del alcance de la declaración de Foo
. En el nivel de paquete o interfaz, debe hacer referencia a Bar
a través de Foo
: Foo.Bar
, como en la declaración del método doSomething
anterior. Alternativamente, podría declarar el método más detalladamente como:
// IQuux.hal doSomething(android.hardware.example@1.0::IQuux.Foo f) generates (android.hardware.example@1.0::IQuux.Foo.Bar fb);
Valores de enumeración totalmente calificados
Si un UDT es un tipo de enumeración, cada valor del tipo de enumeración tiene un nombre completo que comienza con el nombre completo del tipo de enumeración, seguido de dos puntos y luego del nombre del valor de enumeración. Por ejemplo, suponga que el paquete android.hardware.nfc,
versión 1.0
define un tipo de enumeración NfcStatus
:
enum NfcStatus { STATUS_OK, STATUS_FAILED };
Cuando se hace referencia a STATUS_OK
, el nombre completo es:
android.hardware.nfc@1.0::NfcStatus:STATUS_OK
La sintaxis general es PACKAGE @ VERSION :: UDT : VALUE
, donde:
-
PACKAGE @ VERSION :: UDT
es exactamente el mismo nombre completamente calificado para el tipo de enumeración. -
VALUE
es el nombre del valor.
Reglas de autoinferencia
No es necesario especificar un nombre UDT completo. Un nombre UDT puede omitir con seguridad lo siguiente:
- El paquete, por ejemplo
@1.0::IFoo.Type
- Tanto el paquete como la versión, por ejemplo,
IFoo.Type
HIDL intenta completar el nombre usando reglas de autointerferencia (un número de regla más bajo significa una prioridad más alta).
Regla 1
Si no se proporciona ningún paquete ni versión, se intenta realizar una búsqueda de nombre local. Ejemplo:
interface Nfc { typedef string NfcErrorMessage; send(NfcData d) generates (@1.0::NfcStatus s, NfcErrorMessage m); };
NfcErrorMessage
se busca localmente y se encuentra el typedef
arriba. NfcData
también se busca localmente, pero como no está definido localmente, se utilizan las reglas 2 y 3. @1.0::NfcStatus
proporciona una versión, por lo que la regla 1 no se aplica.
Regla 2
Si la regla 1 falla y falta un componente del nombre completo (paquete, versión o paquete y versión), el componente se completa automáticamente con información del paquete actual. Luego, el compilador HIDL busca en el archivo actual (y en todas las importaciones) para encontrar el nombre completo autocompletado. Usando el ejemplo anterior, suponga que la declaración de ExtendedNfcData
se realizó en el mismo paquete ( android.hardware.nfc
) en la misma versión ( 1.0
) que NfcData
, de la siguiente manera:
struct ExtendedNfcData { NfcData base; // … additional members };
El compilador HIDL completa el nombre del paquete y el nombre de la versión del paquete actual para generar el nombre UDT completo android.hardware.nfc@1.0::NfcData
. Como el nombre existe en el paquete actual (suponiendo que se haya importado correctamente), se utiliza para la declaración.
Un nombre en el paquete actual se importa solo si se cumple una de las siguientes condiciones:
- Se importa explícitamente con una declaración de
import
. - Se define en
types.hal
en el paquete actual
Se sigue el mismo proceso si NfcData
fue calificado solo por el número de versión:
struct ExtendedNfcData { // autofill the current package name (android.hardware.nfc) @1.0::NfcData base; // … additional members };
regla 3
Si la regla 2 no produce una coincidencia (el UDT no está definido en el paquete actual), el compilador HIDL busca una coincidencia en todos los paquetes importados. Usando el ejemplo anterior, suponga que ExtendedNfcData
se declara en la versión 1.1
del paquete android.hardware.nfc
, 1.1
importa 1.0
como debería (consulte Extensiones de nivel de paquete ) y la definición especifica solo el nombre UDT:
struct ExtendedNfcData { NfcData base; // … additional members };
El compilador busca cualquier UDT llamado NfcData
y encuentra uno en android.hardware.nfc
en la versión 1.0
, lo que da como resultado un UDT totalmente calificado de android.hardware.nfc@1.0::NfcData
. Si se encuentra más de una coincidencia para un UDT parcialmente calificado, el compilador HIDL genera un error.
Ejemplo
Usando la regla 2, se favorece un tipo importado definido en el paquete actual sobre un tipo importado de otro paquete:
// 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
se interpola comoandroid.hardware.bar@1.0::S
y se encuentra enbar/1.0/types.hal
(porquetypes.hal
se importa automáticamente). -
IFooCallback
se interpola comoandroid.hardware.bar@1.0::IFooCallback
usando la regla 2, pero no se puede encontrar porquebar/1.0/IFooCallback.hal
no se importa automáticamente (como setypes.hal
). Por lo tanto, la regla 3 lo resuelve enandroid.hardware.foo@1.0::IFooCallback
, que se importa a través deimport android.hardware.foo@1.0;
).
tipos.hal
Cada paquete HIDL contiene un archivo types.hal
que contiene UDT que se comparten entre todas las interfaces que participan en ese paquete. Los tipos HIDL siempre son públicos; independientemente de si se declara un UDT en types.hal
o dentro de una declaración de interfaz, estos tipos son accesibles fuera del ámbito en el que están definidos. types.hal
no pretende describir la API pública de un paquete, sino alojar los UDT utilizados por todas las interfaces dentro del paquete. Debido a la naturaleza de HIDL, todos los UDT forman parte de la interfaz.
types.hal
consta de UDT y sentencias de import
. Debido a que types.hal
está disponible para todas las interfaces del paquete (es una importación implícita), estas declaraciones de import
están a nivel de paquete por definición. Los UDT en types.hal
también pueden incorporar UDT e interfaces así importados.
Por ejemplo, para 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;
Se importan:
-
android.hidl.base@1.0::IBase
(implícitamente) -
android.hardware.foo@1.0::types
(implícitamente) - Todo en
android.hardware.bar@1.0
(incluidas todas las interfaces y sustypes.hal
) -
types.hal
deandroid.hardware.baz@1.0::types
(las interfaces enandroid.hardware.baz@1.0
no se importan) -
IQux.hal
ytypes.hal
deandroid.hardware.qux@1.0
-
Quuz
deandroid.hardware.quuz@1.0
(suponiendo queQuuz
esté definido entypes.hal
, se analiza todo el archivotypes.hal
, pero no se importan los tipos que no seanQuuz
).
Control de versiones a nivel de interfaz
Cada interfaz dentro de un paquete reside en su propio archivo. El paquete al que pertenece la interfaz se declara en la parte superior de la interfaz mediante la declaración del package
. Después de la declaración del paquete, se pueden enumerar cero o más importaciones a nivel de interfaz (paquete parcial o completo). Por ejemplo:
package android.hardware.nfc@1.0;
En HIDL, las interfaces pueden heredar de otras interfaces utilizando la palabra clave extends
. Para que una interfaz amplíe otra interfaz, debe tener acceso a ella a través de una declaración de import
. El nombre de la interfaz que se está extendiendo (la interfaz base) sigue las reglas para la calificación de nombre de tipo explicadas anteriormente. Una interfaz puede heredar solo de una interfaz; HIDL no admite la herencia múltiple.
Los siguientes ejemplos de versiones uprev usan el siguiente paquete:
// 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); }
reglas uprev
Para definir un paquete package@major.minor
, A o todo B debe ser verdadero:
Regla A | "Es una versión secundaria de inicio": todas las versiones secundarias anteriores, package@major.0 , package@major.1 , …, package@major.(minor-1) no deben definirse. |
---|
Regla B | Todo lo siguiente es cierto:
|
---|
Por la regla A:
- El paquete puede comenzar con cualquier número de versión menor (por ejemplo,
android.hardware.biometrics.fingerprint
comienza en@2.1
). - El requisito "
android.hardware.foo@1.0
no está definido" significa que el directoriohardware/interfaces/foo/1.0
ni siquiera debería existir.
Sin embargo, la regla A no afecta a un paquete con el mismo nombre de paquete pero con una versión principal diferente (por ejemplo, android.hardware.camera.device
tiene @1.0
y @3.2
definidos; @3.2
no necesita interactuar con @1.0
.) Por lo tanto, @3.2::IExtFoo
puede extender @1.0::IFoo
.
Siempre que el nombre del paquete sea diferente, package@major.minor::IBar
puede extenderse desde una interfaz con un nombre diferente (por ejemplo, android.hardware.bar@1.0::IBar
puede extenderse a android.hardware.baz@2.2::IBaz
). Si una interfaz no declara explícitamente un supertipo con la palabra clave extend
, extenderá android.hidl.base@1.0::IBase
(excepto IBase
).
B.2 y B.3 deben seguirse al mismo tiempo. Por ejemplo, incluso si android.hardware.foo@1.1::IFoo
extiende android.hardware.foo@1.0::IFoo
para pasar la regla B.2, si android.hardware.foo@1.1::IExtBar
extiende android.hardware.foo@1.0::IBar
, esto todavía no es un uprev válido.
Actualización de interfaces
Para actualizar android.hardware.example@1.0
(definido arriba) a @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); }
Esta es una import
a nivel de paquete de la versión 1.0
de android.hardware.example
en types.hal
. Si bien no se agregan nuevos UDT en la versión 1.1
del paquete, aún se necesitan referencias a los UDT en la versión 1.0
, por lo tanto, la importación a nivel de paquete en types.hal
. (Se podría haber logrado el mismo efecto con una importación a nivel de interfaz en IQuux.hal
).
En extends @1.0::IQuux
en la declaración de IQuux
, especificamos la versión de IQuux
que se hereda (se requiere desambiguación porque IQuux
se usa para declarar una interfaz y para heredar de una interfaz). Como las declaraciones son simplemente nombres que heredan todos los atributos de paquete y versión en el sitio de la declaración, la desambiguación debe estar en el nombre de la interfaz base; también podríamos haber usado el UDT totalmente calificado, pero eso habría sido redundante.
La nueva interfaz IQuux
no vuelve a declarar el método fromFooToBar()
que hereda de @1.0::IQuux
; simplemente enumera el nuevo método que agrega fromBarToFoo()
. En HIDL, es posible que los métodos heredados no se vuelvan a declarar en las interfaces secundarias, por lo que la interfaz IQuux
no puede declarar explícitamente el método fromFooToBar()
.
convenciones uprev
A veces, los nombres de las interfaces deben cambiar el nombre de la interfaz extendida. Recomendamos que las extensiones, estructuras y uniones de enumeración tengan el mismo nombre que lo que extienden, a menos que sean lo suficientemente diferentes como para justificar un nuevo nombre. Ejemplos:
// 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 un método puede tener un nuevo nombre semántico (por ejemplo, fooWithLocation
), entonces es preferible. De lo contrario, debe tener un nombre similar al que está extendiendo. Por ejemplo, el método foo_1_1
en @1.1::IFoo
puede reemplazar la funcionalidad del método foo
en @1.0::IFoo
si no hay un nombre alternativo mejor.
Control de versiones a nivel de paquete
El control de versiones de HIDL se produce a nivel de paquete; después de que se publica un paquete, es inmutable (su conjunto de interfaces y UDT no se pueden cambiar). Los paquetes se pueden relacionar entre sí de varias maneras, todas las cuales se pueden expresar a través de una combinación de herencia a nivel de interfaz y creación de UDT por composición.
Sin embargo, un tipo de relación está estrictamente definido y debe aplicarse: herencia compatible con versiones anteriores a nivel de paquete . En este escenario, el paquete principal es el paquete del que se hereda y el paquete secundario es el que amplía el principal. Las reglas de herencia compatibles con versiones anteriores a nivel de paquete son las siguientes:
- Todas las interfaces de nivel superior del paquete principal son heredadas por las interfaces del paquete secundario.
- También se pueden agregar nuevas interfaces al nuevo paquete (sin restricciones sobre las relaciones con otras interfaces en otros paquetes).
- También se pueden agregar nuevos tipos de datos para que los utilicen nuevos métodos de interfaces existentes actualizadas o interfaces nuevas.
Estas reglas se pueden implementar mediante la herencia de nivel de interfaz HIDL y la composición UDT, pero requieren conocimientos de metanivel para saber que estas relaciones constituyen una extensión de paquete compatible con versiones anteriores. Este conocimiento se infiere de la siguiente manera:
Si un paquete cumple con este requisito, hidl-gen
hace cumplir las reglas de compatibilidad con versiones anteriores.