Controle de versão

HIDL requer 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 de 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 por meio de estruturas, uniões e enumerações. 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 para uma interface.
  • Interfaces . Como um bloco de construção básico do HIDL, uma interface consiste em UDT e declarações de método. Interfaces também podem herdar de outra interface.
  • Pacotes . Organiza as 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 versão

Um pacote HIDL (como android.hardware.nfc ), após 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 quaisquer alterações em seus UDTs podem ocorrer apenas 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 a versão semântica 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:

  • Nem um pouco .
  • Extensibilidade compatível com versões anteriores em nível de 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 superior. Funcionalmente, o novo pacote é um superconjunto do pacote antigo, o que significa:
    • 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 de interface local (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 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 este efeito, o seguinte pode ser desejável:
    • As interfaces do novo pacote precisam recorrer aos tipos de dados do pacote antigo.
    • 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 nenhuma correlação entre os dois. Na medida em que existe, 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 de pacotes antigos.

Estruturando interfaces

Para uma interface bem estruturada, adicionar novos tipos de funcionalidades que não fazem parte do projeto original deve exigir uma modificação na interface HIDL. Por outro lado, se você puder ou esperar fazer uma alteração em ambos os lados da interface que introduza novas funcionalidades sem alterar a própria interface, a interface não será estruturada.

O Treble suporta componentes de fornecedor e sistema 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 funcionando 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 passados ​​pelo HIDL devem 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 devem seguir os seguintes requisitos:

  • Pode ser descrito em HIDL diretamente (usando structs enums, etc.) com nomes semânticos e significados.
  • 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 dados opacos forem usados, eles devem ser lidos apenas por um lado da interface HIDL. Por exemplo, se o código vendor.img fornece a um componente no system.img uma mensagem de string ou vec<uint8_t> dados, esses dados não podem ser analisados ​​pelo próprio system.img ; ele só pode ser passado de volta para o vendor.img para interpretar. Ao passar um valor de vendor.img para o código de 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 deve precisar 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 entrelaçarem com os clientes com os quais são desenvolvidas.

Layout de código HIDL

O 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 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, 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 do 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 tipo definido pelo usuário totalmente qualificados

No HIDL, cada UDT tem um nome totalmente qualificado que consiste no nome do UDT, no nome do pacote em que o UDT está definido e na versão do pacote. O nome totalmente qualificado é usado apenas quando as 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 um struct chamado NfcData . No site da declaração (seja em types.hal ou dentro da declaração de uma interface), a declaração simplesmente declara:

struct NfcData {
    vec<uint8_t> data;
};

Ao declarar uma instância desse tipo (seja dentro de uma estrutura de dados ou como um 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 ponto de um pacote HIDL (por exemplo, android.hardware.nfc ).
  • VERSION é o formato de versão major.minor separada por ponto do pacote (por exemplo, 1.0 ).
  • UDT é o nome separado por ponto de um HIDL UDT. 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 de pacote ou interface, você deve se referir a Bar via Foo : Foo.Bar , como na declaração do método doSomething acima. Alternativamente, você pode declarar o método mais detalhadamente 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 totalmente qualificado que começa com o nome totalmente qualificado do tipo enum, seguido por dois-pontos e pelo nome do valor enum. Por exemplo, suponha que o pacote android.hardware.nfc, versão 1.0 , defina um tipo de enumeração 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 é exatamente o mesmo nome totalmente qualificado para o tipo de enumeração.
  • VALUE é o nome do valor.

Regras de inferência automática

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
  • Tanto o pacote quanto a 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, uma pesquisa de nome local será tentada. Exemplo:

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

NfcErrorMessage é procurado localmente e o typedef acima é encontrado. NfcData também é procurado localmente, mas como não é definido localmente, as regras 2 e 3 são usadas. @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 do nome completo estiver ausente (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 todas as importações) para localizar 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 , da seguinte forma:

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 (supondo que seja importado corretamente), ele é usado para a declaração.

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

  • Ele é importado explicitamente com uma instrução de import .
  • É 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 falhar em 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 é o 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 em uma declaração de interface, esses tipos são acessíveis fora do escopo em que são definidos. types.hal não pretende descrever a API pública de um pacote, mas sim 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 de import . Como types.hal é disponibilizado para todas as interfaces do pacote (é uma importação implícita), essas instruções de import são de nível de 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 (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 (assumindo que Quuz está definido em types.hal , todo o arquivo types.hal é analisado, mas tipos diferentes de Quuz não são importados).

Versão 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 de nível de interface (parcial ou pacote inteiro) podem ser listadas. Por exemplo:

package android.hardware.nfc@1.0;

Em HIDL, as interfaces podem herdar de outras interfaces usando a palavra-chave extends . Para uma interface estender outra interface, ela deve ter acesso a ela por meio de uma instrução de 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 suporta herança múltipla.

Os exemplos de versão 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 B deve 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 devem ser definidas.
OU
Regra B

Todas as afirmativas 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 a 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 forma 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 menor (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 de pacote, 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 se android.hardware.foo@1.1::IFoo estender 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 , esta ainda não é uma atualização válida.

Melhorando 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 seja adicionado na versão 1.1 do pacote, as referências a UDTs na versão 1.0 ainda são necessárias, portanto, a importação em nível de pacote em types.hal . (O mesmo efeito poderia ter sido obtido com uma importação de 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 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 ser no nome da interface base; poderíamos ter usado o UDT totalmente qualificado também, mas isso seria redundante.

A nova interface IQuux não declara novamente o método fromFooToBar() herda de @1.0::IQuux ; ele simplesmente lista o novo método que adiciona fromBarToFoo() . Em HIDL, 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 atualização

Às vezes, os nomes de interface devem renomear a interface de extensão. Recomendamos que extensões, structs e uniões enum 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 pode ter um novo nome semântico (por exemplo fooWithLocation ), então é preferível. 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.

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 de várias maneiras, todas expressas 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 no nível do pacote . Nesse cenário, o pacote pai é o pacote que está sendo herdado e o pacote filho é aquele que estende o 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 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 exigem conhecimento de nível meta 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 impõe regras de compatibilidade com versões anteriores.