接口版本控制

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

HIDL 代码结构

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.imgsystem.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.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.(minor-2) 均未定义)或规则 B(如果它是从 @major.(minor-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 不会影响软件包名称相同但主要版本不同的软件包(例如,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 };

如果方法可以有新的语义名称(例如 fooWithLocation),应首选该名称。否则,它应采用与其扩展的内容相似的名称。例如,如果没有更好的备用名称,@1.1::IFoo 中的方法 foo_1_1 可以取代 @1.0::IFoofoo 方法的功能。

软件包级版本编号

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

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

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

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

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