インターフェースのバージョニング

HIDL では、HIDL で記述されたすべてのインターフェースをバージョニングする必要があります。HAL インターフェースは公開後に凍結され、以降の変更はそのインターフェースの新しいバージョンに対して行う必要があります。公開済みの特定のインターフェースを変更することはできませんが、新たなインターフェースで拡張できます。

HIDL コードの構造

HIDL コードの構成要素は、ユーザー定義型、インターフェース、およびパッケージです。

  • ユーザー定義型(UDT)。HIDL では、一連のプリミティブ データ型を使用できるほか、それらを使った構造体、共用体、列挙型によってより複雑な型を作成できます。UDT はインターフェースのメソッドに渡され、パッケージのレベル(すべてのインターフェースに共通)で定義することも、インターフェースに対してローカルに定義することもできます。
  • インターフェース。HIDL の基本的な要素として、インターフェースは UDT とメソッドの宣言で構成されます。インターフェースは、別のインターフェースを継承することもできます。
  • パッケージ。関連する HIDL インターフェースとその操作対象となるデータ型がまとめられています。パッケージは名前とバージョンで識別され、以下が含まれます。
    • types.hal というデータ型定義ファイル。
    • それぞれが独自の .hal ファイルに含まれ 0 個以上のインターフェース。

データ型定義ファイル types.hal には UDT のみが含まれます(パッケージ レベルのすべての UDT が 1 つのファイルに保持されます)。パッケージ内のすべてのインターフェースでターゲット言語の表現を使用できます。

バージョニングの原理

HIDL パッケージ(android.hardware.nfc など)は、特定のバージョン(1.0 など)で公開された後は変更できません。パッケージ内のインターフェースに対する変更または UDT への変更は、別のパッケージでのみ行えます。

HIDL では、バージョニングがインターフェース レベルではなくパッケージ レベルで適用され、パッケージ内のすべてのインターフェースと UDT で同じバージョンが共有されます。パッケージ バージョンは、セマンティック バージョニングに従います。パッチレベルとビルドメタデータ コンポーネントはありません。特定のパッケージにおいて、マイナー バージョンのバンプは、新バージョンのパッケージに古いパッケージとの下位互換性があることを意味します。メジャー バージョンのバンプは、新バージョンのパッケージに古いパッケージとの下位互換性がないことを意味します。

概念的には、パッケージを次のいずれかの方法で別のパッケージに関連付けることが可能です。

  • まったく関連なし
  • パッケージ レベルの下位互換性のある拡張。これは、パッケージの新しいマイナー バージョンの uprev(増分された次のリビジョン)で発生します。新しいパッケージの名前とメジャー バージョンは古いパッケージと同じですが、マイナー バージョンが高くなります。機能的には、新しいパッケージは古いパッケージのスーパーセットです。つまり次のことを意味します。
    • 親パッケージのトップレベル インターフェースが新しいパッケージに存在します。ただし、types.hal に新しいメソッド、新しいインターフェースにローカルな UDT(後述するインターフェース レベルの拡張)、新しい UDT を含めることができます。
    • 新しいパッケージに新しいインターフェースを追加することもできます。
    • 親パッケージのすべてのデータ型が新しいパッケージに存在し、古いパッケージのメソッド(場合によって再実装される)で処理できます。
    • 新しいデータ型を追加して、uprev された既存のインターフェースの新しいメソッド、または新しいインターフェースで使用することもできます。
  • インターフェース レベルの下位互換性のある拡張。追加の機能を提供するだけでコアではない論理的に独立したインターフェースで構成される新しいパッケージは、元のパッケージを拡張することもできます。この場合の望ましい条件は次のとおりです。
    • 新しいパッケージ内のインターフェースで、古いパッケージのデータ型を使用する必要があります。
    • 新しいパッケージ内のインターフェースで、1 つ以上の古いパッケージのインターフェースを拡張できます。
  • 元の下位非互換性を拡張します。これはパッケージのメジャー バージョンの uprev であり、両者間に相関関係は必要ありません。相関関係が存在する場合は、パッケージに含まれる古いバージョンの型の組み合わせと、古いパッケージ インターフェースのサブセットの継承で表すことができます。

インターフェース構造化

適切に構造化されたインターフェースの場合、元の設計にない新しいタイプの機能を追加するには、HIDL インターフェースを変更する必要があります。逆にインターフェース自体は変更せずに、新しい機能を導入するインターフェースの両側を変更する場合、インターフェースは構造化されません。

Treble は、デバイスの vendor.imgsystem.img を別々にコンパイルできる、個別にコンパイルされるベンダー コンポーネントとシステム コンポーネントをサポートしています。vendor.imgsystem.img が長期にわたって機能し続けるように、両者間のすべてのインタラクションが明示的かつ完全に定義される必要があります。これには多くの API サーフェスが含まれますが、主要なサーフェスは HIDL が system.imgvendor.img の境界でプロセス間通信に使用する IPC メカニズムです。

要件

HIDL を介して渡されるすべてのデータを明示的に定義する必要があります。実装とクライアントが個別にコンパイルされたり開発されたりしても、引き続き連携できるようにするには、データが以下の要件を満たす必要があります。

  • HIDL で(構造体の列挙型などを使用して)セマンティックな名前と意味で直接記述できる。
  • ISO/IEC 7816 のような公開標準によって記述できる。
  • ハードウェア標準またはハードウェアの物理レイアウトによって記述できる。
  • 必要に応じて、不透明なデータ(公開キーや ID など)を使用できる。

不透明なデータを使用する場合は、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.$nhardware/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.0NfcData という名前の構造体を定義するとします。宣言箇所(types.hal 内またはインターフェースの宣言内)では、単に次のように宣言されているとします。

struct NfcData {
    vec<uint8_t> data;
};

この型のインスタンスを(データ構造内またはメソッド パラメータで)宣言するときは、完全修飾された型の名前を使用します。

android.hardware.nfc@1.0::NfcData

一般的な構文は PACKAGE@VERSION::UDT です。

  • PACKAGE は HIDL パッケージのドット区切り名です(例: android.hardware.nfc)。
  • VERSION はパッケージのバージョンを表すドット区切りの major.minor 形式です(例: 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 です。

どちらの場合も、BarFoo の宣言のスコープ内でのみ Bar と呼ばれます。パッケージ レベルまたはインターフェース レベルでは、上のメソッド doSomething の宣言のように、Foo を通じて Bar を参照する(Foo.Bar)必要があります。または、次のようにメソッドを詳細に宣言することもできます。

// 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 コンパイラは現在のファイル(およびすべてのインポート ファイル)を調べて、自動入力された完全修飾名を探します。上記の例を使用して、次のように同じパッケージ(android.hardware.nfc)の同じバージョン(1.0)で ExtendedNfcDataNfcData として宣言したとします。

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 コンパイラはインポートされたすべてのパッケージ内をスキャンして一致するものを探します。上記の例を使用して、パッケージ android.hardware.nfc のバージョン 1.1ExtendedNfcData を宣言すると、1.1 によって 1.0 が適切にインポートされます(パッケージ レベルの拡張に関する説明を参照)。定義には次のように UDT 名のみ指定するとします。

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

コンパイラは NfcData という名前の UDT を探して android.hardware.nfc のバージョン 1.0 で見つけます。その結果、android.hardware.nfc@1.0::NfcData の完全修飾された UDT が生成されます。部分修飾されたある 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
};
  • Sandroid.hardware.bar@1.0::S として補完され、bar/1.0/types.hal で見つかります(types.hal が自動的にインポートされるため)。
  • IFooCallback はルール 2 を使用して android.hardware.bar@1.0::IFooCallback として補完されますが、bar/1.0/IFooCallback.hal が(types.hal のように)自動的にインポートされないため見つかりません。したがって、ルール 3 で代わりに android.hardware.foo@1.0::IFooCallbackimport android.hardware.foo@1.0; を介してインポートされる)に解決されます。

types.hal

どの HIDL パッケージにも、パッケージ内のすべてのインターフェースで共有される UDT を含む types.hal ファイルが含まれます。HIDL の型は常に公開されます。UDT が types.hal とインターフェース宣言のどちらで宣言されているかにかかわらず、これらの型には、定義されているスコープ外からアクセスできます。types.hal はパッケージの公開 API を記述するものではなく、パッケージ内のすべてのインターフェースで使用される UDT をホストするものです。HIDL の性質上、すべての UDT がインターフェースに含まれます。

types.hal は UDT と import ステートメントで構成されます。types.hal がパッケージのすべてのインターフェースで使用できるようになる(暗黙的にインポートされる)ため、import ステートメントは定義上パッケージ レベルです。types.hal の UDT には、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 が含まれます)
  • android.hardware.baz@1.0::typestypes.halandroid.hardware.baz@1.0 のインターフェースはインポートされません)
  • android.hardware.qux@1.0IQux.haltypes.hal
  • android.hardware.quuz@1.0QuuzQuuztypes.hal で定義されている場合は、types.hal ファイル全体が解析されますが、Quuz 以外の型はインポートされません)

インターフェース レベルのバージョニング

パッケージ内の各インターフェースはそれぞれのファイル内にあります。インターフェースを含むパッケージは、そのインターフェースの最上部で package ステートメントを使って宣言されます。パッケージ宣言に続いて、0 個以上のインターフェース レベルのインポート(パッケージの一部または全体)があります。次に例を示します。

package android.hardware.nfc@1.0;

HIDL では、extends キーワードを使用してインターフェースが他のインターフェースを継承できます。別のインターフェースを拡張するインターフェースは、import ステートメントによってその別のインターフェースにアクセスできる必要があります。拡張されるインターフェース(ベース インターフェース)の名前は、前述の型名修飾のルールに従います。継承できるインターフェースは 1 つだけです。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);
}

uprev ルール

パッケージ package@major.minor を定義するには、A の条件または B のすべて条件を満たす必要があります。

ルール A 「先頭のマイナー バージョンである」: それより前のマイナー バージョン(package@major.0package@major.1、…、package@major.(minor-1))がどれも定義されていないこと。
または
ルール B

以下をすべて満たすこと。

  1. 「以前のマイナー バージョンが有効である」: package@major.(minor-1) が定義されていて、かつそれが同じルール A(package@major.0package@major.(minor-2) は定義されていない)またはルール B(@major.(minor-2) からの uprev となる)のどちらかを満たしていること。

    かつ

  2. 「同じ名前のインターフェースを 1 つ以上継承する」: package@major.(minor-1)::IFoo を拡張するインターフェース package@major.minor::IFoo が存在する(以前のパッケージにインターフェースがある場合)。

    かつ

  3. 「名前の異なるインターフェースを継承しない」: package@major.(minor-1)::IBaz を拡張する package@major.minor::IBar は存在しないこと(IBarIBaz は 2 つの異なる名前)。同じ名前のインターフェースがある場合、package@major.minor::IBarpackage@major.(minor-k)::IBar を拡張する必要があり、k がそれより小さい IBar は存在しないこと。

ルール A により、次のようになります。

  • パッケージは、任意のマイナー バージョン番号で始まる場合があります(たとえば、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::IBarandroid.hardware.baz@2.2::IBaz を拡張できます)。extend キーワードを使用した明示的なスーパー型の宣言のないインターフェースがあれば、android.hidl.base@1.0::IBase が拡張されます(IBase 自体を除く)。

B.2 と B.3 は同時に満たす必要があります。たとえば、android.hardware.foo@1.1::IFooandroid.hardware.foo@1.0::IFoo を拡張してルール B.2 を満たしても、android.hardware.foo@1.1::IExtBarandroid.hardware.foo@1.0::IBar を拡張する場合は、有効な uprev ではありません。

uprev インターフェース

android.hardware.example@1.0(上記で定義)を @1.1 に uprev するには:

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

これは、types.hal にある android.hardware.example バージョン 1.0 のパッケージ レベルの import です。パッケージのバージョン 1.1 に新しい UDT は追加されていませんが、バージョン 1.0 の UDT への参照が必要なため、types.hal 内でのパッケージ レベルのインポートです(IQuux.hal のインターフェース レベルのインポートでも同じ効果が得られます)。

IQuux の宣言の extends @1.0::IQuux で、継承される IQuux のバージョンを指定しています(インターフェースを宣言して継承するために IQuux を使用しているため、曖昧さを回避する必要があります)。宣言しているのは、宣言箇所にあるすべてのパッケージとバージョンの属性を継承する名前だけなので、ベース インターフェースの名前を明確にする必要があります。完全修飾された UDT も使用できますが、それでは冗長になります。

新しいインターフェース IQuux は、@1.0::IQuux から継承するメソッド fromFooToBar() を再宣言しているわけではありません。追加する新しいメソッドである fromBarToFoo() をリストしているだけです。HIDL では、継承されたメソッドを子インターフェースで再宣言してはならないので、IQuux インターフェースは fromFooToBar() メソッドを明示的に宣言できません。

uprev 規則

場合によっては、インターフェース名は拡張するインターフェースの名前から変更する必要があります。列挙型の拡張、構造体、共用体では、新しい名前のほうが妥当となるほど変更する場合を除いて、拡張する対象と同じ名前にすることをおすすめします。以下に例を示します。

// 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)を指定できる場合は、変更することをおすすめします。それ以外の場合は、拡張する対象と同様の名前を付けてください。たとえば、適切な代替名がない場合は、@1.0::IFoofoo メソッドの機能を @1.1::IFoo のメソッド foo_1_1 で行えます。

パッケージ レベルのバージョニング

HIDL のバージョニングはパッケージ レベルで行われ、公開されたパッケージは変更できません(インターフェースと UDT のセットを変更できません)。パッケージは複数の方法で相互に関連付けることができます。どの方法でも、インターフェース レベルの継承と、UDT 構成の作成との組み合わせにより、関連付けを表します。

ただし、関連性の一つである「パッケージ レベルの下位互換性継承」は、厳密に定義して適用する必要があります。この場合、「親」パッケージが継承元のパッケージで、「子」パッケージが親を継承します。パッケージ レベルの下位互換性継承ルールは次のとおりです。

  1. 親パッケージのすべてのトップレベル インターフェースを、子パッケージのインターフェースが継承します。
  2. 新しいインターフェースを新しいパッケージに追加することもできます(他のパッケージに含まれる他のインターフェースとの関係に制限はありません)。
  3. 新しいデータ型を追加して、uprev された既存のインターフェースの新しいメソッド、または新しいインターフェースで使用することもできます。

これらのルールは、HIDL インターフェース レベルの継承と UDT 構成を使用して実装できますが、下位互換性のあるパッケージ拡張を成立させる関係を理解するためにメタレベルの知識が必要です。この知識から次のように推論できます。

パッケージがこの要件を満たしている場合は、hidl-gen によって下位互換性ルールが適用されます。