HAL 类型
在 Android 8.0 及更高版本中,较低级别的层已重新编写以采用更加模块化的新架构。搭载 Android 8.0 或更高版本的设备必须支持使用 HIDL 语言编写的 HAL,下面列出了一些例外情况。这些 HAL 可以是绑定式 HAL 也可以是直通式 HAL。Android R 也支持使用 AIDL 编写的 HAL。所有 AIDL HAL 均为绑定式。
- 绑定式 HAL。以 HAL 接口定义语言 (HIDL) 或 Android 接口定义语言 (AIDL) 表示的 HAL。这些 HAL 取代了早期 Android 版本中使用的传统 HAL 和旧版 HAL。在绑定式 HAL 中,Android 框架和 HAL 之间通过 Binder 进程间通信 (IPC) 调用进行通信。所有在推出时即搭载了 Android 8.0 或更高版本的设备都必须只支持绑定式 HAL。
- 直通式 HAL。以 HIDL 封装的传统 HAL 或旧版 HAL。这些 HAL 封装了现有的 HAL,可在绑定模式和Same-Process(直通)模式下使用。升级到 Android 8.0 的设备可以使用直通式 HAL。如果有一个 HAL 接口 a.b.c.d@M.N::IFoo,系统会创建两个软件包:
- a.b.c.d@M.N::IFoo-impl。该软件包包含 HAL 的实现,并提供函数 IFoo* HIDL_FETCH_IFoo(const char* name)。在旧版设备上,此软件包经过 dlopen 处理,且实现使用 HIDL_FETCH_IFoo 进行了实例化。您可以使用 hidl-gen、-Lc+±impl 和 -Landroidbp-impl 生成基础代码。
- a.b.c.d@M.N::IFoo-service。打开直通式 HAL,并将其自身注册为 Binder 化服务,从而使同一 HAL 实现能够同时以直通模式和 Binder 化模式使用。
如果有一个类型 IFoo,您可以调用 sp IFoo::getService(string name, bool getStub),以获取对 IFoo 实例的访问权限。
如果 getStub 为 True,则 getService 会尝试仅在直通模式下打开 HAL。如果 getStub 为 False,则 getService 会尝试查找 Binder 化服务;如果未找到,则它会尝试查找直通式服务。除了在 defaultPassthroughServiceImplementation 中,其余情况一律不得使用 getStub 参数。(搭载 Android O 的设备是完全 Binder 化的设备,因此不得在直通模式下打开服务。)
HAL 接口定义语言(简称 HIDL,发音为“hide-l”)是用于指定 HAL 和其用户之间的接口的一种接口描述语言 (IDL),HIDL 旨在用于进程间通信 (IPC)。进程之间的通信采用 Binder 机制。对于必须与进程相关联的代码库,还可以使用直通模式(在 Java 中不受支持)。
HIDL 会尝试尽可能减少复制操作的次数。HIDL 定义的数据以 C++ 标准布局数据结构传递至 C++ 代码,无需解压,可直接使用。此外,HIDL 还提供共享内存接口;由于 RPC 本身有点慢,因此 HIDL 支持两种无需使用 RPC 调用的数据传输方法:共享内存和快速消息队列 (FMQ)。
HAL 实现
HIDL 接口软件包位于 hardware/interfaces 或 vendor/ 目录下(个别情况除外)。顶层 hardware/interfaces 会直接映射到 android.hardware 软件包命名空间;版本是软件包(而不是接口)命名空间下的子目录。
hidl-gen 编译器会将 .hal 文件编译成一组 .h 和 .cpp 文件。这些自动生成的文件可用来构建客户端/服务器实现所关联的共享库。用于构建此共享库的 Android.bp 文件由 hardware/interfaces/update-makefiles.sh 脚本自动生成。每次将新软件包添加到 hardware/interfaces 或在现有软件包中添加/移除 .hal 文件时,您都必须重新运行该脚本,以确保生成的共享库是最新的。
例如,IFoo.hal 示例文件应该位于 hardware/interfaces/samples/1.0 下。IFoo.hal 示例文件会在 samples 软件包中创建一个 IFoo 接口:
package android.hardware.samples@1.0;
interface IFoo {
struct Foo {
int64_t someValue;
handle myHandle;
};
someMethod() generates (vec<uint32_t>);
anotherMethod(Foo foo) generates (int32_t ret);
};
HIDL 软件包中自动生成的文件会关联到与该软件包同名的单个共享库(例如 android.hardware.samples@1.0)。该共享库还会导出单个头文件 IFoo.h,它可以包含在客户端和服务器中。在 Binder 化模式下,使用 hidl-gen 编译器以 IFoo.hal 接口文件作为输入会自动生成以下文件:
- IFoo.h - 描述 C++ 类中的纯 IFoo 接口;它包含 IFoo.hal 文件中的 IFoo 接口中定义的方法和类型(必要时会转换为 C++ 类型)。不包含与用于实现此接口的 RPC 机制(例如 HwBinder)相关的详细信息。该类的命名空间包含软件包名称和版本号,例如 ::android::hardware::samples::IFoo::V1_0。客户端和服务器都包含此头文件:客户端用它来调用方法,服务器用它来实现这些方法。
- IHwFoo.h - 一个头文件,包含用于对接口中使用的数据类型进行序列化的函数的声明。开发者不得直接包含此头文件(它不包含任何类)。
- BpHwFoo.h - 一个类,从 IFoo 继承而来,并描述接口的 HwBinder 代理(客户端)实现。开发者不得直接引用此类。
- BnHwFoo.h - 一个类,保持对 IFoo 实现的引用,并描述接口的 HwBinder 存根(服务器端)实现。开发者不得直接引用此类。
- FooAll.cpp - 一个类,包含 HwBinder 代理和 HwBinder 存根的实现。当客户端调用接口方法时,代理会自动列集来自客户端的参数,并将事务发送到 Binder 内核驱动程序,该内核驱动程序会将事务传送到另一端的存根(该存根随后会调用实际的服务器实现)。
使用软件包中的任何接口的客户端或服务器必须在下面的其中一 (1) 个位置包含该软件包的共享库:
- 在 Android.mk 中:
LOCAL_SHARED_LIBRARIES += android.hardware.samples@1.0- 在 Android.bp 中:
shared_libs: [
/* … */
“android.hardware.samples@1.0”,
],您可能需要包含的其他库
- libhidlbase 包含标准 HIDL 数据类型。从 Android 10 开始,此库还包含先前在libhidltransport 和 libhwbinder 中的所有符号。
- libhidltransport 通过不同的 RPC/IPC 机制处理 HIDL 调用的传输。Android 10 弃用了此库。
- libhwbinder Binder 专用符号。Android 10 弃用了此库。
- libfmq 快速消息队列 IPC。
命名空间:
HIDL 函数和类型(如 Return 和 Void())在命名空间 ::android::hardware 中进行声明。软件包的 C++ 命名空间由软件包名称和版本号确定。例如,hardware/interfaces 下版本为 1.2 的软件包 mypackage 具有以下特质:
C++ 命名空间为 ::android::hardware::mypackage::V1_2 该软件包中 IMyInterface 的完全限定名称为
::android::hardware::mypackage::V1_2::IMyInterface(IMyInterface 是一个标识符,而不是命名空间的一部分)。
在该软件包的 types.hal 文件中定义的类型标识为:::android::hardware::mypackage::V1_2::MyPackageTyp
异步回调:
很多现有的 HAL 实现会与异步硬件通信,这意味着它们需要以异步方式通知客户端已发生的新事件。HIDL 接口可以用作异步回调,因为 HIDL 接口函数可以将 HIDL 接口对象用作参数。
接口文件 IFooCallback.hal 示例
package android.hardware.samples@1.0;
interface IFooCallback {
sendEvent(uint32_t event_id);
sendData(vec<uint8_t> data);
}
IFoo 中采用 IFooCallback 参数的新方法示例:
package android.hardware.samples@1.0;
interface IFoo {
struct Foo {
int64_t someValue;
handle myHandle;
};
someMethod(Foo foo) generates (int32_t ret);
anotherMethod() generates (vec<uint32_t>);
registerCallback(IFooCallback callback);
};
使用 IFoo 接口的客户端是 IFooCallback 接口的服务;它会提供 IFooCallback 的实现:
class FooCallback : public IFooCallback {
Return<void> sendEvent(uint32_t event_id) {
// process the event from the HAL
}
Return<void> sendData(const hidl_vec<uint8_t>& data) {
// process data from the HAL
}
};
HIDL 语法
HIDL 基元与 C++ 数据类型之间的对应关系
HIDL 类型 | C++ 类型 | 头文件/库 |
---|---|---|
enum | enum class | |
uint8_t…uint64_t | uint8_t…uint64_t | <stdint.h> |
int8_t…int64_t | int8_t…int64_t | <stdint.h> |
float | float | |
double | double | |
vec | hidl_vec | libhidlbase |
T[S1][S2]…[SN] | T[S1][S2]…[SN] | |
string | hidl_string | libhidlbase |
handle | hidl_handle | libhidlbase |
safe_union | (custom) struct | |
struct | struct | |
union | union | |
fmq_sync | MQDescriptorSync | libhidlbase |
fmq_unsync | MQDescriptorUnsync | libhidlbase |
HIDL 形式的枚举会变为 C++ 形式的枚举。例如:
enum Mode : uint8_t { WRITE = 1 << 0, READ = 1 << 1 };
enum SpecialMode : Mode { NONE = 0, COMPARE = 1 << 2 };
…会变为:
enum class Mode : uint8_t { WRITE = 1, READ = 2 };
enum class SpecialMode : uint8_t { WRITE = 1, READ = 2, NONE = 0, COMPARE = 4 };
字符串:
hidl_string 类(libhidlbase 的一部分)可用于通过 HIDL 接口传递字符串,它在 /system/libhidl/base/include/hidl/HidlSupport.h 中进行定义。该类中的第一个存储位置是指向其字符缓冲区的指针。
hidl_string 知道如何使用 operator=、隐式类型转换和 .c_str() 函数与 std::string and char*(C 样式的字符串)来回进行转换。HIDL 字符串结构体具有适当的复制构造函数和赋值运算符,可用于:
- 从 std::string 或 C 字符串加载 HIDL 字符串。
- 从 HIDL 字符串创建新的 std::string。
此外,HIDL 字符串还有转换构造函数,因此 C 字符串 (char *) 和 C++ 字符串 (std::string) 可用于采用 HIDL 字符串的方法。
结构体:
HIDL 形式的 struct 只能包含固定大小的数据类型,不能包含任何函数。HIDL 结构体定义会直接映射到 C++ 形式的标准布局 struct,从而确保 struct 具有一致的内存布局。一个结构体可以包含多个指向单独的可变长度缓冲区的 HIDL 类型(包括 handle、string 和 vec)。
内存:
HIDL memory 类型会映射到 libhidlbase 中的 hidl_memory 类,该类表示未映射的共享内存。这是要在 HIDL 中共享内存而必须在进程之间传递的对象。要使用共享内存,需满足以下条件:
- 获取 IAllocator 的实例(当前只有“ashmem”实例可用),并使用该实例分配共享内存。
- IAllocator::allocate() 返回 hidl_memory 对象,该对象可通过 HIDL RPC 传递,并能使用 libhidlmemory 的 mapMemory 函数映射到某个进程。
- mapMemory 返回对可用于访问内存的 sp 对象的引用。(IMemory 和 IAllocator 在 android.hidl.memory@1.0 中进行定义。)
IAllocator 的实例可用于分配内存:
#include <android/hidl/allocator/1.0/IAllocator.h>
#include <android/hidl/memory/1.0/IMemory.h>
#include <hidlmemory/mapping.h>
using ::android::hidl::allocator::V1_0::IAllocator;
using ::android::hidl::memory::V1_0::IMemory;
using ::android::hardware::hidl_memory;
....
sp<IAllocator> ashmemAllocator = IAllocator::getService("ashmem");
ashmemAllocator->allocate(2048, [&](bool success, const hidl_memory& mem) {
if (!success) { /* error */ }
// now you can use the hidl_memory object 'mem' or pass it around
}));
对内存的实际更改必须通过 IMemory 对象完成(要么在创建 mem 的一端完成,要么在通过 HIDL RPC 接收它的一端完成)。
// Same includes as above
sp<IMemory> memory = mapMemory(mem);
void* data = memory->getPointer();
memory->update();
// update memory however you wish after calling update and before calling commit
data[0] = 42;
memory->commit();
// …
memory->update(); // the same memory can be updated multiple times
// …
memory->commit();
函数:
对于具有返回值的每个 HIDL 函数(具有 generates 语句),该函数的 C++ 参数列表中都有一个附加参数:使用 HIDL 函数的返回值调用的回调函数。有一种例外情况:如果 generates 子句包含直接映射到 C++ 基元的单个参数,则会使用回调省略机制(回调会被移除,而返回值则会通过正常的 return 语句从函数返回)。
generates 语句可以产生三种类型的函数签名:
- 如果只有一个属于 C++ 基元的返回值,generates 返回值会由 Return 对象中函数的值返回。
- 如果情况更复杂,generates 返回值则会通过随函数调用本身一起提供的回调参数返回,而函数则返回 Return。
- 如果没有 generates语句,函数会返回 Return<void
由于客户端和服务器端函数具有相同的签名,因此服务器端函数必须返回 Return 类型(即使其实现并不会指出传输错误)。Return 对象使用 Return(myTValue) 进行构建(也可以通过 mTValue 隐式构建,例如在 return 语句中),而 Return 对象则使用 Void() 进行构建。
Return 对象与其 T 值相互之间可以进行隐式转换。您可以在 Return 对象中检查是否存在传输错误,只需调用其 isOk() 方法即可。这项检查不是必需的;不过,如果发生了一个错误,而您未在 Return 对象销毁前对该错误进行检查,或尝试进行了 T 值转换,则客户端进程将会终止并记录一个错误。如果 isOk() 表明存在由开发者代码中的逻辑错误(例如将 nullptr 作为同步回调进行传递)导致的传输错误或失败调用,则可以对 Return 对象调用 description() 以返回适合日志记录的字符串。在这种情况下,您无法确定因调用失败而在服务器上执行的代码可能有多少。另外,您还可以使用 isDeadObject() 方法。此方法表明,之所以会显示 !isOk(),是因为远程对象已崩溃或已不存在。isDeadObject() 一律表示 !isOk()。
另外,您还可以使用 Return<*>::withDefault 方法。此方法会在返回值为 !isOk() 的情况下提供一个值。此方法还会自动将返回对象标记为正常,以免客户端进程遭到终止。
HAL Demo
- 服务器实现:
实现 IFoo 接口的服务器必须包含自动生成的 IFoo 头文件:
#include <android/hardware/samples/1.0/IFoo.h>
该标头由 IFoo 接口的共享库自动导出以进行链接。IFoo.hal 示例:
// IFoo.hal
interface IFoo {
someMethod() generates (vec<uint32_t>);
...
}
IFoo 接口的服务器实现的示例框架:
// From the IFoo.h header
using android::hardware::samples::V1_0::IFoo;
class FooImpl : public IFoo {
Return<void> someMethod(foo my_foo, someMethod_cb _cb) {
vec<uint32_t> return_data;
// Compute return_data
_cb(return_data);
return Void();
}
...
};
要使服务器接口的实现可供客户端使用,您可以:
向 hwservicemanager 注册接口实现(详情见下文)
或者
将接口实现作为接口方法的参数进行传递(详情见异步回调部分)。
注册接口实现时,hwservicemanager 进程会按名称和版本号跟踪设备上正在运行的已注册 HIDL 接口。服务器可以按名称注册 HIDL 接口实现,而客户端则可以按名称和版本号请求服务实现。该进程可提供 HIDL 接口android.hidl.manager@1.0::IServiceManager。
每个自动生成的 HIDL 接口头文件(例如 IFoo.h)都有一个 registerAsService() 方法,可用于向 hwservicemanager 注册接口实现。唯一一个必需的参数是接口实现的名称,因为稍后客户端将使用此名称从 hwservicemanager 检索接口:
::android::sp<IFoo> myFoo = new FooImpl();
::android::sp<IFoo> mySecondFoo = new FooAnotherImpl();
status_t status = myFoo->registerAsService();
status_t anotherStatus = mySecondFoo->registerAsService("another_foo");
hwservicemanager 会将 [package@version::interface, instance_name] 组合视为唯一,以使不同的接口(或同一接口的不同版本)能够采用完全相同的实例名称无冲突地注册。如果您调用的 registerAsService() 具有完全相同的软件包版本、接口和实例名称,则 hwservicemanager 将丢弃对先前注册的服务的引用,并使用新的服务。
- 客户端实现
和服务器一样,客户端也必须 #include 其引用的每个接口
#include <android/hardware/samples/1.0/IFoo.h>
客户端可以通过两种方式获取接口:
通过 I::getService(通过 hwservicemanager)
通过接口方法
每个自动生成的接口头文件都有一个静态 getService 方法,可用于从 hwservicemanager 检索服务实例:
// getService will return nullptr if the service can't be found
sp<IFoo> myFoo = IFoo::getService();
sp<IFoo> myAlternateFoo = IFoo::getService("another_foo");
现在,客户端有一个 IFoo 接口,并可以向其(将其当作本地类实现)调用方法。实际上,实现可以在同一个进程中运行,也可以在不同的进程中运行,甚至还可以在另一个设备上运行(通过 HAL 远程处理)。由于客户端在 1.0 版软件包中包含的 IFoo 对象上调用 getService,因此仅当服务器实现与 1.0 客户端兼容时,hwservicemanager 才会返回该实现。
此外,您也可以使用 castFrom 方法,在不同的接口之间进行类型转换。该方法会通过以下方式发挥作用:对远程接口进行 IPC 调用,以确保底层类型与正在请求的类型相同。如果请求的类型不可用,则返回 nullptr。
sp<V1_0::IFoo> foo1_0 = V1_0::IFoo::getService();
sp<V1_1::IFoo> foo1_1 = V1_1::IFoo::castFrom(foo1_0);