3. HIDL接口定义语言:.hal文件语法、数据类型详解、包名与版本管理
好,我们正式进入HIDL的核心——接口定义语言。说白了,.hal文件就是HIDL世界的“头文件”加“协议描述”。你写好了它,C++和Java两端才能愉快地通信。我刚开始接触HIDL时,觉得这玩意儿不就是个IDL吗?跟AIDL能有多大区别?结果一上手就踩了坑——包名写错、版本号对不上、数据类型用混了,编译直接报错。嗯,今天我们就把这些坑一个个填平。
3.1 .hal文件的基本骨架
一个标准的.hal文件,长什么样?我给你看个最简例子:
package android.hardware.light@2.0;
interface ILight {
getLights() generates (vec<LightState> types);
setLight(LightState type, LightState state) generates (Status status);
};
看到没?就三部分:包声明、接口声明、方法声明。但这里面的门道可不少。
核心原则:.hal文件只描述接口契约,不包含实现。你写的每一个方法签名,都是驱动层和框架层之间的“握手协议”。
3.2 包名与版本管理——最容易翻车的地方
包名的格式是固定的:
package <公司域名倒写>.<硬件模块名>@<主版本号>.<次版本号>;
举个例子:package vendor.example.led@1.0;
这里有几个硬性规则,我当年可是吃过亏的:
- 域名倒写必须全小写——别问我为什么,Google就是这么规定的。你写成
Vendor.Example,编译直接挂。 - 模块名用单数——
light不是lights,camera不是cameras。我见过有人写sensors,结果生成代码时命名空间全乱了。 - 版本号用@分隔——主版本号不兼容时递增,次版本号向后兼容时递增。这个跟语义化版本号一个道理。
我曾经踩过的坑:有一次我把包名写成了android.hardware.light@2.0,但实际HAL实现里用的是@2.1的接口。结果服务死活注册不上,日志里就一句“interface not found”。查了两天才发现是版本号不匹配。记住:客户端和服务端的包名+版本号必须完全一致,一个字符都不能差。
3.3 数据类型详解——HIDL的“类型系统”
HIDL的数据类型,我把它分成三类:基础类型、容器类型、自定义类型。咱们一个一个看。
3.3.1 基础类型
| HIDL类型 | C++对应类型 | Java对应类型 | 说明 |
|---|---|---|---|
int8_t |
int8_t |
byte |
有符号8位 |
uint8_t |
uint8_t |
short |
无符号8位 |
int32_t |
int32_t |
int |
最常用 |
uint64_t |
uint64_t |
long |
大数场景 |
float |
float |
float |
单精度 |
double |
double |
double |
双精度 |
bool |
bool |
boolean |
布尔值 |
string |
hidl_string |
String |
UTF-8编码 |
这里有个细节:string在HIDL里是UTF-8编码的,不是UTF-16。我见过有人从Java层传了个中文String过来,C++端用std::string接,结果乱码了。为什么?因为Java的String是UTF-16,HIDL帮你做了转换,但如果你在C++端又手动转了一次,就出问题了。
3.3.2 容器类型
HIDL提供了三种容器:
vec<T>——动态数组。对应C++的hidl_vec<T>,Java的ArrayList<T>。hidl_array<T, N>——固定大小数组。这个在C++端是模板类,Java端直接生成数组。Memory——共享内存的抽象。用于传递大块数据,比如相机帧。
我的建议:能用vec就别用固定数组。为什么?因为HIDL的固定数组在跨进程传递时,大小是硬编码的。你想想看,如果以后要支持更多数量的传感器,改数组大小就得改接口版本号,多麻烦。用vec就灵活多了。
3.3.3 自定义类型——enum、struct、union
这才是HIDL的精髓。你可以定义自己的数据类型:
enum Status : int32_t {
SUCCESS = 0,
ERROR_GENERIC = -1,
ERROR_NOT_SUPPORTED = -2,
};
struct LightState {
uint8_t brightness;
uint32_t color;
string name;
};
union Data {
int32_t intVal;
float floatVal;
};
注意几个要点:
- enum必须指定底层类型——默认是
int32_t,但你可以改成uint8_t节省空间。 - struct里不能有指针——HIDL是跨进程的,指针传不过去。用
vec或string代替。 - union不常用——因为跨进程传递时,接收方不知道当前是哪个成员有效。我一般用
struct加一个discriminator字段来模拟。
3.4 方法声明——输入、输出与回调
HIDL的方法声明有三种形式:
| 形式 | 语法 | 说明 |
|---|---|---|
| 同步调用 | methodName(T arg) generates (R result); |
最常用,客户端阻塞等待 |
| 单向调用 | oneway methodName(T arg); |
不等待返回,适合通知类场景 |
| 回调模式 | methodName(T arg) generates (R result) callback; |
服务端主动回调客户端 |
我个人习惯:能用同步调用就别用oneway。为什么?因为oneway没有返回值,你没法知道服务端到底处理了没有。调试的时候特别痛苦。我有个项目,用oneway发了一个关灯指令,结果灯没关,查了半天发现是服务端进程挂了,但客户端完全不知道。
注意:oneway方法不能有generates子句。你想啊,都不等返回了,还生成什么结果?这个语法错误编译期就能发现,但架不住有人手滑写错。
3.5 知识结构总览
下面这张图,把HIDL接口定义的核心要素串起来了。你写.hal文件时,脑子里要有这个结构:
3.6 实战建议——写.hal文件时的几个习惯
最后,分享几个我个人的习惯:
- 每个接口单独一个文件——别把所有接口塞到一个.hal里。我见过一个项目,一个文件里写了十几个接口,编译慢不说,改一个接口所有人都得重新生成代码。
- 枚举值从0开始——虽然HIDL允许你指定任意值,但0通常表示“成功”或“正常”。这个习惯跟C/C++的bool兼容,也方便调试。
- 方法参数不超过5个——参数多了,用struct包起来。为什么?因为HIDL的IPC传输有大小限制,参数太多容易超过阈值,而且代码可读性也差。
- 版本号从1.0开始——别从0.x开始。Google的惯例就是1.0起跳,你非要搞个0.1,别人看了会觉得你不专业。
一个小技巧:写.hal文件时,先用hidl-gen -L check命令检查语法。这个工具会告诉你包名对不对、类型有没有拼错、版本号格式是否合法。我每次写完都会跑一遍,省得编译时再被报错打断思路。