HIDL 要求每个使用 HIDL 编写的接口均必须带有版本编号。HAL 接口一经发布便会被冻结,如果要做任何进一步的更改,都只能在接口的新版本中进行。虽然无法对指定的已发布接口进行修改,但可通过其他接口对其进行扩展。
HIDL 代码结构
- 用户定义的类型 (UDT)。HIDL 能够提供对一组基本数据类型的访问权限,这些数据类型可用于通过结构、联合和枚举组成更复杂的类型。UDT 会被传递到接口的方法。可以在软件包级定义 UDT(针对所有接口的通用 UDT),也可以在本地针对某个接口定义 UDT。
- 接口。作为 HIDL 的基本构造块,接口由 UDT 和方法声明组成。接口也可以继承自其他接口。
- 软件包。整理相关 HIDL 接口及其操作的数据类型。软件包通过名称和版本进行标识,包括以下内容:
- 称为
types.hal
的数据类型定义文件。 - 零个或多个接口,每个都位于各自的
.hal
文件中。
- 称为
数据类型定义文件 types.hal
中仅包含 UDT(所有软件包级 UDT 都保存在一个文件中)。采用目标语言的表示法可用于软件包中的所有接口。
版本编号理念
针对指定版本(如 android.hardware.nfc
)发布后,HIDL 软件包(如 1.0
)便不可再改变;您无法对其进行更改。如果要对已发布软件包中的接口进行修改,或要对其 UDT 进行任何更改,都只能在另一个软件包中进行。
在 HIDL 中,版本编号是在软件包级而非接口级应用,并且软件包中的所有接口和 UDT 共用同一个版本。软件包版本遵循语义化版本编号规则,不含补丁级别和构建元数据组成部分。在指定的软件包中,次要版本更新意味着新版本的软件包向后兼容旧软件包,而主要版本更新意味着新版本的软件包不向后兼容旧软件包。
从概念上来讲,软件包可通过以下方式之一与另一个软件包相关:
- 完全不相关。
- 软件包级向后兼容的可扩展性。软件包的新 minor 版本升级(下一个递增的修订版本)中会出现这种情况;新软件包拥有与旧软件包一样的名称和 major 版本,但其 minor 版本会更高。从功能上来讲,新软件包是旧软件包的超集,也就是说:
- 父级软件包的顶级接口会包含在新的软件包中,不过这些接口可以在
types.hal
中有新的方法、新的接口本地 UDT(下文所述的接口级扩展)和新的 UDT。 - 新接口也可以添加到新软件包中。
- 父级软件包的所有数据类型均会包含在新软件包中,并且可由来自旧软件包中的方法(可能经过了重新实现)来处理。
- 新数据类型也可以添加到新软件包中,以供升级的现有接口的新方法使用,或供新接口使用。
- 父级软件包的顶级接口会包含在新的软件包中,不过这些接口可以在
- 接口级向后兼容的可扩展性。新软件包还可以扩展原始软件包,方法是包含逻辑上独立的接口,这些接口仅提供附加功能,并不提供核心功能。若要实现这一目的,可能需要满足以下条件:
- 新软件包中的接口需要依赖于旧软件包的数据类型。
- 新软件包中的接口可以扩展一个或多个旧软件包中的接口。
- 扩展原始的向后不兼容性。这是软件包的一种 major 版本升级,并且新旧两个版本之间不需要存在任何关联。如果存在关联,这种关联可以通过以下方式来表示:组合旧版本软件包中的类型,以及继承旧软件包中的部分接口。
接口结构
对于结构合理的接口,要添加不属于原始设计的新类型的功能,应该需要修改 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/
下。$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
是软件包的点分隔 major.minor 版本格式(例如,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
。
在上述两种情况下,只有在 Foo
的声明范围内才能使用 Bar
来引用 Bar
。在软件包级或接口级,必须通过 Foo
:Foo.Bar
来引用 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
的同一版本 (1.0
) 的同一软件包 (android.hardware.nfc
) 中声明的,如下所示:
struct ExtendedNfcData { NfcData base; // … additional members };
HIDL 编译器会填上当前软件包中的软件包名称和版本名称,以生成完全限定的 UDT 名称 android.hardware.nfc@1.0::NfcData
。由于该名称在当前软件包中已存在(假设它已正确导入),因此它会用于声明。
仅当以下条件之一为 true 时,才会导入当前软件包中的名称:
- 使用
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
版(请参阅软件包级扩展),且定义只会指定 UDT 名称:
struct ExtendedNfcData { NfcData base; // … additional members };
编译器查找名为 NfcData
的所有 UDT,并在 1.0
版本的 android.hardware.nfc
中找到一个,从而产生 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
是自动导入的)。 - 使用规则 2 内插
IFooCallback
后得到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。不论 UDT 是在 types.hal
中还是在接口声明中声明的,HIDL 类型始终是公开的,您可以在这些类型的定义范围之外访问它们。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 必须为 true,或 B 中的所有项必须为 true:
规则 A | “是起始 minor 版本”:所有之前的 minor 版本(package@major.0 、package@major.1 …package@major.(minor-1) )必须均未定义。
|
---|
规则 B | 以下各项均为 true:
|
---|
由于规则 A:
- 软件包可以使用任何起始 minor 版本号(例如,
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
中 1.0
版本的 android.hardware.example
软件包级 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
会强制执行向后兼容性规则。