Версии

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 управление версиями применяется на уровне пакета, а не на уровне интерфейса, и все интерфейсы и UDT в пакете имеют одну и ту же версию. Версии пакетов следуют семантическому управлению версиями без компонентов уровня исправлений и метаданных сборки. В данном пакете незначительное изменение версии означает, что новая версия пакета обратно совместима со старым пакетом, а увеличение основной версии означает, что новая версия пакета не имеет обратной совместимости со старым пакетом.

Концептуально пакет может относиться к другому пакету одним из нескольких способов:

  • Совсем нет .
  • Расширяемость с обратной совместимостью на уровне пакета . Это происходит для новых младших версий uprevs (следующая увеличенная версия) пакета; новый пакет имеет то же имя и основную версию, что и старый пакет, но более высокую дополнительную версию. Функционально новый пакет представляет собой надмножество старого пакета, что означает:
    • Интерфейсы верхнего уровня родительского пакета присутствуют в новом пакете, хотя интерфейсы могут иметь новые методы, новые определяемые пользователем типы интерфейса (расширение уровня интерфейса, описанное ниже) и новые определяемые пользователем типы данных в types.hal .
    • В новый пакет также могут быть добавлены новые интерфейсы.
    • Все типы данных родительского пакета присутствуют в новом пакете и могут обрабатываться (возможно, перереализованными) методами из старого пакета.
    • Также могут быть добавлены новые типы данных для использования либо новыми методами обновленных существующих интерфейсов, либо новыми интерфейсами.
  • Расширяемость на уровне интерфейса с обратной совместимостью . Новый пакет также может расширять исходный пакет, состоя из логически отдельных интерфейсов, которые просто обеспечивают дополнительный функционал, а не основной. Для этой цели может потребоваться следующее:
    • Интерфейсы в новом пакете нуждаются в использовании типов данных старого пакета.
    • Интерфейсы в новом пакете могут расширять интерфейсы одного или нескольких старых пакетов.
  • Расширьте исходную обратную несовместимость . Это основная версия пакета uprev, и между ними не должно быть никакой корреляции. В той мере, в какой это возможно, его можно выразить с помощью комбинации типов из более старой версии пакета и наследования подмножества интерфейсов из старого пакета.

Структурирование интерфейсов

Для хорошо структурированного интерфейса добавление новых типов функций, не являющихся частью исходного дизайна, должно потребовать модификации интерфейса 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 указывать не обязательно. В имени 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
};

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

Упрежные соглашения

Иногда имена интерфейсов должны переименовывать расширяемый интерфейс. Мы рекомендуем, чтобы расширения enum, структуры и объединения имели то же имя, что и то, что они расширяют, если только они не отличаются достаточно, чтобы гарантировать новое имя. Примеры:

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

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

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

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

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