バージョニング

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 の宣言のように、FooFoo.Bar を通じて 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 つの異なる名前です)。同じ名前のインターフェースがある場合、k が小さい IBar が存在しないように、package@major.minor::IBarpackage@major.(minor-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.1::IFoo のメソッド foo_1_1@1.0::IFoofoo メソッドの機能を置き換えられます。

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

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

ただし、1 種類の関係を厳密に定義して適用する必要があります(パッケージレベルの下位互換性継承)。このシナリオでは、親パッケージが継承元のパッケージで、子パッケージが親を継承します。パッケージレベルの下位互換性継承ルールは次のとおりです。

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

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

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