Versionado

HIDL requiere que cada interfaz escrita en HIDL esté versionada. Una vez publicada una interfaz HAL, se congela y cualquier cambio adicional se debe realizar en una nueva versión de esa interfaz. Si bien una interfaz publicada determinada no se puede modificar, otra interfaz puede ampliarla.

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 proporciona acceso a un conjunto de tipos de datos primitivos que se pueden utilizar para componer tipos más complejos mediante estructuras, uniones y enumeraciones. Los UDT se pasan a métodos de interfaces y se pueden definir a nivel de 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 llamado types.hal .
    • Cero o más interfaces, cada una en su propio archivo .hal .

El archivo de definición de tipo de datos types.hal contiene solo UDT (todos los UDT a nivel de paquete se guardan 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 del paquete o cualquier cambio en sus UDT solo pueden realizarse 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 de los paquetes siguen el control de versiones semántico sin el nivel de parche ni los componentes de metadatos de compilación. Dentro de un paquete determinado, 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 mayor 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:

  • De nada .
  • Extensibilidad compatible con versiones anteriores a nivel de paquete . Esto ocurre para nuevas versiones menores (siguiente revisión incrementada) de un paquete; el nuevo paquete tiene el mismo nombre y la misma versión principal que el paquete anterior, pero una versión secundaria superior. Funcionalmente, el nuevo paquete es un superconjunto del paquete antiguo, 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 de interfaz local (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 su uso mediante nuevos métodos de interfaces existentes actualizadas o mediante nuevas interfaces.
  • 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 proporcionan funcionalidad adicional, y no la principal. Para este fin, puede ser conveniente lo siguiente:
    • Las interfaces del nuevo paquete necesitan recurrir a los tipos de datos del paquete antiguo.
    • Las interfaces en un paquete nuevo pueden ampliar las interfaces de uno o más paquetes antiguos.
  • Ampliar la incompatibilidad con versiones anteriores original . Esta es una versión superior del paquete y no tiene por qué haber 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 funcionalidades que no forman parte del diseño original debería requerir una modificación de la interfaz HIDL. Por el contrario, si puede o espera realizar un cambio en ambos lados de la interfaz que introduzca nuevas funciones 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 en un dispositivo y el system.img se pueden compilar por separado. Todas las interacciones entre vendor.img y system.img deben definirse de forma explícita y exhaustiva para que puedan seguir funcionando durante muchos años. Esto incluye muchas superficies API, pero una superficie importante es el mecanismo IPC que HIDL utiliza 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 el 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 directamente en HIDL (usando enumeraciones de estructuras, etc.) con nombres semánticos y significado.
  • Puede describirse mediante un estándar público como ISO/IEC 7816.
  • Puede describirse mediante un estándar de hardware o un diseño físico del hardware.
  • Pueden ser datos opacos (como claves públicas, identificaciones, etc.) si es necesario.

Si se utilizan datos opacos, deben leerse solo en un lado de la interfaz HIDL. Por ejemplo, si el código 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 el propio system.img ; solo se puede devolver a vendor.img para interpretarlo. Al pasar un valor de vendor.img al código de proveedor en system.img o a otro dispositivo, el formato de los datos y cómo se interpretarán se debe describir exactamente y sigue siendo parte de la interfaz .

Pautas

Debería poder escribir una implementación o un cliente de HAL utilizando solo los archivos .hal (es decir, no debería necesitar consultar la fuente de Android ni los estándares públicos). Recomendamos especificar el comportamiento requerido exacto. Declaraciones como "una implementación puede hacer A o B" alientan a las implementaciones a entrelazarse con los clientes con los que se desarrollan.

Diseño de código HIDL

HIDL incluye paquetes principales y de proveedores.

Las interfaces principales HIDL son las especificadas por Google. Los paquetes a los que pertenecen comienzan con android.hardware. y se nombran por subsistema, potencialmente con niveles de denominación anidados. 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 tener 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 de SoC u ODM. El prefijo para paquetes no principales es vendor.$(VENDOR).hardware. donde $(VENDOR) se refiere 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 totalmente 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 completo se utiliza sólo cuando se declaran instancias del tipo y no cuando se define el tipo en sí. Por ejemplo, supongamos que el paquete android.hardware.nfc, la 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 parámetro de método), utilice 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 del paquete, separados por puntos (por ejemplo, 1.0 ).
  • UDT es el nombre separado por puntos de un UDT HIDL. 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 se definió la siguiente declaración anidada en el archivo de tipos comunes del 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 . A nivel de paquete o interfaz, debes hacer referencia a Bar mediante Foo : Foo.Bar , como en la declaración del método doSomething anterior. Alternativamente, podrías declarar el método de manera más detallada 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, entonces 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 seguido del nombre del valor de enumeración. Por ejemplo, supongamos que el paquete android.hardware.nfc, la 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 completo 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 utilizando reglas de interferencia automática (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 anterior. 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 con el 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) el nombre completo autocompletado. Usando el ejemplo anterior, supongamos 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 importe 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 import .
  • Está definido 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 dentro de todos los paquetes importados. Usando el ejemplo anterior, supongamos que ExtendedNfcData está declarado en la versión 1.1 del paquete android.hardware.nfc , 1.1 importa 1.0 como debería (consulte Extensiones a 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 completamente calificado de android.hardware.nfc@1.0::NfcData . Si se encuentra más de una coincidencia para un UDT parcialmente calificado determinado, el compilador HIDL arroja un error.

Ejemplo

Usando la regla 2, un tipo importado definido en el paquete actual tiene preferencia 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 como android.hardware.bar@1.0::S y se encuentra en bar/1.0/types.hal (porque types.hal se importa automáticamente).
  • IFooCallback se interpola como android.hardware.bar@1.0::IFooCallback usando la regla 2, pero no se puede encontrar porque bar/1.0/IFooCallback.hal no se importa automáticamente (como sucede types.hal ). Por lo tanto, la regla 3 lo resuelve en android.hardware.foo@1.0::IFooCallback , que se importa mediante import 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 son siempre públicos; Independientemente de si un UDT está declarado en types.hal o dentro de una declaración de interfaz, estos tipos son accesibles fuera del ámbito donde 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 son parte de la interfaz.

types.hal consta de UDT y declaraciones import . Debido a que types.hal está disponible para todas las interfaces del paquete (es una importación implícita), estas declaraciones import están a nivel de paquete por definición. Los UDT en types.hal también pueden incorporar UDT e interfaces así importadas.

Por ejemplo, para 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 los siguientes:

  • android.hidl.base@1.0::IBase (implícitamente)
  • android.hardware.foo@1.0::types (implícitamente)
  • Todo lo que hay en android.hardware.bar@1.0 (incluidas todas las interfaces y sus types.hal )
  • types.hal de android.hardware.baz@1.0::types (las interfaces en android.hardware.baz@1.0 no se importan)
  • IQux.hal y types.hal de android.hardware.qux@1.0
  • Quuz de android.hardware.quuz@1.0 (suponiendo que Quuz esté definido en types.hal , se analiza todo el archivo types.hal , pero los tipos distintos de Quuz no se importan).

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 package . Después de la declaración del paquete, pueden aparecer 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 usando la palabra clave extends . Para que una interfaz extienda otra interfaz, debe tener acceso a ella mediante una declaración import . El nombre de la interfaz que se va a ampliar (la interfaz base) sigue las reglas para la calificación de nombre de tipo explicadas anteriormente. Una interfaz puede heredar sólo de una interfaz; HIDL no admite herencia múltiple.

Los ejemplos de versiones uprev siguientes utilizan 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 menor inicial": todas las versiones menores anteriores, package@major.0 , package@major.1 ,…, package@major.(minor-1) no deben estar definidas.
O
Regla B

Todo lo siguiente es cierto:

  1. "La versión menor anterior es válida": package@major.(minor-1) debe estar definido y seguir la misma regla A (ninguno de package@major.0 a package@major.(minor-2) está definido) o la regla B (si es un uprev de @major.(minor-2) );

    Y

  2. "Heredar al menos una interfaz con el mismo nombre": Existe una interfaz package@major.minor::IFoo que extiende package@major.(minor-1)::IFoo (si el paquete anterior tiene una interfaz);

    Y

  3. "No hay interfaz heredada con un nombre diferente": No debe existir package@major.minor::IBar que extienda package@major.(minor-1)::IBaz , donde IBar e IBaz son dos nombres diferentes. Si hay una interfaz con el mismo nombre, package@major.minor::IBar debe extender package@major.(minor-k)::IBar de modo que no exista ninguna IBar con una k más pequeña.

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 directorio hardware/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 extender 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 la propia 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.

Interfaces mejoradas

Para actualizar android.hardware.example@1.0 (definido anteriormente) 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 , de ahí 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 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 utilizado 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, los métodos heredados no pueden volver a declararse 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 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 se prefiere. En caso contrario, deberá denominarse de forma similar a lo 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 HIDL se produce a nivel de paquete; Una vez publicado un paquete, es inmutable (su conjunto de interfaces y UDT no se pueden cambiar). Los paquetes pueden relacionarse entre sí de varias maneras, todas las cuales se pueden expresar mediante 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 extiende el paquete principal. Las reglas de herencia compatibles con versiones anteriores a nivel de paquete son las siguientes:

  1. Todas las interfaces de nivel superior del paquete principal se heredan mediante las interfaces del paquete secundario.
  2. También se pueden agregar nuevas interfaces al nuevo paquete (sin restricciones sobre las relaciones con otras interfaces en otros paquetes).
  3. También se pueden agregar nuevos tipos de datos para su uso mediante nuevos métodos de interfaces existentes actualizadas o mediante nuevas interfaces.

Estas reglas se pueden implementar utilizando la herencia a nivel de interfaz HIDL y la composición UDT, pero requieren conocimiento 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 aplica reglas de compatibilidad con versiones anteriores.