第2章:HIDL——HAL接口定义语言
HIDL,全称是HAL Interface Definition Language。说白了,它就是Android系统里用来定义HAL接口的一种专用语言。你想想看,Framework要跟底层硬件打交道,总得有个明确的契约吧?HIDL就是这份契约。
我个人习惯把HIDL理解成「中间人」。它不关心你底层驱动怎么实现,也不管上层Service怎么调用,它只负责把双方沟通的格式定死。这样,Google能升级Framework,芯片厂商能改驱动,只要接口不变,大家各玩各的,互不干扰。
核心要点:HIDL的核心价值在于解耦。它让Android系统升级时,不需要重新编译所有的HAL库。只要接口定义不变,二进制兼容性就能保证。
2.1 HIDL基础语法
HIDL的语法,其实跟C++很像,但更精简。我刚开始接触时,觉得它就是个「瘦身版」的C++头文件。没有复杂的模板,没有异常处理,就是干干净净的接口声明。
一个典型的HIDL文件长这样:
package android.hardware.light@2.0;
interface ILight {
enum Type : int32_t {
BACKLIGHT,
KEYBOARD,
BUTTONS,
NOTIFICATIONS,
ATTENTION
};
struct LightState {
int32_t color;
int32_t flashMode;
int32_t brightness;
};
getSupportedTypes() generates (vec<Type> types);
setLight(Type type, LightState state) generates (Status status);
};
嗯,这里要注意几个关键点:
- package声明:每个HIDL文件都必须以package开头,后面跟着包名和版本号。版本号用@符号分隔,比如
@2.0。 - interface关键字:定义接口,里面可以包含枚举、结构体和方法。
- 方法定义:方法名后面跟参数列表,然后用
generates关键字指定返回值。注意,HIDL的方法可以有多个返回值。
我的经验:写HIDL文件时,尽量把枚举和结构体定义在接口内部。这样生成的代码更紧凑,调用起来也更直观。我曾经见过有人把所有类型定义放在一个单独的types.hal里,结果找起来特别费劲。
2.2 包与命名空间
包名在HIDL里不是随便起的。它有一套严格的命名规则,直接决定了生成代码的目录结构和C++命名空间。
包名的格式是:vendor.product.hardware.module@version
| 组成部分 | 说明 | 示例 |
|---|---|---|
| vendor | 厂商或组织名 | android, google, qcom |
| product | 产品线或硬件类型 | hardware, system, media |
| module | 具体模块名 | light, camera, audio |
| version | 主版本.次版本 | 2.0, 1.3 |
举个例子,android.hardware.light@2.0这个包名,生成C++代码后,对应的命名空间是:
android::hardware::light::V2_0
目录结构则是:
out/soong/.intermediates/
android/hardware/light/
2.0/
android.hardware.light@2.0_genc++/
为什么会这样设计?我个人的理解是:这种命名方式天然解决了命名冲突问题。不同厂商、不同版本的HAL实现,可以共存于同一个系统里。你想想看,如果Google和Qualcomm都定义了camera接口,没有命名空间隔离,那不乱套了?
避坑指南:我曾经遇到过一个问题:包名里的版本号写成了@2而不是@2.0。结果编译时死活找不到生成的代码。记住,版本号必须包含主版本和次版本,哪怕次版本是0也不能省略。
2.3 数据类型与接口定义
HIDL支持的数据类型,我把它分成三类:基础类型、复合类型和特殊类型。
基础类型
| HIDL类型 | C++对应类型 | 说明 |
|---|---|---|
| int8_t / uint8_t | int8_t / uint8_t | 有/无符号8位整数 |
| int16_t / uint16_t | int16_t / uint16_t | 有/无符号16位整数 |
| int32_t / uint32_t | int32_t / uint32_t | 有/无符号32位整数 |
| int64_t / uint64_t | int64_t / uint64_t | 有/无符号64位整数 |
| float / double | float / double | 浮点数 |
| bool | bool | 布尔值 |
| string | android::hardware::hidl_string | 字符串 |
复合类型
- struct:结构体,跟C++的struct一样,可以包含多个字段。
- enum:枚举,可以指定底层类型,比如
enum Type : int32_t。 - union:联合体,用得比较少,但某些场景下很有用。
- vec<T>:动态数组,相当于C++的
std::vector。 - hidl_array<T, N>:固定大小数组。
特殊类型
- handle:用于传递底层文件描述符或原生句柄。
- MemoryBlock:用于共享内存。
- fmq_sync / fmq_unsync:快速消息队列,用于高效的数据传输。
接口定义时,方法可以有三种模式:
- 单向调用(oneway):调用后立即返回,不等待结果。适合通知类操作。
- 同步调用:默认模式,调用方会阻塞直到收到返回值。
- 回调(callback):通过
generates关键字定义返回值,可以返回多个值。
// 单向调用
oneway notifyEvent(int32_t eventId);
// 同步调用,返回单个值
Status setParameter(int32_t key, int32_t value);
// 回调,返回多个值
getStatus() generates (int32_t code, string message);
我的建议:能用同步调用就别用oneway。oneway虽然快,但没法知道调用是否成功。我在调试一个传感器HAL时,用了oneway传数据,结果底层报错了上层完全不知道,排查了半天才发现问题。
2.4 生成C++代码
HIDL文件写好了,怎么变成C++代码?答案是:用hidl-gen工具。
编译系统会自动调用hidl-gen,但如果你想手动生成看看效果,可以这样:
hidl-gen -o output -L c++-impl \
-r android.hardware:hardware/interfaces \
-r android.hidl:system/libhidl/transport \
android.hardware.light@2.0
这条命令会生成什么?我列一下:
| 生成文件 | 说明 |
|---|---|
| ILight.h | 接口头文件,包含纯虚类 |
| ILight.cpp | 接口实现骨架 |
| LightAll.cpp | 所有类型的序列化代码 |
| BnHwLight.h / BpBwLight.h | Binder通信的本地端和代理端 |
| hwLight.h | HAL实现需要继承的基类 |
你真正需要关心的,其实只有两个文件:
- ILight.h:里面定义了纯虚类
ILight,你的HAL实现需要继承它。 - LightAll.cpp:里面包含了所有类型的序列化和反序列化代码,一般不需要修改。
生成的接口类长这样:
struct ILight : public ::android::hardware::HALInterfaceBase {
// 枚举
enum class Type : int32_t {
BACKLIGHT = 0,
KEYBOARD = 1,
BUTTONS = 2,
NOTIFICATIONS = 3,
ATTENTION = 4
};
// 结构体
struct LightState {
int32_t color;
int32_t flashMode;
int32_t brightness;
};
// 方法
virtual Return<void> getSupportedTypes(
std::function<void(const hidl_vec<Type>&)> _hidl_cb) = 0;
virtual Return<Status> setLight(Type type,
const LightState& state) = 0;
};
看到没?HIDL里的generates关键字,在C++里变成了回调函数参数。这是HIDL实现异步调用的方式。
避坑指南:生成代码时,-r参数指定的根路径必须正确。我曾经因为路径写错,导致生成的代码里#include找不到头文件。建议直接用Android源码里的hardware/interfaces和system/libhidl/transport这两个路径。
知识结构总览
下面这张图,把HIDL的核心知识点串起来了:
从这张图能看出来,HIDL的整个流程是单向的:从.hal源文件出发,经过hidl-gen处理,生成一系列C++代码。你作为HAL开发者,只需要关注最左边和最右边——写好.hal文件,然后实现生成的接口类。
嗯,这一章的内容就到这里。HIDL的语法其实不复杂,但细节很多。尤其是包名和版本号,写错了编译都过不了。我建议你动手写一个简单的light HAL试试,走一遍完整的流程,比看十遍文档都管用。