Control de versiones de la interfaz

HIDL requiere que cada interfaz escrita en HIDL tenga control de versiones. Después de que se publica una interfaz de HAL, se inmoviliza y cualquier cambio adicional se debe realizar en una versión nueva de esa interfaz. Si bien no se puede modificar una interfaz publicada determinada, otra interfaz puede extenderla.

Estructura del código de HIDL

El código de HIDL se organiza 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 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 para todas las interfaces) o de forma local en una interfaz.
  • Interfaces. Como un componente básico de HIDL, una interfaz consiste en 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 con un nombre y una versión, y además 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 solo contiene UDT (todas las UDT a nivel del 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 del control de versiones

Un paquete HIDL (como android.hardware.nfc), después de publicarse para una versión determinada (como 1.0), es inmutable y no se puede cambiar. Las modificaciones en las interfaces del paquete o en sus UDT solo se pueden realizar en otro paquete.

En HIDL, el control de versiones se aplica a nivel del paquete, no a nivel de la 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 secundaria implica que la versión nueva del paquete es retrocompatible con la anterior, y un aumento de versión principal implica que la versión nueva del paquete no es retrocompatible con la anterior.

Conceptualmente, un paquete puede relacionarse con otro de varias maneras:

  • En absoluto.
  • Extensibilidad retrocompatible a nivel del paquete. Esto ocurre para los nuevos uprevs de versión secundaria (próxima revisión incrementada) de un paquete. El paquete nuevo tiene el mismo nombre y la misma versión principal que el paquete anterior, pero una versión secundaria superior. Funcionalmente, el paquete nuevo es un superconjunto del paquete antiguo, lo que significa lo siguiente:
    • Las interfaces de nivel superior del paquete superior están presentes en el paquete nuevo, aunque las interfaces pueden tener métodos nuevos, UDT locales de interfaz nuevas (la extensión a nivel de la interfaz que se describe a continuación) y UDT nuevas en types.hal.
    • También se pueden agregar interfaces nuevas al paquete nuevo.
    • Todos los tipos de datos del paquete superior están presentes en el nuevo paquete y pueden controlarse con los métodos (posiblemente reimplementados) del paquete anterior.
    • También se pueden agregar nuevos tipos de datos para que los usen métodos nuevos de interfaces existentes actualizadas o interfaces nuevas.
  • Extensibilidad retrocompatible a nivel de la interfaz. El paquete nuevo también puede extender el paquete original, ya que consta de interfaces lógicamente separadas que solo proporcionan funcionalidad adicional, y no la principal. Para ello, te recomendamos que hagas lo siguiente:
    • Las interfaces del paquete nuevo deben recurrir a los tipos de datos del paquete anterior.
    • Las interfaces del paquete nuevo pueden extender las interfaces de uno o más paquetes antiguos.
  • Extiende la incompatibilidad con versiones anteriores original. Esta es una actualización de versión principal del paquete y no es necesario que haya ninguna correlación entre ambas. 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 de paquetes anteriores.

Estructuración de la interfaz

Para obtener una interfaz bien estructurada, agregar nuevos tipos de funciones que no forman parte del diseño original debería requerir una modificación de la interfaz HIDL. Por el contrario, si puedes o esperas hacer un cambio en ambos lados de la interfaz que presente una funcionalidad nueva sin cambiar la interfaz en sí, 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 plataformas de API, pero una de las principales es el mecanismo de IPC que usa HIDL para la comunicación entre procesos en el límite system.img/vendor.img.

Requisitos

Todos los datos que se pasan a través de HIDL deben definirse de forma explícita. Para garantizar que una implementación y un cliente puedan seguir trabajando juntos, incluso cuando se compilan por separado o se desarrollan de forma independiente, los datos deben cumplir con los siguientes requisitos:

  • Se pueden describir directamente en HIDL (con enums de structs, etcétera) con nombres y significados semánticos.
  • Se puede describir con un estándar público, como ISO/IEC 7816.
  • Se puede describir con un estándar de hardware o un diseño físico del hardware.
  • Si es necesario, pueden ser datos opacos (como claves públicas, IDs, etcétera).

Si se usan datos opacos, solo un lado de la interfaz de HIDL debe leerlos. 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>, system.img no puede analizar esos datos; solo se pueden pasar a vendor.img para que los interprete. Cuando se pasa 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 deben interpretar deben describirse con exactitud y siguen siendo parte de la interfaz.

Lineamientos

Debes poder escribir una implementación o un cliente de un HAL solo con los archivos .hal (es decir, no deberías tener que consultar la fuente de Android ni los estándares públicos). Te recomendamos que especifiques el comportamiento requerido exacto. Las afirmaciones como "una implementación podría hacer A o B" fomentan que las implementaciones se entrelacen con los clientes con los que se desarrollan.

Diseño del código de HIDL

HIDL incluye paquetes principales y de proveedores.

Las interfaces principales de HIDL son las que especifica Google. Los paquetes a los que pertenecen comienzan con android.hardware. y se nombran por subsistema, posiblemente con niveles anidados de nombres. Por ejemplo, el paquete de NFC se llama android.hardware.nfc y el paquete de la cámara se llama 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 podría 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 origen.

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 un mapeo fijo entre el prefijo del paquete android.hardware. y la ruta de acceso hardware/interfaces/.

Los paquetes no principales (del proveedor) son los que produce el proveedor del SoC o el ODM. El prefijo de los paquetes que no son principales es vendor.$(VENDOR).hardware., en el que $(VENDOR) hace referencia a un proveedor de SoC o OEM/ODM. Esto se asigna a la ruta vendor/$(VENDOR)/interfaces en el árbol (esta asignación también está codificada de forma fija).

Nombres de tipos definidos por el usuario completamente calificados

En HIDL, cada UDT tiene un nombre completamente calificado que consta del nombre de la UDT, el nombre del paquete en el que se define la UDT y la versión del paquete. El nombre completamente calificado solo se usa cuando se declaran instancias del tipo y no cuando se define el tipo en sí. Por ejemplo, supongamos que la versión 1.0 del paquete android.hardware.nfc, define una estructura llamada 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 indica lo siguiente:

struct NfcData {
    vec<uint8_t> data;
};

Cuando declares una instancia de este tipo (ya sea dentro de una estructura de datos o como un parámetro de método), usa el nombre de tipo completamente calificado:

android.hardware.nfc@1.0::NfcData

La sintaxis general es PACKAGE@VERSION::UDT, en la que:

  • PACKAGE es el nombre de un paquete HIDL separado por puntos (p.ej., android.hardware.nfc).
  • VERSION es el formato de versión principal.secundaria del paquete separado por puntos (p.ej., 1.0).
  • UDT es el nombre de una UDT de HIDL separado por puntos. Dado que HIDL admite UDT anidados y las interfaces de HIDL pueden contener UDT (un tipo de declaración anidada), se usan puntos para acceder a los nombres.

Por ejemplo, si se definió la siguiente declaración anidada en el archivo de tipos comunes en la versión 1.0 del paquete android.hardware.example:

// types.hal
package android.hardware.example@1.0;
struct Foo {
    struct Bar {
        // …
    };
    Bar cheers;
};

El nombre completamente calificado 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 completamente calificado de Bar es android.hardware.example@1.0::IQuux.Foo.Bar.

En ambos casos, se puede hacer referencia a Bar solo como Bar dentro del alcance de la declaración de Foo. En el nivel del paquete o de la interfaz, debes hacer referencia a Bar a través de Foo:Foo.Bar, como en la declaración del método doSomething anterior. Como alternativa, puedes declarar el método de forma más detallada de la siguiente manera:

// 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 completamente calificados

Si un UDT es un tipo de enum, cada valor del tipo de enum tiene un nombre completamente calificado que comienza con el nombre completamente calificado del tipo de enum, seguido de dos puntos y, luego, del nombre del valor de enum. Por ejemplo, asumamos que la versión 1.0 del paquete android.hardware.nfc, define un tipo de enumeración NfcStatus:

enum NfcStatus {
    STATUS_OK,
    STATUS_FAILED
};

Cuando se hace referencia a STATUS_OK, el nombre completamente calificado es el siguiente:

android.hardware.nfc@1.0::NfcStatus:STATUS_OK

La sintaxis general es PACKAGE@VERSION::UDT:VALUE, en la que:

  • PACKAGE@VERSION::UDT es el mismo nombre completamente calificado del tipo de enum.
  • VALUE es el nombre del valor.

Reglas de inferencia automática

No es necesario especificar un nombre de UDT completamente calificado. Un nombre de UDT puede omitir de forma segura lo siguiente:

  • El paquete, p.ej., @1.0::IFoo.Type
  • Tanto el paquete como la versión, p.ej., IFoo.Type

HIDL intenta completar el nombre con 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 intentará realizar una búsqueda de nombre local. Ejemplo:

interface Nfc {
    typedef string NfcErrorMessage;
    send(NfcData d) generates (@1.0::NfcStatus s, NfcErrorMessage m);
};

Se busca NfcErrorMessage de forma local y se encuentra el typedef que está por encima. NfcData también se busca de forma local, pero como no se define de forma local, se usan las reglas 2 y 3. @1.0::NfcStatus proporciona una versión, por lo que no se aplica la regla 1.

Regla 2

Si falla la regla 1 y falta un componente del nombre completamente calificado (paquete, versión o paquete y versión), el componente se completa automáticamente con la información del paquete actual. Luego, el compilador de HIDL busca en el archivo actual (y en todas las importaciones) para encontrar el nombre completamente calificado autocompletado. Con 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 de HIDL completa el nombre del paquete y el nombre de la versión del paquete actual para producir el nombre de la UDT completamente calificado android.hardware.nfc@1.0::NfcData. Como el nombre existe en el paquete actual (suponiendo que se haya importado correctamente), se usa para la declaración.

Un nombre en el paquete actual solo se importa si se cumple una de las siguientes condiciones:

  • Se importa de forma explícita con una sentencia import.
  • Se define en types.hal en el paquete actual.

Se sigue el mismo proceso si NfcData se calificó solo con 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 genera una coincidencia (la UDT no está definida en el paquete actual), el compilador de HIDL busca una coincidencia en todos los paquetes importados. En el ejemplo anterior, supongamos que ExtendedNfcData se declara en la versión 1.1 del paquete android.hardware.nfc, que 1.1 importa 1.0 como debería (consulta Extensiones a nivel del paquete) y que la definición solo especifica el nombre de la UDT:

struct ExtendedNfcData {
    NfcData base;
    // … additional members
};

El compilador busca cualquier UDT llamada NfcData y encuentra una en android.hardware.nfc en la versión 1.0, lo que genera una UDT completamente calificada de android.hardware.nfc@1.0::NfcData. Si se encuentra más de una coincidencia para una UDT parcialmente calificada, el compilador de HIDL genera un error.

Ejemplo

Con la regla 2, se prefiere un tipo importado definido en el paquete actual a 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 con la regla 2, pero no se puede encontrar porque bar/1.0/IFooCallback.hal no se importa automáticamente (como lo hace types.hal). Por lo tanto, la regla 3 lo resuelve en android.hardware.foo@1.0::IFooCallback, que se importa a través de import android.hardware.foo@1.0;).

types.hal

Cada paquete de HIDL contiene un archivo types.hal que contiene UDT que se comparten entre todas las interfaces que participan en ese paquete. Los tipos de HIDL siempre son públicos, independientemente de si se declara un UDT en types.hal o dentro de una declaración de interfaz, se puede acceder a estos tipos fuera del alcance en el que se definen. types.hal no está diseñado para describir la API pública de un paquete, sino para alojar UDTs que usan todas las interfaces dentro del paquete. Debido a la naturaleza de HIDL, todos los UDTs son parte de la interfaz.

types.hal consta de UDT y sentencias import. Debido a que types.hal está disponible para cada interfaz del paquete (es una importación implícita), estas sentencias import están a nivel del paquete por definición. Las UDT en types.hal también pueden incorporar UDT y interfaces importadas de esta manera.

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 los siguientes elementos:

  • android.hidl.base@1.0::IBase (de forma implícita)
  • android.hardware.foo@1.0::types (de forma implícita)
  • Todo en android.hardware.bar@1.0 (incluidas todas las interfaces y su types.hal)
  • types.hal de android.hardware.baz@1.0::types (no se importan las interfaces de android.hardware.baz@1.0)
  • IQux.hal y types.hal de android.hardware.qux@1.0
  • Quuz de android.hardware.quuz@1.0 (suponiendo que Quuz se define en types.hal, se analiza todo el archivo types.hal, pero no se importan los tipos que no sean Quuz).

Control de versiones a nivel de la 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 con la sentencia package. Después de la declaración del paquete, se pueden enumerar cero o más importaciones a nivel de la interfaz (parciales o de todo el paquete). Por ejemplo:

package android.hardware.nfc@1.0;

En HIDL, las interfaces pueden heredar de otras interfaces con la palabra clave extends. Para que una interfaz extienda otra, debe tener acceso a ella a través de una sentencia import. El nombre de la interfaz que se extiende (la interfaz base) sigue las reglas de calificación de nombre de tipo que se explicaron anteriormente. Una interfaz solo puede heredar de una interfaz. HIDL no admite la herencia múltiple.

En los siguientes ejemplos de control de versiones de uprev, se usa 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 de Uprev

Para definir un paquete package@major.minor, A o todo B deben ser verdaderos:

Regla A "Is a start minor version": No se deben definir todas las versiones secundarias anteriores, package@major.0, package@major.1, …, package@major.(minor-1).
OR
Regla B

Se cumplen todas las siguientes condiciones:

  1. "La versión menor anterior es válida": package@major.(minor-1) debe definirse y seguir la misma regla A (no se define ninguna de las versiones de package@major.0 a package@major.(minor-2)) o la regla B (si es una versión anterior 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 un package@major.minor::IBar que extienda package@major.(minor-1)::IBaz, donde IBar y IBaz sean 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 IBar con una k más pequeña.

Debido a la regla A:

  • El paquete puede comenzar con cualquier número de versión secundaria (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, pero con una versión principal diferente (por ejemplo, android.hardware.camera.device tiene definidos @1.0 y @3.2; @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, extiende android.hidl.base@1.0::IBase (excepto IBase).

Se deben seguir B.2 y B.3 al mismo tiempo. Por ejemplo, incluso si android.hardware.foo@1.1::IFoo extiende android.hardware.foo@1.0::IFoo para aprobar la regla B.2, si un android.hardware.foo@1.1::IExtBar extiende android.hardware.foo@1.0::IBar, esta aún no es una versión anterior válida.

Interfaces de uprev

Para actualizar android.hardware.example@1.0 (definido anteriormente) a @1.1, haz lo siguiente:

// 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);
}

Este es un import a nivel del paquete de la versión 1.0 de android.hardware.example en types.hal. Si bien no se agregan UDT nuevas en la versión 1.1 del paquete, las referencias a las UDT en la versión 1.0 aún son necesarias, de ahí la importación a nivel del paquete en types.hal. (Se podría haber logrado el mismo efecto con una importación a nivel de la 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 la aclaración porque IQuux se usa para declarar una interfaz y heredar de una interfaz). Como las declaraciones son solo nombres que heredan todos los atributos de paquete y versión en el sitio de la declaración, la aclaración debe estar en el nombre de la interfaz base. También podríamos haber usado la UDT completamente calificada, pero eso habría sido redundante.

La nueva interfaz IQuux no vuelve a declarar el método fromFooToBar() que hereda de @1.0::IQuux; solo enumera el método nuevo que agrega fromBarToFoo(). En HIDL, los métodos heredados no se pueden volver a declarar en las interfaces secundarias, por lo que la interfaz IQuux no puede declarar el método fromFooToBar() de forma explícita.

Convenciones de uprev

A veces, los nombres de las interfaces deben cambiar el nombre de la interfaz que se extiende. Recomendamos que las extensiones de enum, las estructuras y las uniones tengan el mismo nombre que lo que extienden, a menos que sean lo suficientemente diferentes como para justificar un nombre nuevo. 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), se prefiere ese. De lo contrario, debe tener un nombre similar al que extiende. 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 del paquete

El control de versiones de HIDL se realiza a nivel del paquete. Una vez que se publica un paquete, es inmutable (no se puede cambiar su conjunto de interfaces y UDT). Los paquetes se pueden relacionar entre sí de varias maneras, todas las cuales se pueden expresar mediante una combinación de herencia a nivel de la interfaz y compilación de UDT por composición.

Sin embargo, un tipo de relación está definido de forma estricta y se debe aplicar: la herencia retrocompatible a nivel del paquete. En esta situación, el paquete superior es el paquete del que se hereda, y el paquete secundario es el que extiende el superior. Las reglas de herencia retrocompatibles a nivel del paquete son las siguientes:

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

Estas reglas se pueden implementar con la herencia a nivel de la interfaz de HIDL y la composición de UDT, pero requieren conocimiento a nivel de meta para saber que estas relaciones constituyen una extensión de paquete retrocompatible. Este conocimiento se infiere de la siguiente manera:

Si un paquete cumple con este requisito, hidl-gen aplica las reglas de retrocompatibilidad.