HIDL 要求每个用 HIDL 编写的接口都必须进行版本控制。在发布 HAL 接口后,它将被冻结,任何进一步的更改都必须在新版本的接口中进行。虽然给定的已发布接口无法修改,但可以通过另一个接口进行扩展。
HIDL 代码结构
- 用户定义的类型 (UDT)。HIDL 提供对一组原始数据类型的访问,这些数据类型可用于通过结构、联合和枚举来构成更复杂的类型。UDT 传递给接口的方法,并且可以在软件包级别(所有接口通用)或接口本地级别定义。
- 接口。作为 HIDL 的基本构建块,接口由 UDT 和方法声明组成。接口也可以从另一个接口继承。
- 软件包。组织相关的 HIDL 接口及其操作的数据类型。软件包由名称和版本标识,包括以下内容
- 名为
types.hal
的数据类型定义文件。 - 零个或多个接口,每个接口都在其自己的
.hal
文件中。
- 名为
数据类型定义文件 types.hal
仅包含 UDT(所有软件包级 UDT 都保存在单个文件中)。目标语言的表示形式可用于软件包中的所有接口。
版本控制理念
HIDL 软件包(例如 android.hardware.nfc
)在针对给定版本(例如 1.0
)发布后,是不可变的;无法更改。对软件包中的接口的修改或对其 UDT 的任何更改只能在另一个软件包中进行。
在 HIDL 中,版本控制应用于包级别,而非接口级别,并且一个包中的所有接口和 UDT 共享相同的版本。包版本遵循语义版本控制,但不包含补丁级别和构建元数据组件。在给定的包中,次要版本号的提升意味着新版本的包向后兼容旧版本的包,而主要版本号的提升意味着新版本的包不向后兼容旧版本的包。
从概念上讲,一个包可以通过以下几种方式与另一个包相关联
- 完全无关.
- 包级别向后兼容的扩展。这种情况发生在包的新次要版本升级(下一个递增修订版)时;新包与旧包具有相同的名称和主版本号,但次版本号更高。从功能上讲,新包是旧包的超集,这意味着
- 父包的顶层接口在新包中仍然存在,尽管这些接口可能具有新方法、新的接口本地 UDT(如下所述的接口级别扩展)以及
types.hal
中的新 UDT。 - 也可以将新接口添加到新包中。
- 父包的所有数据类型在新包中仍然存在,并且可以由旧包中(可能已重新实现的)方法处理。
- 也可以添加新的数据类型,供升级后的现有接口的新方法或新接口使用。
- 父包的顶层接口在新包中仍然存在,尽管这些接口可能具有新方法、新的接口本地 UDT(如下所述的接口级别扩展)以及
- 接口级别向后兼容的扩展。新包还可以通过包含逻辑上独立的接口来扩展原始包,这些接口仅提供附加功能,而不是核心功能。为此,以下内容可能是理想的
- 新包中的接口需要访问旧包的数据类型。
- 新包中的接口可以扩展一个或多个旧包的接口。
- 扩展原始的向后不兼容性。这是包的主要版本升级,两个版本之间不必有任何关联。在存在关联的情况下,可以使用旧版本包中的类型和旧包接口子集的继承来表达这种关联。
接口结构
对于结构良好的接口,添加不属于原始设计的新类型功能应该需要修改 HIDL 接口。相反,如果您可以或期望在接口的两侧进行更改,从而在不更改接口本身的情况下引入新功能,那么该接口的结构就不好。
Treble 支持单独编译的供应商组件和系统组件,其中设备上的 vendor.img
和 system.img
可以单独编译。vendor.img
和 system.img
之间的所有交互都必须明确且彻底地定义,以便它们可以在多年内继续工作。这包括许多 API 表面,但主要的表面是 IPC 机制 HIDL 用于 system.img
/vendor.img
边界上的进程间通信。
要求
通过 HIDL 传递的所有数据都必须显式定义。为了确保即使在单独编译或独立开发的情况下,实现和客户端也能继续协同工作,数据必须符合以下要求
- 可以直接在 HIDL 中描述(使用结构体、枚举等),并带有语义名称和含义。
- 可以由公共标准(如 ISO/IEC 7816)描述。
- 可以由硬件标准或硬件的物理布局描述。
- 如果需要,可以是opaque数据(例如公钥、ID 等)。
如果使用 opaque 数据,则必须仅由 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/
下。版本为 $m.$n
的包 android.hardware.
[name1
].[name2
]… 位于 hardware/interfaces/name1/name2/
…/$m.$n/
下;例如,版本为 3.4
的包 android.hardware.camera
位于目录 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 的包名称和包版本组成。完全限定的名称仅在声明类型实例时使用,而不是在定义类型本身时使用。例如,假设版本为 1.0
的包 android.hardware.nfc,
定义了一个名为 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(一种嵌套声明类型),因此使用点来访问名称。
例如,如果在版本为 1.0
的包 android.hardware.example
中的公共类型文件中定义了以下嵌套声明
// 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
。在包或接口级别,您必须通过 Foo
引用 Bar
: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 是枚举类型,则枚举类型的每个值都有一个完全限定的名称,该名称以枚举类型的完全限定名称开头,后跟一个冒号,然后是枚举值的名称。例如,假设版本为 1.0
的包 android.hardware.nfc,
定义了一个枚举类型 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
的声明与 NfcData
在同一包(android.hardware.nfc
)的同一版本(1.0
)中进行,如下所示
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
在包 android.hardware.nfc
的版本 1.1
中声明,1.1
按应有的方式导入 1.0
(请参阅包级别扩展),并且定义仅指定 UDT 名称
struct ExtendedNfcData { NfcData base; // … additional members };
编译器查找任何名为 NfcData
的 UDT,并在版本 1.0
的 android.hardware.nfc
中找到一个,从而得到完全限定的 UDT 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
使用规则 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 包都包含一个 types.hal
文件,其中包含在该包中参与的所有接口之间共享的 UDT。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
语句声明。在包声明之后,可能会列出零个或多个接口级别的导入(部分或整个包)。例如
package android.hardware.nfc@1.0;
在 HIDL 中,接口可以使用 extends
关键字从其他接口继承。为了使接口扩展另一个接口,它必须通过 import
语句访问它。被扩展的接口的名称(基接口)遵循上面解释的类型名称限定规则。一个接口只能从一个接口继承;HIDL 不支持多重继承。
下面的升级版本控制示例使用以下包
// 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 的所有条件都必须为真
规则 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
,这仍然不是有效的升级。
升级接口
要将 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); }
这是 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()
方法。
升级约定
有时接口名称必须重命名扩展接口。我们建议枚举扩展、结构体和联合体与它们扩展的对象具有相同的名称,除非它们差异足够大,需要使用新名称。示例
// 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::IFoo
中 foo
方法的功能。
包级别版本控制
HIDL 版本控制发生在包级别;发布包后,它是不可变的(其接口和 UDT 集无法更改)。包可以通过多种方式相互关联,所有这些都可以通过接口级别继承和通过组合构建 UDT 来表达。
但是,一种类型的关系是严格定义的,并且必须强制执行:包级别向后兼容的继承。在这种情况下,父包是被继承的包,而子包是扩展父包的包。包级别向后兼容的继承规则如下
- 父包的所有顶层接口都由子包中的接口继承。
- 也可以将新接口添加到新包中(对与其他包中的其他接口的关系没有限制)。
- 也可以添加新的数据类型,供升级后的现有接口的新方法或新接口使用。
这些规则可以使用 HIDL 接口级别继承和 UDT 组合来实现,但需要元级别知识才能知道这些关系构成了向后兼容的包扩展。此知识推断如下
如果包满足此要求,则 hidl-gen
将强制执行向后兼容性规则。