接口版本控制

HIDL 要求每个用 HIDL 编写的接口都必须进行版本控制。在发布 HAL 接口后,它将被冻结,任何进一步的更改都必须在新版本的接口中进行。虽然给定的已发布接口无法修改,但可以通过另一个接口进行扩展。

HIDL 代码结构

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。
    • 也可以将新接口添加到新包中。
    • 父包的所有数据类型在新包中仍然存在,并且可以由旧包中(可能已重新实现的)方法处理。
    • 也可以添加新的数据类型,供升级后的现有接口的新方法或新接口使用。
  • 接口级别向后兼容的扩展。新包还可以通过包含逻辑上独立的接口来扩展原始包,这些接口仅提供附加功能,而不是核心功能。为此,以下内容可能是理想的
    • 新包中的接口需要访问旧包的数据类型。
    • 新包中的接口可以扩展一个或多个旧包的接口。
  • 扩展原始的向后不兼容性。这是包的主要版本升级,两个版本之间不必有任何关联。在存在关联的情况下,可以使用旧版本包中的类型和旧包接口子集的继承来表达这种关联。

接口结构

对于结构良好的接口,添加不属于原始设计的新类型功能应该需要修改 HIDL 接口。相反,如果您可以或期望在接口的两侧进行更改,从而在不更改接口本身的情况下引入新功能,那么该接口的结构就不好。

Treble 支持单独编译的供应商组件和系统组件,其中设备上的 vendor.imgsystem.img 可以单独编译。vendor.imgsystem.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 引用 BarFoo.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,并找到上面的 typedefNfcData 也在本地查找,但由于未在本地定义,因此使用规则 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.0android.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::typestypes.halandroid.hardware.baz@1.0 中的接口未导入)
  • 来自 android.hardware.qux@1.0IQux.haltypes.hal
  • 来自 android.hardware.quuz@1.0Quuz(假设 Quuztypes.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.0package@major.1、…、package@major.(minor-1) 都必须未定义。
规则 B

以下所有条件都为真

  1. “之前的次要版本有效”:package@major.(minor-1) 必须已定义并遵循相同的规则 A(package@major.0package@major.(minor-2) 中没有一个被定义)或规则 B(如果是从 @major.(minor-2) 升级);

    并且

  2. “继承至少一个同名接口”:存在一个接口 package@major.minor::IFoo,它扩展了 package@major.(minor-1)::IFoo(如果之前的包具有接口);

    并且

  3. “没有继承名称不同的接口”:不得存在 package@major.minor::IBar,它扩展了 package@major.(minor-1)::IBaz,其中 IBarIBaz 是两个不同的名称。如果存在同名接口,则 package@major.minor::IBar 必须扩展 package@major.(minor-k)::IBar,以便不存在更小 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::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.halandroid.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::IFoofoo 方法的功能。

包级别版本控制

HIDL 版本控制发生在包级别;发布包后,它是不可变的(其接口和 UDT 集无法更改)。包可以通过多种方式相互关联,所有这些都可以通过接口级别继承和通过组合构建 UDT 来表达。

但是,一种类型的关系是严格定义的,并且必须强制执行:包级别向后兼容的继承。在这种情况下,包是被继承的包,而包是扩展父包的包。包级别向后兼容的继承规则如下

  1. 父包的所有顶层接口都由子包中的接口继承。
  2. 也可以将新接口添加到新包中(对与其他包中的其他接口的关系没有限制)。
  3. 也可以添加新的数据类型,供升级后的现有接口的新方法或新接口使用。

这些规则可以使用 HIDL 接口级别继承和 UDT 组合来实现,但需要元级别知识才能知道这些关系构成了向后兼容的包扩展。此知识推断如下

如果包满足此要求,则 hidl-gen 将强制执行向后兼容性规则。