Управление версиями интерфейса

Согласно требованиям HIDL, каждый интерфейс, написанный на HIDL, должен иметь версию. После публикации интерфейс HAL замораживается, и любые дальнейшие изменения должны вноситься в новую версию этого интерфейса. Опубликованный интерфейс нельзя изменить, но можно расширить с помощью другого интерфейса.

Структура кода HIDL

Код HIDL организован в пользовательские типы, интерфейсы и пакеты:

  • Типы, заданные пользователем (UDT). HIDL предоставляет доступ к набору примитивных типов данных, которые можно использовать для создания более сложных типов с помощью структур, объединений и перечислений. UDT передаются методам интерфейсов и могут быть определены на уровне пакета (для всех интерфейсов) или локально для интерфейса.
  • Интерфейсы. Интерфейс – это основной структурный элемент HIDL, который состоит из пользовательских типов данных и объявлений методов. Интерфейсы также могут наследовать свойства других интерфейсов.
  • Пакеты. Организует связанные интерфейсы HIDL и типы данных, с которыми они работают. Пакет идентифицируется по названию и версии и включает в себя следующее:
    • Файл определения типа данных types.hal.
    • Ноль или более интерфейсов, каждый в отдельном файле .hal.

Файл определения типа данных types.hal содержит только пользовательские типы данных (все пользовательские типы данных на уровне пакета хранятся в одном файле). Представления на целевом языке доступны во всех интерфейсах пакета.

Принципы управления версиями

После публикации пакета HIDL (например, android.hardware.nfc) для определенной версии (например, 1.0) он становится неизменяемым. Изменения в интерфейсах пакета или его пользовательских типах данных можно вносить только в другом пакете.

В 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 или общедоступные стандарты). Рекомендуем указывать точное поведение. Утверждения, подобные "реализация может делать A или B", приводят к тому, что реализации становятся взаимосвязанными с клиентами, для которых они разрабатываются.

Структура кода 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/.

Неосновные (поставщика) пакеты создаются поставщиком процессора или ODM. Префикс для неосновных пакетов – vendor.$(VENDOR).hardware., где $(VENDOR) – это поставщик однокристальной системы или производитель оригинального оборудования/разработчик оригинального изделия. Это соответствует пути vendor/$(VENDOR)/interfaces в дереве (это сопоставление также задано в коде).

Полные имена пользовательских типов

В HIDL у каждого типа, определенного пользователем, есть полное имя, состоящее из названия типа, названия пакета, в котором он определен, и версии пакета. Полное имя используется только при объявлении экземпляров типа, а не при определении самого типа. Предположим, что в пакете 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. Поскольку 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 является перечислением, то каждое значение этого перечисления имеет полное имя, которое начинается с полного имени типа перечисления, за которым следует двоеточие, а затем название значения перечисления. Например, команда assume package android.hardware.nfc, version 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 не дает совпадения (UDT не определен в текущем пакете), компилятор HIDL ищет совпадение во всех импортированных пакетах. В примере выше предположим, что ExtendedNfcData объявлен в версии 1.1 пакета android.hardware.nfc, 1.1 импортирует 1.0 как положено (см. Расширения на уровне пакета), а в определении указано только название пользовательского типа:

struct ExtendedNfcData {
    NfcData base;
    // … additional members
};

Компилятор ищет любой UDT с именем NfcData и находит его в android.hardware.nfc версии 1.0, в результате чего получается полностью квалифицированный UDT 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;.

types.hal

Каждый пакет HIDL содержит файл types.hal с типами, определенными пользователем, которые используются во всех интерфейсах, входящих в этот пакет. Типы HIDL всегда являются общедоступными. Независимо от того, объявлен ли тип, определяемый пользователем, в types.hal или в объявлении интерфейса, он доступен за пределами области, в которой определен. types.hal не предназначен для описания общедоступного API пакета, а скорее для размещения UDT, используемых всеми интерфейсами в пакете. Из-за особенностей HIDL все определяемые пользователем типы являются частью интерфейса.

types.hal состоит из пользовательских типов данных и операторов import. Поскольку types.hal доступен для каждого интерфейса пакета (это неявный импорт), эти операторы import по определению относятся к уровню пакета. В пользовательские типы данных в types.hal можно также включать импортированные пользовательские типы данных и интерфейсы.

Например, для 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 не поддерживает множественное наследование.

В примерах управления версиями ниже используется следующий пакет:

// 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, должно быть истинным либо утверждение А, либо все утверждения из группы Б:

Правило А "Is a start minor version": все предыдущие промежуточные версии, package@major.0, package@major.1, …, package@major.(minor-1) не должны быть определены.
ИЛИ
Правило Б

Выполняются все следующие условия:

  1. "Предыдущая младшая версия действительна": package@major.(minor-1) должен быть определен и соответствовать правилу А (ни один из параметров package@major.0–package@major.(minor-2) не определен) или правилу Б (если это обновление с @major.(minor-2));

    И

  2. "Наследует хотя бы один интерфейс с тем же названием": существует интерфейс package@major.minor::IFoo, который расширяет package@major.(minor-1)::IFoo (если в предыдущем пакете есть интерфейс);

    И

  3. "Нет унаследованного интерфейса с другим названием": не должно существовать интерфейса package@major.minor::IBar, который расширяет package@major.(minor-1)::IBaz, где IBar и IBaz – два разных названия. Если существует интерфейс с таким же названием, package@major.minor::IBar должен расширять package@major.(minor-k)::IBar так, чтобы не существовало IBar с меньшим k.

Из-за правила А:

  • Пакет может начинаться с любого номера промежуточной версии (например, android.hardware.biometrics.fingerprint начинается с @2.1).
  • Требование "android.hardware.foo@1.0 не определено" означает, что каталог hardware/interfaces/foo/1.0 не должен существовать.

Однако правило А не влияет на пакет с тем же названием, но другой основной версией (например, в 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 пакета не добавляются новые пользовательские типы данных, ссылки на них в версии 1.0 по-прежнему необходимы, поэтому в types.hal используется импорт на уровне пакета. (Того же эффекта можно было бы достичь, импортировав данные на уровне интерфейса в IQuux.hal.)

В extends @1.0::IQuux в объявлении IQuux мы указали версию IQuux, которая наследуется (уточнение необходимо, поскольку IQuux используется для объявления интерфейса и наследования от интерфейса). Поскольку объявления – это просто названия, которые наследуют все атрибуты пакета и версии в месте объявления, устранение неоднозначности должно быть в названии базового интерфейса. Мы могли бы также использовать полное имя типа, определенного пользователем, но это было бы избыточно.

Новый интерфейс 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 осуществляется на уровне пакета; после публикации пакет становится неизменяемым (набор его интерфейсов и типов, определенных пользователем, нельзя изменить). Пакеты могут быть связаны друг с другом несколькими способами, каждый из которых можно выразить с помощью комбинации наследования на уровне интерфейса и построения UDT по композиции.

Однако один тип отношений строго определен и должен соблюдаться: обратная совместимость на уровне пакета. В этом случае родительский пакет – это пакет, от которого наследуется дочерний, а дочерний пакет – это пакет, который расширяет родительский. Правила наследования на уровне пакета, обеспечивающие обратную совместимость:

  1. Все интерфейсы верхнего уровня родительского пакета наследуются интерфейсами дочернего пакета.
  2. В новый пакет также можно добавить новые интерфейсы (без ограничений на связи с другими интерфейсами в других пакетах).
  3. Новые типы данных также можно добавлять для использования в новых методах обновленных существующих интерфейсов или в новых интерфейсах.

Эти правила можно реализовать с помощью наследования на уровне интерфейса HIDL и композиции UDT, но для этого требуются знания на метауровне, чтобы понять, что эти отношения представляют собой обратно совместимое расширение пакета. Эти знания выводятся следующим образом:

Если пакет соответствует этому требованию, hidl-gen применяет правила обратной совместимости.