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.img
と system.img
を別々にコンパイルできる、個別にコンパイルされるベンダー コンポーネントとシステム コンポーネントをサポートしています。vendor.img
と system.img
が長期にわたって機能し続けるように、両者間のすべてのインタラクションが明示的かつ完全に定義される必要があります。これには多くの API サーフェスが含まれますが、主要なサーフェスは HIDL が system.img
と vendor.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.$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
はパッケージのバージョンを表すドット区切りの 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
です。
どちらの場合も、Bar
は Foo
の宣言のスコープ内でのみ 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
)で ExtendedNfcData
を 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 コンパイラはインポートされたすべてのパッケージ内をスキャンして一致するものを探します。上記の例を使用して、パッケージ android.hardware.nfc
のバージョン 1.1
で ExtendedNfcData
を宣言すると、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 };
S
はandroid.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::IFooCallback
(import 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::types
のtypes.hal
(android.hardware.baz@1.0
のインターフェースはインポートされません)android.hardware.qux@1.0
のIQux.hal
とtypes.hal
android.hardware.quuz@1.0
のQuuz
(Quuz
がtypes.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.0 、package@major.1 、…、package@major.(minor-1) )がどれも定義されていないこと。 |
---|
ルール B | 以下をすべて満たすこと。
|
---|
ルール 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::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 ではありません。
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::IFoo
の foo
メソッドの機能を @1.1::IFoo
のメソッド foo_1_1
で行えます。
パッケージ レベルのバージョニング
HIDL のバージョニングはパッケージ レベルで行われ、公開されたパッケージは変更できません(インターフェースと UDT のセットを変更できません)。パッケージは複数の方法で相互に関連付けることができます。どの方法でも、インターフェース レベルの継承と、UDT 構成の作成との組み合わせにより、関連付けを表します。
ただし、関連性の一つである「パッケージ レベルの下位互換性継承」は、厳密に定義して適用する必要があります。この場合、「親」パッケージが継承元のパッケージで、「子」パッケージが親を継承します。パッケージ レベルの下位互換性継承ルールは次のとおりです。
- 親パッケージのすべてのトップレベル インターフェースを、子パッケージのインターフェースが継承します。
- 新しいインターフェースを新しいパッケージに追加することもできます(他のパッケージに含まれる他のインターフェースとの関係に制限はありません)。
- 新しいデータ型を追加して、uprev された既存のインターフェースの新しいメソッド、または新しいインターフェースで使用することもできます。
これらのルールは、HIDL インターフェース レベルの継承と UDT 構成を使用して実装できますが、下位互換性のあるパッケージ拡張を成立させる関係を理解するためにメタレベルの知識が必要です。この知識から次のように推論できます。
パッケージがこの要件を満たしている場合は、hidl-gen
によって下位互換性ルールが適用されます。