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
.
- 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 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.
- 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
- 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 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 viaimport 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 seustypes.hal
) -
types.hal
deandroid.hardware.baz@1.0::types
(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
(assumindo queQuuz
está definido emtypes.hal
, todo o arquivotypes.hal
é analisado, mas tipos diferentes deQuuz
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. |
---|
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á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óriohardware/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:
- Todas as interfaces de nível superior do pacote pai são herdadas pelas interfaces do 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 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.