Controle de versão

O HIDL exige que todas as interfaces escritas em HIDL sejam versionadas. Depois que uma interface HAL é publicada, ela é congelada e quaisquer alterações adicionais devem ser feitas em uma nova versão dessa interface. Embora uma determinada 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) . HIDL fornece acesso a um conjunto de tipos de dados primitivos que podem ser usados ​​para compor tipos mais complexos através de estruturas, uniões e enumerações. Os UDTs são passados ​​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 bloco de construção básico do HIDL, uma interface consiste em UDT e declarações de métodos. As interfaces também podem herdar de outra interface.
  • Pacotes . Organiza interfaces HIDL relacionadas e os tipos de dados nos quais 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 seu próprio arquivo .hal .

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

Filosofia de versionamento

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

No HIDL, o controle de versão se aplica 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 de patch e os componentes de metadados de compilação. Dentro de um determinado pacote, um aumento de versão secundária implica que a nova versão do pacote é compatível com versões anteriores do pacote antigo e um aumento de versão principal implica que a nova versão do pacote não é compatível com versões anteriores do pacote antigo.

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

  • De jeito nenhum .
  • Extensibilidade compatível com versões anteriores em nível de pacote . Isso ocorre para novos uprevs de versões secundárias (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 superior. Funcionalmente, o novo pacote é um superconjunto do pacote 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, novos UDTs locais de interface (a extensão de nível de interface descrita abaixo) e novos 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 manipulados 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 existentes atualizadas ou por novas interfaces.
  • Extensibilidade compatível com versões anteriores em nível de 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 este efeito, pode ser desejável o seguinte:
    • As interfaces no novo pacote precisam recorrer aos tipos de dados do pacote antigo.
    • As interfaces em novos pacotes podem estender interfaces de um ou mais pacotes antigos.
  • Estenda a incompatibilidade original com versões anteriores . Esta é uma atualização da versão principal do pacote e não precisa haver qualquer correlação entre os dois. Na medida em que existe, pode ser expresso com uma combinação de tipos da versão mais antiga do pacote e herança de um subconjunto de interfaces de pacotes antigos.

Estruturação de interfaces

Para uma interface bem estruturada, adicionar novos tipos de funcionalidades que não fazem parte do design original deve exigir uma modificação na interface HIDL. Por outro lado, se você pode ou espera fazer uma alteração em ambos os lados da interface que introduza novas funcionalidades sem alterar a interface em si, então a interface não está estruturada.

Treble oferece suporte a componentes de sistema e fornecedor compilados separadamente, nos quais o vendor.img em um dispositivo e o system.img podem ser compilados separadamente. Todas as interações entre vendor.img e system.img devem ser definidas de forma explícita e completa para que possam continuar a funcionar por muitos anos. Isso inclui muitas superfícies de API, mas uma superfície importante é o mecanismo IPC que o HIDL usa para comunicação entre processos no limite system.img / vendor.img .

Requisitos

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

  • Pode ser descrito diretamente em HIDL (usando enums de estruturas, etc.) com nomes e significados semânticos.
  • Pode ser descrito por um padrão público como 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 forem usados ​​dados opacos, eles deverã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 ; ele só pode ser repassado ao vendor.img para interpretação. Ao passar um valor de vendor.img para o código do fornecedor em system.img ou para outro dispositivo, o formato dos dados e como eles devem ser interpretados devem ser descritos com exatidão e ainda fazem parte da interface.

Diretrizes

Você deve ser capaz de escrever uma implementação ou cliente de um HAL usando apenas os arquivos .hal (ou seja, você não precisa consultar a fonte 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 interligarem com os clientes com os quais são desenvolvidas.

Layout de código HIDL

HIDL inclui pacotes principais e de fornecedores.

As interfaces HIDL principais são aquelas especificadas pelo Google. Os pacotes aos quais pertencem começam com android.hardware. e são nomeados por subsistema, potencialmente com níveis aninhados de nomenclatura. Por exemplo, o pacote NFC é denominado 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 possuem uma versão além do nome. Por exemplo, o pacote android.hardware.camera pode estar na versão 3.4 ; isso é importante, pois a versão de um pacote afeta seu posicionamento na árvore de origem.

Todos os pacotes principais são colocados em hardware/interfaces/ no sistema de compilação. 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/. Existe um mapeamento codificado entre o prefixo do pacote android.hardware. e o caminho hardware/interfaces/ .

Pacotes não essenciais (fornecedor) são aqueles produzidos pelo fornecedor SoC ou ODM. O prefixo para pacotes não essenciais é vendor.$(VENDOR).hardware. onde $(VENDOR) refere-se a um fornecedor de SoC ou OEM/ODM. Isso mapeia para o caminho vendor/$(VENDOR)/interfaces na árvore (esse mapeamento também é codificado).

Nomes de tipos definidos pelo usuário totalmente qualificados

No HIDL, todo UDT possui um nome totalmente qualificado que consiste no nome do UDT, no nome do pacote onde o UDT está definido e na versão do pacote. O nome totalmente qualificado é usado somente quando instâncias do tipo são declaradas e não onde o próprio tipo é definido. Por exemplo, suponha que o pacote android.hardware.nfc, versão 1.0 , defina uma estrutura chamada NfcData . No local da declaração (seja em types.hal ou dentro da declaração de uma interface), a declaração simplesmente afirma:

struct NfcData {
    vec<uint8_t> data;
};

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

android.hardware.nfc@1.0::NfcData

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

  • PACKAGE é o nome separado por pontos de um pacote HIDL (por exemplo, android.hardware.nfc ).
  • VERSION é o formato da versão principal.minor separada por pontos do pacote (por exemplo, 1.0 ).
  • UDT é o nome separado por pontos de um UDT HIDL. Como o HIDL suporta UDTs aninhados e as interfaces HIDL podem conter UDTs (um tipo de declaração aninhada), os pontos são usados ​​para acessar os nomes.

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

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

O nome totalmente qualificado para 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 para Bar é android.hardware.example@1.0::IQuux.Foo.Bar .

Em ambos os casos, Bar pode ser referido como Bar apenas no âmbito da declaração de Foo . No nível do pacote ou interface, você deve se referir a Bar via Foo : Foo.Bar , como na declaração do método doSomething acima. Alternativamente, você poderia declarar o método de forma mais detalhada como:

// 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 enum, cada valor do tipo enum terá um nome completo que começa com o nome completo do tipo enum, seguido por dois pontos e, em seguida, seguido pelo nome do valor enum. Por exemplo, suponha que o pacote android.hardware.nfc, versão 1.0 defina um tipo de enum NfcStatus :

enum NfcStatus {
    STATUS_OK,
    STATUS_FAILED
};

Ao referir-se a STATUS_OK , o nome completo é:

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

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

  • PACKAGE @ VERSION :: UDT é exatamente o mesmo nome totalmente qualificado para o tipo enum.
  • VALUE é o nome do valor.

Regras de autoinferência

Um nome UDT totalmente qualificado não precisa ser especificado. Um nome UDT pode omitir com segurança o seguinte:

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

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

Regra 1

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

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

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

Regra 2

Se a regra 1 falhar e um componente com nome completo estiver faltando (pacote, versão ou pacote e versão), o componente será preenchido automaticamente com informações do pacote atual. O compilador HIDL então 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 foi feita no mesmo pacote ( android.hardware.nfc ) na mesma versão ( 1.0 ) de NfcData , conforme segue:

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 UDT totalmente qualificado android.hardware.nfc@1.0::NfcData . Como o nome existe no pacote atual (assumindo que foi importado corretamente), ele é usado para a declaração.

Um nome no pacote atual será importado somente 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 (o UDT não está definido no pacote atual), o compilador HIDL procura 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 importa 1.0 como deveria (consulte Extensões em nível de pacote ) e a definição especifica apenas o nome UDT:

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

O compilador procura qualquer UDT chamado 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 é favorecido em relação a 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 via import android.hardware.foo@1.0; ).

tipos.hal

Cada pacote HIDL contém um arquivo types.hal contendo UDTs que são 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 dentro de uma declaração de interface, esses tipos são acessíveis fora do escopo onde estão definidos. types.hal não se destina a descrever a API pública de um pacote, mas sim a hospedar UDTs usados ​​por todas as interfaces dentro do 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 no nível do pacote por definição. UDTs em types.hal também podem incorporar UDTs e interfaces assim 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;

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 seus types.hal )
  • types.hal de android.hardware.baz@1.0::types (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 (assumindo que Quuz está definido em types.hal , todo o arquivo types.hal é analisado, mas tipos diferentes de Quuz não são importados).

Versionamento em nível de interface

Cada interface dentro de um pacote reside em seu próprio arquivo. O pacote ao qual a interface pertence é declarado na parte superior da interface usando a instrução package . Após a declaração do pacote, zero ou mais importações em nível de interface (pacote parcial ou completo) podem ser listadas. Por 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 interface, ela deve ter acesso a ela por meio de uma instrução import . O nome da interface que está sendo estendida (a interface base) segue as regras para qualificação de nome de tipo explicadas acima. Uma interface pode herdar apenas de uma interface; HIDL não oferece suporte a herança múltipla.

Os exemplos de versionamento uprev 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 atualização

Para definir um pacote package@major.minor , A ou todos os B devem ser verdadeiros:

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 devem 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) deve ser definido e seguir a mesma regra A (nenhuma de package@major.0 até package@major.(minor-2) está definida) ou regra B (se for um uprev 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 deve existir package@major.minor::IBar que estenda package@major.(minor-1)::IBaz , onde IBar e IBaz são dois nomes diferentes. Se houver uma interface com o mesmo nome, package@major.minor::IBar deve estender package@major.(minor-k)::IBar de modo que não exista IBar com um k menor.

Por causa da regra A:

  • O pacote pode começar com qualquer número de versão secundário (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 deveria existir.

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 .

Desde que o nome do pacote seja diferente, package@major.minor::IBar pode se estender de uma interface com um nome diferente (por exemplo, android.hardware.bar@1.0::IBar pode estender android.hardware.baz@2.2::IBaz ). Se uma interface não declarar explicitamente um supertipo com a palavra-chave extend , ela estenderá android.hidl.base@1.0::IBase (exceto o próprio IBase ).

B.2 e B.3 devem ser seguidos ao mesmo tempo. Por exemplo, mesmo que android.hardware.foo@1.1::IFoo estenda android.hardware.foo@1.0::IFoo para passar a regra B.2, se um android.hardware.foo@1.1::IExtBar estender android.hardware.foo@1.0::IBar , este ainda não é um uprev válido.

Atualização de interfaces

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

Esta é uma import em nível de pacote da versão 1.0 de android.hardware.example em types.hal . Embora nenhum novo UDT tenha sido adicionado na versão 1.1 do pacote, as referências aos UDT na versão 1.0 ainda são necessárias, daí a importação no nível do pacote em types.hal . (O mesmo efeito poderia ter sido alcançado com uma importação em nível de 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 para herdar de uma interface). 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 deve estar no nome da interface base; poderíamos ter usado também a UDT totalmente qualificada, mas isso teria sido redundante.

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

Convenções Uprev

Às vezes, os nomes das interfaces devem renomear a interface de extensão. Recomendamos que extensões de enum, estruturas e uniões tenham o mesmo nome que 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 ), então isso será preferido. Caso contrário, deve ser nomeado de forma semelhante ao que está estendendo. 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 em nível de pacote

O versionamento HIDL ocorre no nível do pacote; após a publicação de um pacote, ele é imutável (seu conjunto de interfaces e UDTs não pode ser alterado). Os pacotes podem se relacionar entre si de diversas maneiras, todas elas expressáveis ​​por meio de uma combinação de herança em nível de interface e construção de UDTs por composição.

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

  1. Todas as interfaces de nível superior do pacote pai são herdadas pelas interfaces do 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 existentes atualizadas ou por novas interfaces.

Essas regras podem ser implementadas usando herança em nível de interface HIDL e composição UDT, mas requerem conhecimento de meta-nível para saber que esses relacionamentos constituem uma extensão de pacote compatível com versões anteriores. Esse conhecimento é inferido da seguinte forma:

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