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

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;
};

Полное имя Barandroid.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);
};

Полное имя Barandroid.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) не должны определяться.
ИЛИ
Правило Б

Все нижеследующее верно:

  1. «Предыдущая дополнительная версия действительна»: package@major.(minor-1) должен быть определен и следовать тому же правилу A (ни один из package@major.0 до package@major.(minor-2) не определен) или правилу B (если это обновление от @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 вообще не должен существовать.

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

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

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

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

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