版本编号

HIDL 要求每个使用 HIDL 编写的接口均必须带有版本编号。HAL 接口一经发布便会被冻结,如果要做任何进一步的更改,都只能在接口的新版本中进行。虽然无法对指定的已发布接口进行修改,但可通过其他接口对其进行扩展。

HIDL 代码结构

HIDL 代码按用户定义的类型、接口和软件包进行组织

  • 用户定义的类型 (UDT):HIDL 能够提供对一组基本数据类型的访问权限,这些数据类型可用于通过结构、联合和枚举组成更复杂的类型。UDT 会被传递到接口的方法。可以在软件包级定义 UDT(针对所有接口的通用 UDT),也可以在本地针对某个接口定义 UDT。
  • 接口:作为 HIDL 的基本构造块,接口由 UDT 和方法声明组成。接口也可以继承自其他接口。
  • 软件包:整理相关 HIDL 接口及其操作的数据类型。软件包通过名称和版本进行标识,包括以下内容:
    • 称为 types.hal 的数据类型定义文件。
    • 零个或多个接口,每个都位于各自的 .hal 文件中。

数据类型定义文件 types.hal 中仅包含 UDT(所有软件包级 UDT 都保存在一个文件中)。采用目标语言的表示法可用于软件包中的所有接口。

版本编号理念

针对指定版本(如 1.0)发布后,HIDL 软件包(如 android.hardware.nfc)便不可再改变;您无法对其进行更改。如果要对已发布软件包中的接口进行修改,或要对其 UDT 进行任何更改,都只能在另一个软件包中进行。

在 HIDL 中,版本编号是在软件包级而非接口级应用,并且软件包中的所有接口和 UDT 共用同一个版本。软件包版本遵循语义化版本编号规则,不含补丁程序级别和编译元数据组成部分。在指定的软件包中,minor 版本更新意味着新版本的软件包向后兼容旧软件包,而 major 版本更新意味着新版本的软件包不向后兼容旧软件包。

从概念上来讲,软件包可通过以下方式之一与另一个软件包相关:

  • 完全不相关
  • 软件包级向后兼容的可扩展性。软件包的新 minor 版本升级(下一个递增的修订版本)中会出现这种情况;新软件包拥有与旧软件包一样的名称和 major 版本,但其 minor 版本会更高。从功能上来讲,新软件包是旧软件包的超集,也就是说:
    • 父级软件包的顶级接口会包含在新的软件包中,不过这些接口可以在 types.hal 中有新的方法、新的接口本地 UDT(下文所述的接口级扩展)和新的 UDT。
    • 新接口也可以添加到新软件包中。
    • 父级软件包的所有数据类型均会包含在新软件包中,并且可由来自旧软件包中的方法(可能经过了重新实现)来处理。
    • 新数据类型也可以添加到新软件包中,以供升级的现有接口的新方法使用,或供新接口使用。
  • 接口级向后兼容的可扩展性。新软件包还可以扩展原始软件包,方法是包含逻辑上独立的接口,这些接口仅提供附加功能,并不提供核心功能。要实现这一目的,可能需要满足以下条件:
    • 新软件包中的接口需要依赖于旧软件包的数据类型。
    • 新软件包中的接口可以扩展一个或多个旧软件包中的接口。
  • 扩展原始的向后不兼容性。这是软件包的一种 major 版本升级,并且新旧两个版本之间不需要存在任何关联。如果存在关联,则这种关联可以通过以下方式来表示:组合旧版本软件包中的类型,以及继承旧软件包中的部分接口。

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.halandroid.hardware.baz@1.0 中的接口不会被导入)
  • android.hardware.qux@1.0 中的 IQux.haltypes.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.0package@major.1package@major.(minor-1))必须均未定义。
规则 B

以下各项均为 true:

  1. “以前的 minor 版本有效”:package@major.(minor-1) 必须已定义,并且遵循相同的规则 A(从 package@major.0package@major.(major-2) 均未定义)或规则 B(如果它是从 @major.(major-2) 升级而来);



  2. “继承至少一个具有相同名称的接口”:存在扩展 package@major.(minor-1)::IFoo 的接口 package@major.minor::IFoo



  3. “没有具有不同名称的继承接口”:不得存在扩展 package@major.(minor-1)::IBazpackage@major.minor::IBar,其中 IBarIBaz 是两个不同的名称。如果存在具有相同名称的接口,则 package@major.minor::IBar 必须扩展 package@major.(minor-k)::IBar,以确保不存在 k 较小的 IBar。

由于规则 A:

  • 软件包可以使用任何起始 minor 版本号(例如,android.hardware.biometrics.fingerprint 的起始版本号是 @2.1)。
  • android.hardware.foo@1.0 未定义”这项要求意味着目录 hardware/interfaces/foo/1.0 甚至不应存在。

不过,规则 A 不会影响软件包名称相同但 major 版本不同的软件包(例如,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::IBaseIBase 本身除外)。

必须同时遵循 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.hal1.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 };

除非方法有必要使用新名称,否则应采用与其扩展的内容相似的名称。例如,@1.1::IFoo 中的方法 foo_1_1 可以取代 @1.0::IFoofoo 方法的功能。

软件包级版本编号

HIDL 版本编号在软件包级进行;软件包一经发布,便不可再改变(它的一套接口和 UDT 无法更改)。软件包可通过多种方式彼此建立关系,所有这些关系都可通过接口级继承和构建 UDT 的组合(按构成)来表示。

不过,有一种类型的关系经过严格定义,且必须强制执行,即软件包级向后兼容的继承。在这种情况下,父级软件包是被继承的软件包,而子软件包是扩展父级的软件包。软件包级向后兼容的继承规则如下:

  1. 父级软件包的所有接口都会被子软件包中的接口继承。
  2. 父级软件包中的所有数据类型均会包含在新软件包中,并且可由来自旧软件包中的方法(可能经过了重新实现)来处理。
  3. 新接口也可以添加到新软件包中(与其他软件包中其他接口的关系不受限制)。
  4. 新数据类型也可以添加到新软件包中,以供升级的现有接口的新方法使用,或供新接口使用。

这些规则可以使用 HIDL 接口级继承和 UDT 构成来实现,但需要元级知识才能了解这些关系如何构成向后兼容的软件包扩展。元级知识按以下方式推断:

如果软件包符合这一要求,则 hidl-gen 会强制执行向后兼容性规则。