Controle de versões da interface

O HIDL exige que todas as interfaces escritas em HIDL sejam versionadas. Depois que uma interface HAL é publicada, ela é congelada, e qualquer mudança adicional precisa ser feita em uma nova versão dessa interface. Embora uma interface publicada não possa ser modificada, ela pode ser estendida por outra interface.

Estrutura do código HIDL

O código HIDL é organizado em tipos, interfaces e pacotes definidos pelo usuário:

  • Tipos definidos pelo usuário (UDTs). O HIDL fornece acesso a um conjunto de tipos de dados primitivos que podem ser usados para compor tipos mais complexos por meio de estruturas, uniões e enumerações. Os UDTs são transmitidos para métodos de interfaces e podem ser definidos no nível de um pacote (comum a todas as interfaces) ou localmente em uma interface.
  • Interfaces. Como um elemento básico de construção do HIDL, uma interface consiste em declarações de UDT e de método. As interfaces também podem herdar de outra interface.
  • Pacotes. Organiza interfaces HIDL relacionadas e os tipos de dados em que elas operam. Um pacote é identificado por um nome e uma versão e inclui o seguinte:
    • Arquivo de definição de tipo de dados chamado types.hal.
    • Zero ou mais interfaces, cada uma em um arquivo .hal.

O arquivo de definição de tipo de dados types.hal contém apenas UDTs. Todos os UDTs no nível do pacote são mantidos em um único arquivo. As representações no idioma de destino estão disponíveis para todas as interfaces no pacote.

Filosofia de controle de versões

Um pacote HIDL (como android.hardware.nfc), depois de ser publicado para uma determinada versão (como 1.0), é imutável; ele não pode ser alterado. Modificações nas interfaces do pacote ou em qualquer mudança nas UDTs só podem ocorrer em outro pacote.

No HIDL, o controle de versão é aplicado no nível do pacote, não no nível da interface, e todas as interfaces e UDTs em um pacote compartilham a mesma versão. As versões do pacote seguem o versionamento semântico sem o nível do patch e os componentes de metadados de build. Em um pacote específico, um aumento de versão secundária implica que a nova versão do pacote é compatível com a versão anterior, e um aumento de versão principal implica que a nova versão do pacote não é compatível com a versão anterior.

Conceitualmente, um pacote pode se relacionar a outro de várias maneiras:

  • De jeito nenhum.
  • Extensibilidade compatível com versões anteriores no nível do pacote. Isso ocorre para novos uprevs de versão secundária (próxima revisão incrementada) de um pacote. O novo pacote tem o mesmo nome e versão principal do pacote antigo, mas uma versão secundária mais alta. Funcionalmente, o novo pacote é um superconjunto do antigo, ou seja:
    • As interfaces de nível superior do pacote pai estão presentes no novo pacote, embora as interfaces possam ter novos métodos, novas UDTs locais da interface (a extensão de nível de interface descrita abaixo) e novas UDTs em types.hal.
    • Novas interfaces também podem ser adicionadas ao novo pacote.
    • Todos os tipos de dados do pacote pai estão presentes no novo pacote e podem ser processados pelos métodos (possivelmente reimplementados) do pacote antigo.
    • Novos tipos de dados também podem ser adicionados para uso por novos métodos de interfaces atualizadas ou por novas interfaces.
  • Extensibilidade compatível com versões anteriores no nível da interface. O novo pacote também pode estender o pacote original, consistindo em interfaces logicamente separadas que simplesmente fornecem funcionalidade adicional, e não a principal. Para isso, é recomendável:
    • As interfaces no novo pacote precisam recorrer aos tipos de dados do pacote antigo.
    • As interfaces em um novo pacote podem estender interfaces de um ou mais pacotes antigos.
  • Ampliar a incompatibilidade com versões anteriores original. Esta é uma versão principal anterior do pacote, e não precisa haver nenhuma correlação entre as duas. Se houver, ele pode ser expresso com uma combinação de tipos da versão mais antiga do pacote e herança de um subconjunto de interfaces do pacote antigo.

Estruturação da interface

Para uma interface bem estruturada, adicionar novos tipos de funcionalidade que não fazem parte do design original exige uma modificação na interface HIDL. Por outro lado, se você puder ou esperar fazer uma mudança em ambos os lados da interface que introduza novas funcionalidades sem mudar a própria interface, então ela não está estruturada.

O Treble oferece suporte a componentes do sistema e do fornecedor compilados separadamente, em que o vendor.img em um dispositivo e o system.img podem ser compilados separadamente. Todas as interações entre vendor.img e system.img precisam ser definidas de forma explícita e completa para que possam continuar funcionando por muitos anos. Isso inclui muitas plataformas de API, mas uma plataforma principal é o mecanismo de IPC que a HIDL usa para comunicação entre processos na fronteira system.img/vendor.img.

Requisitos

Todos os dados transmitidos pelo HIDL precisam ser definidos explicitamente. Para garantir que uma implementação e um cliente possam continuar trabalhando juntos, mesmo quando compilados separadamente ou desenvolvidos de forma independente, os dados precisam obedecer aos seguintes requisitos:

  • Podem ser descritos diretamente no HIDL (usando enums de structs etc.) com nomes e significados semânticos.
  • Pode ser descrito por um padrão público, como o ISO/IEC 7816.
  • Pode ser descrito por um padrão de hardware ou layout físico de hardware.
  • Podem ser dados opacos (como chaves públicas, IDs etc.), se necessário.

Se dados opacos forem usados, eles precisarão ser lidos apenas por um lado da interface HIDL. Por exemplo, se o código vendor.img fornecer a um componente no system.img uma mensagem de string ou dados vec<uint8_t>, esses dados não poderão ser analisados pelo próprio system.img. Eles só poderão ser transmitidos de volta para vendor.img para interpretação. Ao transmitir um valor de vendor.img para o código do fornecedor em system.img ou para outro dispositivo, o formato dos dados e como ele será interpretado precisam ser descritos exatamente e ainda fazer parte da interface.

Diretrizes

Você precisa ser capaz de escrever uma implementação ou um cliente de um HAL usando apenas os arquivos .hal. Ou seja, não é necessário consultar a origem do Android ou os padrões públicos. Recomendamos especificar o comportamento exato necessário. Declarações como "uma implementação pode fazer A ou B" incentivam as implementações a se tornarem entrelaçadas com os clientes para os quais são desenvolvidas.

Layout do código HIDL

O HIDL inclui pacotes principais e do fornecedor.

As interfaces principais do HIDL são especificadas pelo Google. Os pacotes a que pertencem começam com android.hardware. e são nomeados por subsistema, possivelmente com níveis aninhados de nomenclatura. Por exemplo, o pacote NFC é chamado android.hardware.nfc, e o pacote da câmera é android.hardware.camera. Em geral, um pacote principal tem o nome android.hardware.[name1].[name2]…. Os pacotes HIDL têm uma versão além do nome. Por exemplo, o pacote android.hardware.camera pode estar na versão 3.4. Isso é importante, porque a versão de um pacote afeta a posição dele na árvore de origem.

Todos os pacotes principais são colocados em hardware/interfaces/ no sistema de build. O pacote android.hardware.[name1].[name2]… na versão $m.$n está em hardware/interfaces/name1/name2//$m.$n/; o pacote android.hardware.camera versão 3.4 está no diretório hardware/interfaces/camera/3.4/.. Um mapeamento fixo existe entre o prefixo do pacote android.hardware. e o caminho hardware/interfaces/.

Os pacotes não principais (do fornecedor) são aqueles produzidos pelo fornecedor do SoC ou ODM. O prefixo para pacotes não principais é vendor.$(VENDOR).hardware., em que $(VENDOR) se refere a um fornecedor de SoC ou OEM/ODM. Isso é mapeado para o caminho vendor/$(VENDOR)/interfaces na árvore (esse mapeamento também é codificado).

Nomes de tipo definido pelo usuário totalmente qualificados

No HIDL, cada UDT tem um nome totalmente qualificado que consiste no nome da UDT, no nome do pacote em que a UDT é definida e na versão do pacote. O nome totalmente qualificado é usado apenas quando instâncias do tipo são declaradas e não quando o tipo em si é definido. Por exemplo, suponha que o pacote android.hardware.nfc, versão 1.0 defina uma estrutura chamada NfcData. No site da declaração (seja em types.hal ou na declaração de uma interface), a declaração simplesmente afirma:

struct NfcData {
    vec<uint8_t> data;
};

Ao declarar uma instância desse tipo (em uma estrutura de dados ou como parâmetro de método), use o nome do tipo totalmente qualificado:

android.hardware.nfc@1.0::NfcData

A sintaxe geral é PACKAGE@VERSION::UDT, em que:

  • PACKAGE é o nome separado por ponto de um pacote HIDL (por exemplo, android.hardware.nfc).
  • VERSION é o formato de versão principal.secundária separada por ponto do pacote (por exemplo, 1.0).
  • UDT é o nome separado por ponto de uma UDT HIDL. Como o HIDL oferece suporte a UDTs aninhados e as interfaces HIDL podem conter UDTs (um tipo de declaração aninhada), pontos são usados para acessar os nomes.

Por exemplo, se a declaração aninhada a seguir foi definida no arquivo de tipos comuns na versão 1.0 do pacote android.hardware.example:

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

O nome totalmente qualificado de Bar é android.hardware.example@1.0::Foo.Bar. Se, além de estar no pacote acima, a declaração aninhada estivesse em uma interface chamada IQuux:

// IQuux.hal
package android.hardware.example@1.0;
interface IQuux {
    struct Foo {
        struct Bar {
            // …
        };
        Bar cheers;
    };
    doSomething(Foo f) generates (Foo.Bar fb);
};

O nome totalmente qualificado de Bar é android.hardware.example@1.0::IQuux.Foo.Bar.

Em ambos os casos, Bar pode ser referido como Bar apenas no escopo da declaração de Foo. No nível do pacote ou da interface, é necessário fazer referência a Bar por meio de Foo: Foo.Bar, como na declaração do método doSomething acima. Como alternativa, declare o método de forma mais detalhada:

// IQuux.hal
doSomething(android.hardware.example@1.0::IQuux.Foo f) generates (android.hardware.example@1.0::IQuux.Foo.Bar fb);

Valores de enumeração totalmente qualificados

Se um UDT for um tipo enumerado, cada valor do tipo enumerado terá um nome totalmente qualificado que começa com o nome totalmente qualificado do tipo enumerado, seguido por dois-pontos e pelo nome do valor enumerado. Por exemplo, suponha que o pacote android.hardware.nfc, versão 1.0 define um tipo de enumerado NfcStatus:

enum NfcStatus {
    STATUS_OK,
    STATUS_FAILED
};

Ao se referir a STATUS_OK, o nome totalmente qualificado é:

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

A sintaxe geral é PACKAGE@VERSION::UDT:VALUE, onde:

  • PACKAGE@VERSION::UDT é o mesmo nome totalmente qualificado para o tipo de enumeração.
  • VALUE é o nome do valor.

Regras de inferência automática

Não é necessário especificar um nome de UDT totalmente qualificado. Um nome de UDT pode omitir o seguinte com segurança:

  • O pacote, por exemplo, @1.0::IFoo.Type
  • Pacote e versão, por exemplo, IFoo.Type

O HIDL tenta preencher o nome usando regras de interferência automática (o número de regras mais baixo significa prioridade mais alta).

Regra 1

Se nenhum pacote e versão forem fornecidos, uma pesquisa de nome local será tentada. Exemplo:

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

O NfcErrorMessage é pesquisado localmente, e o typedef acima dele é encontrado. NfcData também é pesquisado localmente, mas, como não é definido localmente, as regras 2 e 3 são usadas. @1.0::NfcStatus fornece uma versão, então a regra 1 não se aplica.

Regra 2

Se a regra 1 falhar e um componente do nome totalmente qualificado estiver ausente (pacote, versão ou pacote e versão), o componente será preenchido automaticamente com informações do pacote atual. O compilador HIDL procura no arquivo atual (e em todas as importações) para encontrar o nome totalmente qualificado preenchido automaticamente. Usando o exemplo acima, suponha que a declaração de ExtendedNfcData tenha sido feita no mesmo pacote (android.hardware.nfc) na mesma versão (1.0) que NfcData, conforme abaixo:

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

O compilador HIDL preenche o nome do pacote e o nome da versão do pacote atual para produzir o nome de UDT totalmente qualificado android.hardware.nfc@1.0::NfcData. Como o nome existe no pacote atual (assumindo que ele foi importado corretamente), ele é usado para a declaração.

Um nome no pacote atual só é importado se uma das seguintes condições for verdadeira:

  • Ele é importado explicitamente com uma instrução import.
  • Está definido em types.hal no pacote atual

O mesmo processo é seguido se NfcData foi qualificado apenas pelo número da versão:

struct ExtendedNfcData {
    // autofill the current package name (android.hardware.nfc)
    @1.0::NfcData base;
    // … additional members
};

Regra 3

Se a regra 2 não produzir uma correspondência (a UDT não estiver definida no pacote atual), o compilador HIDL vai procurar uma correspondência em todos os pacotes importados. Usando o exemplo acima, suponha que ExtendedNfcData seja declarado na versão 1.1 do pacote android.hardware.nfc, 1.1 importe 1.0 como deveria (consulte Extensões no nível do pacote) e a definição especifica apenas o nome do UDT:

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

O compilador procura qualquer UDT com o nome NfcData e encontra um em android.hardware.nfc na versão 1.0, resultando em um UDT totalmente qualificado de android.hardware.nfc@1.0::NfcData. Se mais de uma correspondência for encontrada para um determinado UDT parcialmente qualificado, o compilador HIDL gerará um erro.

Exemplo

Usando a regra 2, um tipo importado definido no pacote atual é preferido em vez de um tipo importado de outro pacote:

// 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 é interpolado como android.hardware.bar@1.0::S e encontrado em bar/1.0/types.hal, porque types.hal é importado automaticamente.
  • IFooCallback é interpolado como android.hardware.bar@1.0::IFooCallback usando a regra 2, mas não pode ser encontrado porque bar/1.0/IFooCallback.hal não é importado automaticamente (como types.hal). Assim, a regra 3 resolve para android.hardware.foo@1.0::IFooCallback, que é importado por import android.hardware.foo@1.0;.

types.hal

Cada pacote HIDL contém um arquivo types.hal com UDTs compartilhados entre todas as interfaces participantes desse pacote. Os tipos HIDL são sempre públicos. Independentemente de um UDT ser declarado em types.hal ou em uma declaração de interface, esses tipos são acessíveis fora do escopo em que são definidos. types.hal não tem como objetivo descrever a API pública de um pacote, mas sim hospedar UDTs usadas por todas as interfaces no pacote. Devido à natureza do HIDL, todos os UDTs fazem parte da interface.

types.hal consiste em UDTs e instruções import. Como types.hal é disponibilizado para todas as interfaces do pacote (é uma importação implícita), essas instruções import são definidas no nível do pacote. Os UDTs em types.hal também podem incorporar UDTs e interfaces importadas.

Por exemplo, para um 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;

Os seguintes itens são importados:

  • android.hidl.base@1.0::IBase (implicitamente)
  • android.hardware.foo@1.0::types (implicitamente)
  • Tudo em android.hardware.bar@1.0 (incluindo todas as interfaces e o types.hal)
  • types.hal de android.hardware.baz@1.0::types (as interfaces em android.hardware.baz@1.0 não são importadas)
  • IQux.hal e types.hal de android.hardware.qux@1.0
  • Quuz de android.hardware.quuz@1.0 (supondo que Quuz seja definido em types.hal, o arquivo types.hal inteiro é analisado, mas tipos diferentes de Quuz não são importados).

Controle de versões no nível da interface

Cada interface em um pacote fica em um arquivo próprio. O pacote ao qual a interface pertence é declarado na parte de cima da interface usando a instrução package. Após a declaração do pacote, zero ou mais importações no nível da interface (parcial ou pacote completo) podem ser listadas. Exemplo:

package android.hardware.nfc@1.0;

No HIDL, as interfaces podem herdar de outras interfaces usando a palavra-chave extends. Para que uma interface estenda outra, ela precisa ter acesso a ela por uma instrução import. O nome da interface que está sendo estendida (a interface base) segue as regras de qualificação de tipo e nome explicadas acima. Uma interface pode herdar apenas de uma interface. O HIDL não oferece suporte a herança múltipla.

Os exemplos de controle de versão de atualização abaixo usam o seguinte pacote:

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

Regras de Uprev

Para definir um package@major.minor de pacote, A ou todo o B precisa ser verdadeiro:

Regra A "É uma versão secundária inicial": todas as versões secundárias anteriores, package@major.0, package@major.1, …, package@major.(minor-1) não precisam ser definidas.
OU
Regra B

Todas as afirmações a seguir são verdadeiras:

  1. "A versão secundária anterior é válida": package@major.(minor-1) precisa ser definido e seguir a mesma regra A (nenhum de package@major.0 a package@major.(minor-2) está definido) ou a regra B (se for uma versão anterior de @major.(minor-2));

    E

  2. "Herdar pelo menos uma interface com o mesmo nome": existe uma interface package@major.minor::IFoo que estende package@major.(minor-1)::IFoo (se o pacote anterior tiver uma interface);

    E

  3. "Nenhuma interface herdada com um nome diferente": não pode haver package@major.minor::IBar que estenda package@major.(minor-1)::IBaz, em que IBar e IBaz são dois nomes diferentes. Se houver uma interface com o mesmo nome, package@major.minor::IBar precisará estender package@major.(minor-k)::IBar para que nenhuma IBar exista com um k menor.

Por causa da regra A:

  • O pacote pode começar com qualquer número de versão secundária. Por exemplo, android.hardware.biometrics.fingerprint começa em @2.1.
  • O requisito "android.hardware.foo@1.0 não está definido" significa que o diretório hardware/interfaces/foo/1.0 nem sequer existe.

No entanto, a regra A não afeta um pacote com o mesmo nome, mas com uma versão principal diferente. Por exemplo, android.hardware.camera.device tem @1.0 e @3.2 definidos. @3.2 não precisa interagir com @1.0. Portanto, @3.2::IExtFoo pode estender @1.0::IFoo.

Contanto que o nome do pacote seja diferente, package@major.minor::IBar pode ser estendido de uma interface com um nome diferente (por exemplo, android.hardware.bar@1.0::IBar pode ser estendido de android.hardware.baz@2.2::IBaz). Se uma interface não declara explicitamente um supertipo com a palavra-chave extend, ela é estendida de android.hidl.base@1.0::IBase (exceto IBase).

As seções B.2 e B.3 precisam ser seguidas ao mesmo tempo. Por exemplo, mesmo que android.hardware.foo@1.1::IFoo estenda android.hardware.foo@1.0::IFoo para atender à regra B.2, se um android.hardware.foo@1.1::IExtBar estender android.hardware.foo@1.0::IBar, ainda não será uma atualização válida.

Interfaces do uprev

Para atualizar android.hardware.example@1.0 (definido acima) para @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);
}

Este é um import no nível do pacote da versão 1.0 de android.hardware.example em types.hal. Embora nenhuma UDT nova seja adicionada na versão 1.1 do pacote, as referências a UDTs na versão 1.0 ainda são necessárias. Por isso, a importação no nível do pacote em types.hal é necessária. O mesmo efeito poderia ter sido alcançado com uma importação no nível da interface em IQuux.hal.

Em extends @1.0::IQuux na declaração de IQuux, especificamos a versão de IQuux que está sendo herdada. A desambiguação é necessária porque IQuux é usado para declarar uma interface e herdar dela. Como as declarações são simplesmente nomes que herdam todos os atributos de pacote e versão no site da declaração, a desambiguação precisa estar no nome da interface de base. Também poderíamos ter usado o UDT totalmente qualificado, mas isso seria redundante.

A nova interface IQuux não declara novamente o método fromFooToBar() herdado de @1.0::IQuux. Ela simplesmente lista o novo método que adiciona fromBarToFoo(). No HIDL, os métodos herdados não podem ser declarados novamente nas interfaces filhas. Portanto, a interface IQuux não pode declarar o método fromFooToBar() explicitamente.

Convenções de uprev

Às vezes, os nomes das interfaces precisam renomear a interface estendida. Recomendamos que as extensões, structs e uniões de enumeração tenham o mesmo nome que o que elas estendem, a menos que sejam suficientemente diferentes para justificar um novo nome. Exemplos:

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

Se um método puder ter um novo nome semântico (por exemplo, fooWithLocation), esse será o preferido. Caso contrário, ele precisa ser nomeado de forma semelhante ao que está sendo estendido. Por exemplo, o método foo_1_1 em @1.1::IFoo pode substituir a funcionalidade do método foo em @1.0::IFoo se não houver um nome alternativo melhor.

Controle de versão no nível do pacote

A versão HIDL ocorre no nível do pacote. Depois que um pacote é publicado, ele é imutável (o conjunto de interfaces e UDTs não pode ser alterado). Os pacotes podem se relacionar de várias maneiras, todas expressáveis por uma combinação de herança no nível da interface e criação de UDTs por composição.

No entanto, um tipo de relacionamento é estritamente definido e precisa ser aplicado: Herança compatível com versões anteriores no nível do pacote. Nesse cenário, o pacote pai é o que está sendo herdado, e o filho é o que está estendendo o pacote pai. As regras de herança compatíveis com versões anteriores no nível do pacote são as seguintes:

  1. Todas as interfaces de nível superior do pacote pai são herdadas por interfaces no pacote filho.
  2. Novas interfaces também podem ser adicionadas ao novo pacote (sem restrições sobre relacionamentos com outras interfaces em outros pacotes).
  3. Novos tipos de dados também podem ser adicionados para uso por novos métodos de interfaces atualizadas ou por novas interfaces.

Essas regras podem ser implementadas usando a herança no nível da interface e a composição de UDT da HIDL, mas exigem conhecimento no nível meta para saber que essas relações são uma extensão de pacote compatível com versões anteriores. Esse conhecimento é inferido da seguinte maneira:

Se um pacote atender a esse requisito, o hidl-gen vai aplicar regras de compatibilidade com versões anteriores.