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
.
- Arquivo de definição de tipo de dados chamado
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.
- 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
- 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 comoandroid.hardware.bar@1.0::S
e encontrado embar/1.0/types.hal
, porquetypes.hal
é importado automaticamente.IFooCallback
é interpolado comoandroid.hardware.bar@1.0::IFooCallback
usando a regra 2, mas não pode ser encontrado porquebar/1.0/IFooCallback.hal
não é importado automaticamente (comotypes.hal
). Assim, a regra 3 resolve paraandroid.hardware.foo@1.0::IFooCallback
, que é importado porimport 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 otypes.hal
) types.hal
deandroid.hardware.baz@1.0::types
(as interfaces emandroid.hardware.baz@1.0
não são importadas)IQux.hal
etypes.hal
deandroid.hardware.qux@1.0
Quuz
deandroid.hardware.quuz@1.0
(supondo queQuuz
seja definido emtypes.hal
, o arquivotypes.hal
inteiro é analisado, mas tipos diferentes deQuuz
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.
|
---|
Regra B | Todas as afirmações a seguir são verdadeiras:
|
---|
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óriohardware/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:
- Todas as interfaces de nível superior do pacote pai são herdadas por interfaces no pacote filho.
- Novas interfaces também podem ser adicionadas ao novo pacote (sem restrições sobre relacionamentos com outras interfaces em outros pacotes).
- 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.