HIDL требует, чтобы каждый интерфейс, написанный на HIDL, имел версию. После публикации интерфейса HAL он замораживается, и любые дальнейшие изменения необходимо вносить в новую версию этого интерфейса. Хотя данный опубликованный интерфейс нельзя изменить, его можно расширить с помощью другого интерфейса.
Структура HIDL-кода
Код HIDL организован в определяемые пользователем типы, интерфейсы и пакеты:
- Пользовательские типы (UDT) . HIDL обеспечивает доступ к набору примитивных типов данных, которые можно использовать для создания более сложных типов с помощью структур, объединений и перечислений. UDT передаются методам интерфейсов и могут быть определены на уровне пакета (общий для всех интерфейсов) или локально для интерфейса.
- Интерфейсы . Являясь базовым строительным блоком HIDL, интерфейс состоит из UDT и объявлений методов. Интерфейсы также могут наследовать от другого интерфейса.
- Пакеты . Организует связанные интерфейсы HIDL и типы данных, с которыми они работают. Пакет идентифицируется по имени и версии и включает в себя следующее:
- Файл определения типа данных, называемый
types.hal
. - Ноль или более интерфейсов, каждый в отдельном файле
.hal
.
- Файл определения типа данных, называемый
Файл определения типа types.hal
содержит только UDT (все UDT уровня пакета хранятся в одном файле). Представления на целевом языке доступны для всех интерфейсов пакета.
Философия управления версиями
Пакет HIDL (например, android.hardware.nfc
) после публикации для данной версии (например, 1.0
) является неизменяемым; его нельзя изменить. Модификации интерфейсов в пакете или любые изменения его UDT могут иметь место только в другом пакете.
В HIDL управление версиями применяется на уровне пакета, а не на уровне интерфейса, и все интерфейсы и определяемые пользователем типы в пакете имеют одну и ту же версию. Версии пакета следуют семантическому управлению версиями без уровня исправления и компонентов метаданных сборки. В данном пакете незначительное изменение версии означает, что новая версия пакета обратно совместима со старым пакетом, а увеличение основной версии означает, что новая версия пакета не имеет обратной совместимости со старым пакетом.
Концептуально пакет может быть связан с другим пакетом одним из нескольких способов:
- Нисколько .
- Обратная совместимость на уровне пакета . Это происходит при обновлении новой второстепенной версии (следующей увеличенной версии) пакета; новый пакет имеет то же имя и основную версию, что и старый пакет, но более высокую второстепенную версию. Функционально новый пакет является расширенным набором старого пакета, что означает:
- Интерфейсы верхнего уровня родительского пакета присутствуют в новом пакете, хотя интерфейсы могут иметь новые методы, новые локальные для интерфейса определяемые пользователем типы (расширение уровня интерфейса, описанное ниже) и новые определяемые пользователем типы в
types.hal
. - В новый пакет также можно добавить новые интерфейсы.
- Все типы данных родительского пакета присутствуют в новом пакете и могут обрабатываться (возможно, переопределенными) методами из старого пакета.
- Также можно добавлять новые типы данных для использования либо новыми методами обновленных существующих интерфейсов, либо новыми интерфейсами.
- Интерфейсы верхнего уровня родительского пакета присутствуют в новом пакете, хотя интерфейсы могут иметь новые методы, новые локальные для интерфейса определяемые пользователем типы (расширение уровня интерфейса, описанное ниже) и новые определяемые пользователем типы в
- Обратная совместимость на уровне интерфейса . Новый пакет также может расширить исходный пакет, состоя из логически отдельных интерфейсов, которые просто предоставляют дополнительную функциональность, а не основную. Для этой цели может быть желательно следующее:
- Интерфейсы в новом пакете должны использовать типы данных старого пакета.
- Интерфейсы в новом пакете могут расширять интерфейсы одного или нескольких старых пакетов.
- Расширьте исходную обратную несовместимость . Это обновленная версия пакета, и между ними не должно быть никакой корреляции. В той степени, в которой это возможно, это может быть выражено комбинацией типов из старой версии пакета и наследованием подмножества интерфейсов старого пакета.
Структурирование интерфейса
Для хорошо структурированного интерфейса добавление новых типов функциональности, которые не являются частью исходного дизайна, должно потребовать модификации интерфейса HIDL. И наоборот, если вы можете или ожидаете внести изменения в обе стороны интерфейса, которые представят новую функциональность, не меняя при этом сам интерфейс, тогда интерфейс не структурирован.
Treble поддерживает отдельно скомпилированные компоненты поставщика и системы, в которых vendor.img
на устройстве и system.img
могут быть скомпилированы отдельно. Все взаимодействия vendor.img
и system.img
должны быть явно и тщательно определены, чтобы они могли работать в течение многих лет. Сюда входит множество API-интерфейсов, но основной из них является механизм IPC, который HIDL использует для межпроцессного взаимодействия на границе system.img
/ vendor.img
.
Требования
Все данные, передаваемые через HIDL, должны быть явно определены. Чтобы гарантировать, что реализация и клиент могут продолжать работать вместе, даже если они компилируются отдельно или разрабатываются независимо, данные должны соответствовать следующим требованиям:
- Может быть описано непосредственно в HIDL (с использованием перечислений структур и т. д.) с семантическими именами и значениями.
- Может быть описан общедоступным стандартом, таким как ISO/IEC 7816.
- Может быть описан стандартом аппаратного обеспечения или физическим расположением аппаратного обеспечения.
- При необходимости могут быть непрозрачные данные (например, открытые ключи, идентификаторы и т. д.).
Если используются непрозрачные данные, их должна читать только одна сторона интерфейса HIDL. Например, если vendor.img
передает компоненту в system.img
строковое сообщение или данные vec<uint8_t>
, эти данные не могут быть проанализированы самим system.img
; его можно только передать обратно в vendor.img
для интерпретации. При передаче значения из vendor.img
в код поставщика на system.img
или на другое устройство формат данных и способ их интерпретации должны быть точно описаны и по-прежнему являются частью интерфейса .
Рекомендации
Вы должны иметь возможность написать реализацию или клиент HAL, используя только файлы .hal (т. е. вам не нужно смотреть исходный код Android или общедоступные стандарты). Мы рекомендуем указать точное требуемое поведение. Такие утверждения, как «реализация может делать А или Б», побуждают реализации переплетаться с клиентами, для которых они разрабатываются.
Компоновка HIDL-кода
HIDL включает в себя базовые пакеты и пакеты поставщиков.
Базовые интерфейсы HIDL указаны Google. Пакеты, к которым они принадлежат, начинаются с android.hardware.
и именуются по подсистеме, возможно, с вложенными уровнями именования. Например, пакет NFC называется android.hardware.nfc
, а пакет камеры — android.hardware.camera
. Обычно основной пакет имеет имя android.hardware.
[ name1
].[ name2
]…. Пакеты HIDL помимо имени имеют версию. Например, пакет android.hardware.camera
может иметь версию 3.4
; это важно, поскольку версия пакета влияет на его размещение в дереве исходного кода.
Все основные пакеты размещаются в разделе hardware/interfaces/
в системе сборки. Пакет android.hardware.
[ name1
].[ name2
]… в версии $m.$n
находится в папке hardware/interfaces/name1/name2/
… /$m.$n/
; Пакет android.hardware.camera
версии 3.4
находится в каталоге hardware/interfaces/camera/3.4/.
Между префиксом пакета android.hardware.
и путь hardware/interfaces/
.
Неосновные (вендорные) пакеты — это пакеты, произведенные поставщиком SoC или ODM. Префикс для неосновных пакетов vendor.$(VENDOR).hardware.
где $(VENDOR)
относится к поставщику SoC или OEM/ODM. Это соответствует пути vendor/$(VENDOR)/interfaces
в дереве (это сопоставление также жестко запрограммировано).
Полные имена определяемых пользователем типов.
В HIDL каждый UDT имеет полное имя, состоящее из имени UDT, имени пакета, в котором определен UDT, и версии пакета. Полное имя используется только при объявлении экземпляров типа, а не там, где определен сам тип. Например, предположим, что пакет android.hardware.nfc,
версии 1.0
определяет структуру с именем NfcData
. На сайте объявления (будь то в types.hal
или в объявлении интерфейса) объявление просто гласит:
struct NfcData { vec<uint8_t> data; };
При объявлении экземпляра этого типа (в структуре данных или в качестве параметра метода) используйте полное имя типа:
android.hardware.nfc@1.0::NfcData
Общий синтаксис: PACKAGE @ VERSION :: UDT
, где:
-
PACKAGE
— это имя HIDL-пакета, разделенное точками (например,android.hardware.nfc
). -
VERSION
— это формат пакета «основная.дополнительная версия», разделенный точками (например,1.0
). -
UDT
— это имя HIDL UDT, разделенное точками. Поскольку HIDL поддерживает вложенные UDT, а интерфейсы HIDL могут содержать UDT (тип вложенного объявления), для доступа к именам используются точки.
Например, если следующее вложенное объявление было определено в файле общих типов в пакете android.hardware.example
версии 1.0
:
// types.hal package android.hardware.example@1.0; struct Foo { struct Bar { // … }; Bar cheers; };
Полное имя Bar
— android.hardware.example@1.0::Foo.Bar
. Если бы вложенное объявление было не только в вышеуказанном пакете, но и в интерфейсе под названием IQuux
:
// IQuux.hal package android.hardware.example@1.0; interface IQuux { struct Foo { struct Bar { // … }; Bar cheers; }; doSomething(Foo f) generates (Foo.Bar fb); };
Полное имя Bar
— android.hardware.example@1.0::IQuux.Foo.Bar
.
В обоих случаях Bar
можно называть Bar
только в рамках объявления Foo
. На уровне пакета или интерфейса вы должны ссылаться на Bar
через Foo
: Foo.Bar
, как в объявлении метода doSomething
выше. В качестве альтернативы вы можете объявить метод более подробно:
// IQuux.hal doSomething(android.hardware.example@1.0::IQuux.Foo f) generates (android.hardware.example@1.0::IQuux.Foo.Bar fb);
Полные значения перечисления
Если UDT является перечислимым типом, то каждое значение перечисленного типа имеет полное имя, которое начинается с полного имени перечислимого типа, за которым следует двоеточие, а затем имя значения перечисления. Например, предположим, что пакет android.hardware.nfc,
версии 1.0
определяет перечисляемый тип NfcStatus
:
enum NfcStatus { STATUS_OK, STATUS_FAILED };
При обращении к STATUS_OK
полное имя выглядит так:
android.hardware.nfc@1.0::NfcStatus:STATUS_OK
Общий синтаксис: PACKAGE @ VERSION :: UDT : VALUE
, где:
-
PACKAGE @ VERSION :: UDT
— это то же самое полное имя для типа перечисления. -
VALUE
— это имя значения.
Правила автовыведения
Полное имя определяемого пользователем типа указывать не требуется. В имени UDT можно безопасно опустить следующее:
- Пакет, например
@1.0::IFoo.Type
- И пакет, и версия, например
IFoo.Type
HIDL пытается завершить имя, используя правила автоинтерференции (меньший номер правила означает более высокий приоритет).
Правило 1
Если пакет и версия не указаны, предпринимается попытка поиска по локальному имени. Пример:
interface Nfc { typedef string NfcErrorMessage; send(NfcData d) generates (@1.0::NfcStatus s, NfcErrorMessage m); };
NfcErrorMessage
ищется локально и находится указанный выше typedef
. NfcData
также ищется локально, но поскольку он не определен локально, используются правила 2 и 3. @1.0::NfcStatus
предоставляет версию, поэтому правило 1 не применяется.
Правило 2
Если правило 1 не выполняется и компонент с полным именем отсутствует (пакет, версия или пакет и версия), компонент автоматически заполняется информацией из текущего пакета. Затем компилятор HIDL просматривает текущий файл (и все импортированные файлы), чтобы найти автозаполненное полное имя. Используя приведенный выше пример, предположим, что объявление ExtendedNfcData
было сделано в том же пакете ( android.hardware.nfc
) той же версии ( 1.0
), что и NfcData
, следующим образом:
struct ExtendedNfcData { NfcData base; // … additional members };
Компилятор HIDL заполняет имя пакета и имя версии из текущего пакета, чтобы создать полное имя UDT android.hardware.nfc@1.0::NfcData
. Поскольку имя существует в текущем пакете (при условии, что оно импортировано правильно), оно используется для объявления.
Имя в текущем пакете импортируется только в том случае, если выполняется одно из следующих условий:
- Он импортируется явно с помощью оператора
import
. - Он определен в
types.hal
текущего пакета.
Тот же процесс выполняется, если NfcData
был указан только номер версии:
struct ExtendedNfcData { // autofill the current package name (android.hardware.nfc) @1.0::NfcData base; // … additional members };
Правило 3
Если правило 2 не дает совпадения (определяемый пользователем тип не определен в текущем пакете), компилятор HIDL сканирует совпадения во всех импортированных пакетах. Используя приведенный выше пример, предположим, что ExtendedNfcData
объявлен в версии 1.1
пакета android.hardware.nfc
, 1.1
импортирует 1.0
как и должно (см. Расширения уровня пакета ), а в определении указано только имя UDT:
struct ExtendedNfcData { NfcData base; // … additional members };
Компилятор ищет любой определяемый пользователем тип с именем NfcData
и находит его в android.hardware.nfc
версии 1.0
, в результате чего получается полностью квалифицированный определяемый пользователем тип android.hardware.nfc@1.0::NfcData
. Если для данного частично определенного определяемого пользователем типа найдено более одного совпадения, компилятор HIDL выдает ошибку.
Пример
Используя правило 2, импортированный тип, определенный в текущем пакете, имеет преимущество перед импортированным типом из другого пакета:
// 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
интерполируется какandroid.hardware.bar@1.0::S
и находится вbar/1.0/types.hal
(посколькуtypes.hal
импортируется автоматически). -
IFooCallback
интерполируется какandroid.hardware.bar@1.0::IFooCallback
с использованием правила 2, но его невозможно найти, так какbar/1.0/IFooCallback.hal
не импортируется автоматически (какtypes.hal
). Таким образом, правило 3 вместо этого разрешает его вandroid.hardware.foo@1.0::IFooCallback
, который импортируется черезimport android.hardware.foo@1.0;
).
типы.hal
Каждый пакет HIDL содержит файл types.hal
, содержащий UDT, которые являются общими для всех интерфейсов, участвующих в этом пакете. Типы HIDL всегда общедоступны; независимо от того, объявлен ли UDT в types.hal
или в объявлении интерфейса, эти типы доступны за пределами области, в которой они определены. types.hal
не предназначен для описания общедоступного API пакета, а скорее для размещения пользовательских типов, используемых всеми интерфейсами в пакете. Из-за особенностей HIDL все UDT являются частью интерфейса.
types.hal
состоит из UDT и операторов import
. Поскольку types.hal
доступен каждому интерфейсу пакета (это неявный импорт), эти операторы import
по определению относятся к уровню пакета. UDT в types.hal
также могут включать в себя импортированные таким образом UDT и интерфейсы.
Например, для 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;
Импортируются следующие данные:
-
android.hidl.base@1.0::IBase
(неявно) -
android.hardware.foo@1.0::types
(неявно) - Все в
android.hardware.bar@1.0
(включая все интерфейсы и ихtypes.hal
) -
types.hal
изandroid.hardware.baz@1.0::types
(интерфейсы вandroid.hardware.baz@1.0
не импортируются) -
IQux.hal
иtypes.hal
изandroid.hardware.qux@1.0
-
Quuz
изandroid.hardware.quuz@1.0
(при условии, чтоQuuz
определен вtypes.hal
, анализируется весь файлtypes.hal
, но типы, отличные отQuuz
, не импортируются).
Управление версиями на уровне интерфейса
Каждый интерфейс внутри пакета находится в отдельном файле. Пакет, к которому принадлежит интерфейс, объявляется в верхней части интерфейса с помощью оператора package
. После объявления пакета может быть указано ноль или более импортов на уровне интерфейса (частичный или полный пакет). Например:
package android.hardware.nfc@1.0;
В HIDL интерфейсы могут наследовать от других интерфейсов с помощью ключевого слова extends
. Чтобы интерфейс мог расширять другой интерфейс, он должен иметь к нему доступ через оператор import
. Имя расширяемого интерфейса (базовый интерфейс) соответствует правилам квалификации имени типа, описанным выше. Интерфейс может наследовать только один интерфейс; HIDL не поддерживает множественное наследование.
В приведенных ниже примерах управления версиями uprev используется следующий пакет:
// 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); }
Правила Упрев
Чтобы определить пакет package@major.minor
, либо A, либо все B должны быть истинными:
Правило А | «Является начальной дополнительной версией»: все предыдущие дополнительные версии package@major.0 , package@major.1 , …, package@major.(minor-1) не должны определяться. |
---|
Правило Б | Все нижеследующее верно:
|
---|
Из-за правила А:
- Пакет может начинаться с любого младшего номера версии (например,
android.hardware.biometrics.fingerprint
начинается с@2.1
.) - Требование «
android.hardware.foo@1.0
не определено» означает, что каталогhardware/interfaces/foo/1.0
вообще не должен существовать.
Однако правило A не влияет на пакет с тем же именем пакета, но с другой основной версией (например, android.hardware.camera.device
определены как @1.0
, так и @3.2
; @3.2
не обязательно должен взаимодействовать с @1.0
.) Следовательно, @3.2::IExtFoo
может расширять @1.0::IFoo
.
Если имя пакета отличается, package@major.minor::IBar
может расширяться от интерфейса с другим именем (например, android.hardware.bar@1.0::IBar
может расширяться от android.hardware.baz@2.2::IBaz
). Если в интерфейсе явно не объявлен супертип с ключевым словом extend
, он расширяет android.hidl.base@1.0::IBase
(кроме самого IBase
).
B.2 и B.3 должны соблюдаться одновременно. Например, даже если android.hardware.foo@1.1::IFoo
расширяет android.hardware.foo@1.0::IFoo
для прохождения правила B.2, если android.hardware.foo@1.1::IExtBar
расширяет android.hardware.foo@1.0::IBar
, это все еще недействительная версия uprev.
Упрев интерфейсы
Чтобы обновить android.hardware.example@1.0
(определено выше) до @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); }
Это import
на уровне пакета версии 1.0
файла android.hardware.example
в types.hal
. Хотя в версию пакета 1.1
не добавляются новые UDT, ссылки на UDT в версии 1.0
по-прежнему необходимы, поэтому импортируется на уровне пакета в types.hal
. (Того же эффекта можно было бы достичь с помощью импорта на уровне интерфейса в IQuux.hal
.)
В extends @1.0::IQuux
в объявлении IQuux
мы указали наследуемую версию IQuux
(требуется устранение неоднозначности, поскольку IQuux
используется для объявления интерфейса и наследования от интерфейса). Поскольку объявления — это просто имена, которые наследуют все атрибуты пакета и версии на сайте объявления, устранение неоднозначности должно быть в имени базового интерфейса; мы могли бы также использовать полностью квалифицированный UDT, но это было бы излишним.
Новый интерфейс IQuux
не переобъявляет метод fromFooToBar()
который он наследует от @1.0::IQuux
; он просто перечисляет новый метод, который он добавляет fromBarToFoo()
. В HIDL унаследованные методы не могут быть снова объявлены в дочерних интерфейсах, поэтому интерфейс IQuux
не может явно объявить метод fromFooToBar()
.
Соглашения Упрев
Иногда имена интерфейсов должны переименовывать расширяемый интерфейс. Мы рекомендуем, чтобы расширения, структуры и объединения перечислений имели то же имя, что и то, что они расширяют, если только они не отличаются настолько, что требуется новое имя. Примеры:
// 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 };
Если метод может иметь новое семантическое имя (например, fooWithLocation
), то это предпочтительно. В противном случае его следует назвать так же, как то, что он расширяет. Например, метод foo_1_1
в @1.1::IFoo
может заменить функциональность метода foo
в @1.0::IFoo
если нет лучшего альтернативного имени.
Управление версиями на уровне пакета
Управление версиями HIDL происходит на уровне пакета; после публикации пакета он становится неизменяемым (набор его интерфейсов и пользовательских типов не может быть изменен). Пакеты могут связываться друг с другом несколькими способами, каждый из которых можно выразить посредством комбинации наследования на уровне интерфейса и построения пользовательских типов по композиции.
Однако один тип отношений строго определен и должен соблюдаться: обратно совместимое наследование на уровне пакета . В этом сценарии родительский пакет — это пакет, от которого наследуется, а дочерний пакет — это пакет, расширяющий родительский. Правила обратно совместимого наследования на уровне пакета следующие:
- Все интерфейсы верхнего уровня родительского пакета наследуются от интерфейсов дочернего пакета.
- В новый пакет также можно добавить новые интерфейсы (нет ограничений на отношения с другими интерфейсами в других пакетах).
- Также можно добавлять новые типы данных для использования либо новыми методами обновленных существующих интерфейсов, либо новыми интерфейсами.
Эти правила могут быть реализованы с использованием наследования на уровне интерфейса HIDL и композиции UDT, но для понимания того, что эти отношения представляют собой обратно совместимое расширение пакета, требуются знания метауровня. Эти знания заключаются в следующем:
Если пакет соответствует этому требованию, hidl-gen
применяет правила обратной совместимости.