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
.
- 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 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.
- 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
- 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 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 (como é otypes.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 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 seustypes.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
(assumindo queQuuz
está definido emtypes.hal
, todo o arquivotypes.hal
é analisado, mas tipos diferentes deQuuz
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. |
---|
Regra B | Todas as afirmativas a seguir são verdadeiras:
|
---|
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óriohardware/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:
- 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 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.