数据类型

本部分将介绍 HIDL 数据类型。如需了解实现详情,请参阅 HIDL C++(对于 C++ 实现)或 HIDL Java(对于 Java 实现)。

与 C++ 的相似之处包括:

  • structs 使用 C++ 语法;unions 默认支持 C++ 语法。结构体和联合都必须具有名称;不支持匿名结构体和联合。
  • HIDL 中允许使用 typedef(与在 C++ 中一样)。
  • 允许使用 C++ 样式的注释,并且将此类注释复制到生成的头文件中。

与 Java 的相似之处包括:

  • 对于每个文件,HIDL 都会定义一个 Java 样式的命名空间,并且这些命名空间必须以 android.hardware. 开头。生成的 C++ 命名空间为 ::android::hardware::…
  • 文件的所有定义都包含在一个 Java 样式的 interface 封装容器中。
  • HIDL 数组声明遵循 Java 样式,而非 C++ 样式。例如:
    struct Point {
        int32_t x;
        int32_t y;
    };
    Point[3] triangle;   // sized array
    
  • 备注类似于 javadoc 格式。

数据表示法

采用标准布局(plain-old-data 类型要求的子集)的 structunion 在所生成的 C++ 代码中具有一致的内存布局,这是通过 structunion 成员上的显式对齐属性实现的。

基元 HIDL 类型以及 enumbitfield 类型(始终从基元类型派生而来)会映射到标准 C++ 类型,例如 cstdint 中的 std::uint32_t

由于 Java 不支持无符号的类型,因此无符号的 HIDL 类型会映射到相应的有符号 Java 类型。结构体会映射到 Java 类;数组会映射到 Java 数组;Java 目前不支持联合。字符串在内部以 UTF8 格式存储。由于 Java 仅支持 UTF16 字符串,因此发送到或来自 Java 实现的字符串值会进行转换;在重新转换回来后,字符串值可能不会与原来的值完全相同,这是因为字符集并非总能顺畅映射。

在 C++ 中通过 IPC 接收的数据会被标记为 const,并存储在仅在函数调用期间存在的只读内存中。在 Java 中通过 IPC 接收的数据已被复制到 Java 对象中,因此无需进行额外的复制操作即可保留下来(并且可以对其进行修改)。

注释

可以将 Java 样式的注释添加到类型声明中。注释由 HIDL 编译器的供应商测试套件 (VTS) 后端解析,但 HIDL 编译器实际上并不理解任何此类经过解析的注释。相反,经过解析的 VTS 注释将由 VTS 编译器 (VTSC) 处理。

注释使用 Java 语法:@annotation@annotation(value)@annotation(id=value, id=value…),其中值可以是常量表达式、字符串或用 {} 括起来的一系列值,正如在 Java 中一样。可以将多个名称相同的注释附加到同一项中。

前向声明

在 HIDL 中,结构体不能采用前向声明,因此无法实现用户定义的自指数据类型(例如,您不能在 HIDL 中描述关联的列表,也不能描述树)。大多数现有(Android 8.x 之前的)HAL 都对使用前向声明有限制,这种限制可以通过重新排列数据结构声明来移除。

由于存在这种限制,因此可以通过简单的深层复制按值复制数据结构,而无需跟踪可能在一个自引用数据结构中多次出现的指针值。如果将同一项数据传递两次(例如,使用两个方法参数或使用两个指向该数据的 vec<T>),会生成并传送两个单独的副本。

嵌套式声明

HIDL 支持根据需要嵌套任意多层的声明(有一种例外情况,请见下方的备注)。例如:

interface IFoo {
    uint32_t[3][4][5][6] multidimArray;

    vec<vec<vec<int8_t>>> multidimVector;

    vec<bool[4]> arrayVec;

    struct foo {
        struct bar {
            uint32_t val;
        };
        bar b;
    }
    struct baz {
        foo f;
        foo.bar fb; // HIDL uses dots to access nested type names
    }
    …

例外情况是:接口类型只能嵌入到 vec<T> 中,并且只能嵌套一层(不能出现 vec<vec<IFoo>> 这样的情况)。

原始指针语法

HIDL 语言不使用 *,并且不支持 C/C++ 原始指针的全面灵活性。如需详细了解 HIDL 如何封装指针和数组/矢量,请参阅 vec<T> 模板

接口

interface 关键字有以下两种用途。

  • 用于打开 .hal 文件中接口的定义。
  • 可用作结构体/联合字段、方法参数和返回项中的特殊类型。该关键字被视为一般接口,与 android.hidl.base@1.0::IBase 同义。

例如,IServiceManager 具有以下方法:

get(string fqName, string name) generates (interface service);

该方法可按名称查找某个接口。此外,该方法与使用 android.hidl.base@1.0::IBase 替换接口完全一样。

接口只能以两种方式传递:作为顶级参数,或作为 vec<IMyInterface> 的成员。它们不能是嵌套式矢量、结构体、数组或联合的成员。

MQDescriptorSync 和 MQDescriptorUnsync

MQDescriptorSyncMQDescriptorUnsync 类型用于通过 HIDL 接口传递已同步或未同步的快速消息队列 (FMQ) 描述符。如需了解详情,请参阅 HIDL C++(Java 不支持 FMQ)。

memory 类型

memory 类型用于表示 HIDL 中未映射的共享内存。只有 C++ 支持该类型。可以在接收端使用这种类型的值初始化 IMemory 对象,从而映射内存并使其可用。如需了解详情,请参阅 HIDL C++

警告:位于共享内存中的结构化数据所属的类型必须符合以下条件:其格式在传递 memory 的接口版本的生命周期内绝不会改变。否则,HAL 可能会发生严重的兼容性问题。

pointer 类型

pointer 类型仅供 HIDL 内部使用。

bitfield<T> 类型模板

bitfield<T>(其中 T用户定义的枚举)表明该值是 T 中定义的枚举值的按位“或”值。在生成的代码中,bitfield<T> 显示为 T 的基础类型。例如:

enum Flag : uint8_t {
    HAS_FOO = 1 << 0,
    HAS_BAR = 1 << 1,
    HAS_BAZ = 1 << 2
};
typedef bitfield<Flag> Flags;
setFlags(Flags flags) generates (bool success);

编译器会按照处理 uint8_t 的相同方式处理 Flag 类型。

为什么不使用 (u)int8_t/(u)int16_t/(u)int32_t/(u)int64_t?使用 bitfield 可向读取器提供额外的 HAL 信息,读取器现在知道 setFlags 采用 Flag 的按位“或”值(即,知道使用 int16_t 调用 setFlags 是无效的)。如果没有 bitfield,该信息只能通过文档传达。此外,VTS 实际上可以检查标记的值是否为 Flag 的按位“或”值。

句柄基元类型

警告:任何类型的地址(哪怕是实体设备地址)都不得是原生句柄的一部分。在进程之间传递该信息很危险,会导致进程容易受到攻击。在进程之间传递的任何值都必须先经过验证,然后才能用于在进程内查找分配的内存。否则,错误的句柄可能会导致内存访问错误或内存损坏。

HIDL 语义是按值复制,这意味着参数会被复制。所有大型数据或需要在进程之间共享的数据(例如同步栅栏)都是通过传递指向以下持久对象的文件描述符进行处理:针对共享内存的 ashmem、实际文件或可隐藏在文件描述符后的任何其他内容。Binder 驱动程序会将文件描述符复制到其他进程中。

native_handle_t

Android 支持 native_handle_t,这是 libcutils 中定义的一般句柄概念。

typedef struct native_handle
{
  int version;        /* sizeof(native_handle_t) */
  int numFds;         /* number of file-descriptors at &data[0] */
  int numInts;        /* number of ints at &data[numFds] */
  int data[0];        /* numFds + numInts ints */
} native_handle_t;

原生句柄是整数和文件描述符的集合(按值传递)。单个文件描述符可存储在没有整数、包含单个文件描述符的原生句柄中。使用封装有 handle 基元类型的原生句柄的传递句柄可确保将相应的原生句柄直接包含在 HIDL 中。

native_handle_t 的大小可变,因此无法直接包含在结构体中。句柄字段会生成指向单独分配的 native_handle_t 的指针。

在早期版本的 Android 中,原生句柄是使用 libcutils 中的相同函数创建的。在 Android 8.0 及更高版本中,这些函数现在已被复制到 android::hardware::hidl 命名空间或移至 NDK。HIDL 自动生成的代码会自动对这些函数进行序列化和反序列化,而无需用户编写的代码参与。

句柄和文件描述符所有权

当您调用传递(或返回)hidl_handle 对象(复合类型的顶级或一部分)的 HIDL 接口方法时,其中包含的文件描述符的所有权如下所述:

  • hidl_handle 对象作为参数传递的调用方会保留对其封装的 native_handle_t 中包含的文件描述符的所有权;该调用方在完成对这些文件描述符的操作后,必须将这些文件描述符关闭。
  • 通过将 hidl_handle 对象传递到 _cb 函数来返回该对象的进程会保留对该对象封装的 native_handle_t 中包含的文件描述符的所有权;该进程在完成对这些文件描述符的操作后,必须将这些文件描述符关闭。
  • 接收 hidl_handle传输拥有对相应对象封装的 native_handle_t 中的文件描述符的所有权;接收器可在事务回调期间按原样使用这些文件描述符,但如果想在回调完成后继续使用这些文件描述符,则必须克隆原生句柄。事务完成时,传输将自动对文件描述符执行 close() 操作。

HIDL 不支持在 Java 中使用句柄(因为 Java 根本不支持句柄)。

有大小的数组

对于 HIDL 结构体中有大小的数组,其元素可以是结构体可包含的任何类型:

struct foo {
uint32_t[3] x; // array is contained in foo
};

字符串

字符串在 C++ 和 Java 中的显示方式不同,但基础传输存储类型是 C++ 结构。如需了解详情,请参阅 HIDL C++ 数据类型HIDL Java 数据类型

注意:通过 HIDL 接口将字符串传递到 Java 或从 Java 传递字符串(包括从 Java 传递到 Java)将会导致字符集转换,而此项转换可能无法精确保留原始编码。

vec<T> 类型模板

vec<T> 模板表示包含 T 实例的可变大小的缓冲区。

T 可以是以下项之一:

  • 基元类型(例如 uint32_t)
  • 字符串
  • 用户定义的枚举
  • 用户定义的结构体
  • 接口,或 interface 关键字(vec<IFoo>vec<interface> 仅在作为顶级参数时受支持)
  • 句柄
  • bitfield<U>
  • vec<U>,其中 U 可以是此列表中的任何一项,接口除外(例如,vec<vec<IFoo>> 不受支持)
  • U[](有大小的 U 数组),其中 U 可以是此列表中的任何一项,接口除外

用户定义的类型

本部分介绍了用户定义的类型。

枚举

HIDL 不支持匿名枚举。另一方面,HIDL 中的枚举与 C++11 类似:

enum name : type { enumerator , enumerator = constexpr , …  }

基本枚举是根据 HIDL 中的某个整数类型而定义的。如果没有为基于整数类型的枚举的第一个枚举器指定值,则此值默认为 0。如果没有为后面的枚举器指定值,则此值默认为先前的值加 1。例如:

// RED == 0
// BLUE == 4 (GREEN + 1)
enum Color : uint32_t { RED, GREEN = 3, BLUE }

枚举也可以从先前定义的枚举继承。如果没有为子枚举的第一个枚举器指定值(在本例中为 FullSpectrumColor),则此值默认为父枚举的最后一个枚举器的值加 1。例如:

// ULTRAVIOLET == 5 (Color:BLUE + 1)
enum FullSpectrumColor : Color { ULTRAVIOLET }

警告:枚举继承的作用顺序与大多数其他类型的集成是相反的。子枚举值不可用作父枚举值。这是因为子枚举包含的值多于父枚举。但是,父枚举值可以安全地用作子枚举值,因为根据定义,子枚举值是父枚举值的超集。在设计接口时请谨记这一点,因为这意味着在以后的接口迭代中,引用父枚举的类型无法引用子枚举。

枚举的值通过冒号语法(而不是像嵌套式类型一样使用点语法)引用。语法是 Type:VALUE_NAME。如果在相同的枚举类型或子类型中引用枚举的值,则无需指定类型。示例:

enum Grayscale : uint32_t { BLACK = 0, WHITE = BLACK + 1 };
enum Color : Grayscale { RED = WHITE + 1 };
enum Unrelated : uint32_t { FOO = Color:RED + 1 };

从 Android 10 开始,枚举具有可以在常量表达式中使用的 len 属性。MyEnum::len 是相应枚举中的条目总数。这不同于值的总数,当值重复时,值的总数可能会较小。

结构体

HIDL 不支持匿名结构体。另一方面,HIDL 中的结构体与 C 非常类似。

HIDL 不支持完全包含在结构体内且长度可变的数据结构。这包括 C/C++ 中有时用作结构体最后一个字段且长度不定的数组(有时会看到其大小为 [0])。HIDL vec<T> 表示数据存储在单独的缓冲区中且大小动态变化的数组;此类实例由 struct 中的 vec<T> 的实例表示。

同样,string 可包含在 struct 中(关联的缓冲区是相互独立的)。在生成的 C++ 代码中,HIDL 句柄类型的实例通过指向实际原生句柄的指针来表示,因为基础数据类型的实例的长度可变。

联合

HIDL 不支持匿名联合。另一方面,联合与 C 类似。

联合不能包含修正类型(指针、文件描述符、Binder 对象等等)。它们不需要特殊字段或关联的类型,只需通过 memcpy() 或等效函数即可复制。联合不能直接包含(或通过其他数据结构包含)需要设置 Binder 偏移量(即句柄或 Binder 接口引用)的任何内容。例如:

union UnionType {
uint32_t a;
//  vec<uint32_t> r;  // Error: can't contain a vec<T>
uint8_t b;1
};
fun8(UnionType info); // Legal

联合还可以在结构体中进行声明。例如:

struct MyStruct {
    union MyUnion {
      uint32_t a;
      uint8_t b;
    }; // declares type but not member

    union MyUnion2 {
      uint32_t a;
      uint8_t b;
    } data; // declares type but not member
  }